diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java index 41ccdf0d5..81cfa1a31 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java @@ -15,9 +15,9 @@ */ package org.springframework.data.relational.core.dialect; -import org.springframework.data.relational.core.sql.render.SelectRenderContext; import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.data.relational.core.sql.render.SelectRenderContext; /** * Represents a dialect that is implemented by a particular database. Please note that not all features are supported by @@ -63,4 +63,14 @@ public interface Dialect { default IdentifierProcessing getIdentifierProcessing() { return IdentifierProcessing.ANSI; } + + /** + * Returns the {@link Escaper} used for {@code LIKE} value escaping. + * + * @return the {@link Escaper} used for {@code LIKE} value escaping. + * @since 2.0 + */ + default Escaper getLikeEscaper() { + return Escaper.DEFAULT; + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java index 9242d4a0a..038a30092 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java @@ -31,7 +31,7 @@ public class SqlServerDialect extends AbstractDialect { */ public static final SqlServerDialect INSTANCE = new SqlServerDialect(); - protected SqlServerDialect() { } + protected SqlServerDialect() {} private static final LimitClause LIMIT_CLAUSE = new LimitClause() { @@ -84,6 +84,15 @@ public class SqlServerDialect extends AbstractDialect { return LIMIT_CLAUSE; } + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.Dialect#getLikeEscaper() + */ + @Override + public Escaper getLikeEscaper() { + return Escaper.DEFAULT.withRewriteFor("[", "]"); + } + /* * (non-Javadoc) * @see org.springframework.data.relational.core.dialect.AbstractDialect#getSelectContext() diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Criteria.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Criteria.java index a6b3d3abd..10626797c 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Criteria.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Criteria.java @@ -22,6 +22,7 @@ import java.util.List; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.data.util.Pair; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -93,7 +94,6 @@ public class Criteria implements CriteriaDefinition { this.ignoreCase = false; } - /** * Static factory method to create an empty Criteria. * @@ -417,6 +417,24 @@ public class Criteria implements CriteriaDefinition { */ Criteria notIn(Collection values); + /** + * Creates a {@link Criteria} using between ({@literal BETWEEN begin AND end}). + * + * @param begin must not be {@literal null}. + * @param end must not be {@literal null}. + * @since 2.2 + */ + Criteria between(Object begin, Object end); + + /** + * Creates a {@link Criteria} using not between ({@literal NOT BETWEEN begin AND end}). + * + * @param begin must not be {@literal null}. + * @param end must not be {@literal null}. + * @since 2.2 + */ + Criteria notBetween(Object begin, Object end); + /** * Creates a {@link Criteria} using less-than ({@literal <}). * @@ -582,6 +600,32 @@ public class Criteria implements CriteriaDefinition { return createCriteria(Comparator.NOT_IN, values); } + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#between(java.lang.Object, java.lang.Object) + */ + @Override + public Criteria between(Object begin, Object end) { + + Assert.notNull(begin, "Begin value must not be null!"); + Assert.notNull(end, "End value must not be null!"); + + return createCriteria(Comparator.BETWEEN, Pair.of(begin, end)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#notBetween(java.lang.Object, java.lang.Object) + */ + @Override + public Criteria notBetween(Object begin, Object end) { + + Assert.notNull(begin, "Begin value must not be null!"); + Assert.notNull(end, "End value must not be null!"); + + return createCriteria(Comparator.NOT_BETWEEN, Pair.of(begin, end)); + } + /* * (non-Javadoc) * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#lessThan(java.lang.Object) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/CriteriaDefinition.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/CriteriaDefinition.java index 8fbe7ae33..8b922f350 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/CriteriaDefinition.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/CriteriaDefinition.java @@ -135,6 +135,6 @@ public interface CriteriaDefinition { } enum Comparator { - INITIAL, EQ, NEQ, LT, LTE, GT, GTE, IS_NULL, IS_NOT_NULL, LIKE, NOT_LIKE, NOT_IN, IN, IS_TRUE, IS_FALSE + INITIAL, EQ, NEQ, BETWEEN, NOT_BETWEEN, LT, LTE, GT, GTE, IS_NULL, IS_NOT_NULL, LIKE, NOT_LIKE, NOT_IN, IN, IS_TRUE, IS_FALSE } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/ValueFunction.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/ValueFunction.java new file mode 100644 index 000000000..9fe41b4c4 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/ValueFunction.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.query; + +import java.util.function.Function; +import java.util.function.Supplier; + +import org.springframework.data.relational.core.dialect.Escaper; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Represents a value function to return arbitrary values that can be escaped before returning the actual value. Can be + * used with the criteria API for deferred value retrieval. + * + * @author Mark Paluch + * @since 2.0 + * @see Escaper + * @see Supplier + */ +@FunctionalInterface +public interface ValueFunction extends Function { + + /** + * Produces a value by considering the given {@link Escaper}. + * + * @param escaper the escaper to use. + * @return the return value, may be {@literal null}. + */ + @Nullable + @Override + T apply(Escaper escaper); + + /** + * Adapts this value factory into a {@link Supplier} by using the given {@link Escaper}. + * + * @param escaper the escaper to use. + * @return the value factory + */ + default Supplier toSupplier(Escaper escaper) { + + Assert.notNull(escaper, "Escaper must not be null"); + + return () -> apply(escaper); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Between.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Between.java new file mode 100644 index 000000000..2de5fb539 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Between.java @@ -0,0 +1,96 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.sql; + +import org.springframework.util.Assert; + +/** + * BETWEEN {@link Condition} comparing between {@link Expression}s. + *

+ * Results in a rendered condition: {@code BETWEEN AND }. + * + * @author Mark Paluch + * @since 2.2 + */ +public class Between extends AbstractSegment implements Condition { + + private final Expression column; + private final Expression begin; + private final Expression end; + private final boolean negated; + + private Between(Expression column, Expression begin, Expression end, boolean negated) { + + super(column, begin, end); + + this.column = column; + this.begin = begin; + this.end = end; + this.negated = negated; + } + + /** + * Creates a new {@link Between} {@link Condition} given two {@link Expression}s. + * + * @param columnOrExpression left side of the comparison. + * @param begin begin value of the comparison. + * @param end end value of the comparison. + * @return the {@link Between} condition. + */ + public static Between create(Expression columnOrExpression, Expression begin, Expression end) { + + Assert.notNull(columnOrExpression, "Column or expression must not be null!"); + Assert.notNull(begin, "Begin value must not be null!"); + Assert.notNull(end, "end value must not be null!"); + + return new Between(columnOrExpression, begin, end, false); + } + + /** + * @return the column {@link Expression}. + */ + public Expression getColumn() { + return column; + } + + /** + * @return the begin {@link Expression}. + */ + public Expression getBegin() { + return begin; + } + + /** + * @return the end {@link Expression}. + */ + public Expression getEnd() { + return end; + } + + public boolean isNegated() { + return negated; + } + + @Override + public Between not() { + return new Between(this.column, this.begin, this.end, !negated); + } + + @Override + public String toString() { + return column.toString() + " BETWEEN " + begin.toString() + " AND " + end.toString(); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/BooleanLiteral.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/BooleanLiteral.java new file mode 100644 index 000000000..1dcf9c37b --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/BooleanLiteral.java @@ -0,0 +1,47 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.sql; + +/** + * Represents a {@link Boolean} literal. + * + * @author Mark Paluch + * @since 2.0 + */ +public class BooleanLiteral extends Literal { + + BooleanLiteral(boolean content) { + super(content); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.Literal#getContent() + */ + @Override + public Boolean getContent() { + return super.getContent(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.Literal#toString() + */ + @Override + public String toString() { + return getContent() ? "TRUE" : "FALSE"; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Column.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Column.java index 170709151..39f42c4f2 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Column.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Column.java @@ -163,7 +163,31 @@ public class Column extends AbstractSegment implements Expression, Named { } /** - * Creates a {@code <} (less) {@link Condition} {@link Condition}. + * Creates a {@code BETWEEN} {@link Condition}. + * + * @param begin begin value for the comparison. + * @param end end value for the comparison. + * @return the {@link Between} condition. + * @since 2.0 + */ + public Between between(Expression begin, Expression end) { + return Conditions.between(this, begin, end); + } + + /** + * Creates a {@code NOT BETWEEN} {@link Condition}. + * + * @param begin begin value for the comparison. + * @param end end value for the comparison. + * @return the {@link Between} condition. + * @since 2.0 + */ + public Between notBetween(Expression begin, Expression end) { + return Conditions.notBetween(this, begin, end); + } + + /** + * Creates a {@code <} (less) {@link Condition}. * * @param expression right side of the comparison. * @return the {@link Comparison} condition. @@ -173,7 +197,7 @@ public class Column extends AbstractSegment implements Expression, Named { } /** - * CCreates a {@code <=} (greater ) {@link Condition} {@link Condition}. + * CCreates a {@code <=} (greater) {@link Condition}. * * @param expression right side of the comparison. * @return the {@link Comparison} condition. @@ -193,7 +217,7 @@ public class Column extends AbstractSegment implements Expression, Named { } /** - * Creates a {@code <=} (greater or equal to) {@link Condition} {@link Condition}. + * Creates a {@code <=} (greater or equal to) {@link Condition}. * * @param expression right side of the comparison. * @return the {@link Comparison} condition. @@ -212,6 +236,17 @@ public class Column extends AbstractSegment implements Expression, Named { return Conditions.like(this, expression); } + /** + * Creates a {@code NOT LIKE} {@link Condition}. + * + * @param expression right side of the comparison. + * @return the {@link Like} condition. + * @since 2.0 + */ + public Like notLike(Expression expression) { + return Conditions.notLike(this, expression); + } + /** * Creates a new {@link In} {@link Condition} given right {@link Expression}s. * diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java index 0e3c9bcc1..bcfb58c2e 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java @@ -87,6 +87,32 @@ public abstract class Conditions { return Comparison.create(leftColumnOrExpression, "!=", rightColumnOrExpression); } + /** + * Creates a {@code BETWEEN} {@link Condition}. + * + * @param columnOrExpression left side of the comparison. + * @param begin begin value of the comparison. + * @param end end value of the comparison. + * @return the {@link Comparison} condition. + * @since 2.0 + */ + public static Between between(Expression columnOrExpression, Expression begin, Expression end) { + return Between.create(columnOrExpression, begin, end); + } + + /** + * Creates a {@code NOT BETWEEN} {@link Condition}. + * + * @param columnOrExpression left side of the comparison. + * @param begin begin value of the comparison. + * @param end end value of the comparison. + * @return the {@link Comparison} condition. + * @since 2.0 + */ + public static Between notBetween(Expression columnOrExpression, Expression begin, Expression end) { + return between(columnOrExpression, begin, end).not(); + } + /** * Creates a {@code <} (less) {@link Condition} comparing {@code left} is less than {@code right}. * @@ -144,6 +170,18 @@ public abstract class Conditions { return Like.create(leftColumnOrExpression, rightColumnOrExpression); } + /** + * Creates a {@code NOT LIKE} {@link Condition}. + * + * @param leftColumnOrExpression left side of the comparison. + * @param rightColumnOrExpression right side of the comparison. + * @return the {@link Comparison} condition. + * @since 2.0 + */ + public static Like notLike(Expression leftColumnOrExpression, Expression rightColumnOrExpression) { + return Like.create(leftColumnOrExpression, rightColumnOrExpression).not(); + } + /** * Creates a {@code IN} {@link Condition clause}. * diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java index 67f6c37a1..689b14bc8 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java @@ -18,6 +18,7 @@ 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; @@ -60,6 +61,34 @@ public class Functions { return SimpleFunction.create("COUNT", new ArrayList<>(columns)); } + /** + * Creates a new {@code UPPER} function. + * + * @param expression expression to apply count, must not be {@literal null}. + * @return the new {@link SimpleFunction upper function} for {@code expression}. + * @since 2.0 + */ + public static SimpleFunction upper(Expression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + + return SimpleFunction.create("UPPER", Collections.singletonList(expression)); + } + + /** + * Creates a new {@code LOWER} function. + * + * @param expression expression to apply lower, must not be {@literal null}. + * @return the new {@link SimpleFunction lower function} for {@code expression}. + * @since 2.0 + */ + public static SimpleFunction lower(Expression expression) { + + Assert.notNull(expression, "Columns must not be null!"); + + return SimpleFunction.create("LOWER", Collections.singletonList(expression)); + } + // Utility constructor. private Functions() {} } 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 index ed75515ce..58cdb8734 100644 --- 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 @@ -29,13 +29,15 @@ public class Like extends AbstractSegment implements Condition { private final Expression left; private final Expression right; + private final boolean negated; - private Like(Expression left, Expression right) { + private Like(Expression left, Expression right, boolean negated) { super(left, right); this.left = left; this.right = right; + this.negated = negated; } /** @@ -50,7 +52,7 @@ public class Like extends AbstractSegment implements Condition { Assert.notNull(leftColumnOrExpression, "Left expression must not be null!"); Assert.notNull(rightColumnOrExpression, "Right expression must not be null!"); - return new Like(leftColumnOrExpression, rightColumnOrExpression); + return new Like(leftColumnOrExpression, rightColumnOrExpression, false); } /** @@ -67,6 +69,15 @@ public class Like extends AbstractSegment implements Condition { return right; } + public boolean isNegated() { + return negated; + } + + @Override + public Like not() { + return new Like(this.left, this.right, !negated); + } + @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/SQL.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SQL.java index 50f824b7d..e1d602ffb 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SQL.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SQL.java @@ -78,6 +78,18 @@ public abstract class SQL { return new NamedBindMarker(name); } + /** + * Creates a new {@link BooleanLiteral} rendering either {@code TRUE} or {@literal FALSE} depending on the given + * {@code value}. + * + * @param value the literal content. + * @return a new {@link BooleanLiteral}. + * @since 2.0 + */ + public static BooleanLiteral literalOf(boolean value) { + return new BooleanLiteral(value); + } + /** * Creates a new {@link StringLiteral} from the {@code content}. * diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/BetweenVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/BetweenVisitor.java new file mode 100644 index 000000000..03d319d05 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/BetweenVisitor.java @@ -0,0 +1,123 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.sql.render; + +import org.springframework.data.relational.core.sql.Between; +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 + * @see Between + * @since 2.0 + */ +class BetweenVisitor extends FilteredSubtreeVisitor { + + private final Between between; + private final RenderContext context; + private final RenderTarget target; + private final StringBuilder part = new StringBuilder(); + private boolean renderedTestExpression = false; + private boolean renderedPreamble = false; + private boolean done = false; + private @Nullable PartRenderer current; + + BetweenVisitor(Between condition, RenderContext context, RenderTarget target) { + super(it -> it == condition); + this.between = 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 && !done) { + + if (renderedPreamble) { + + part.append(" AND "); + part.append(current.getRenderedPart()); + done = true; + } + + if (renderedTestExpression && !renderedPreamble) { + + part.append(' '); + + if (between.isNegated()) { + part.append("NOT "); + } + + part.append("BETWEEN "); + renderedPreamble = true; + part.append(current.getRenderedPart()); + } + + if (!renderedTestExpression) { + part.append(current.getRenderedPart()); + renderedTestExpression = true; + } + + 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/ComparisonVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ComparisonVisitor.java index 3f9a37364..0811e1ccc 100644 --- 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 @@ -18,6 +18,7 @@ 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.SimpleFunction; import org.springframework.data.relational.core.sql.Visitable; import org.springframework.lang.Nullable; 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 index 86877480e..49b1d33b3 100644 --- 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 @@ -16,12 +16,13 @@ package org.springframework.data.relational.core.sql.render; import org.springframework.data.relational.core.sql.AndCondition; +import org.springframework.data.relational.core.sql.Between; import org.springframework.data.relational.core.sql.Comparison; import org.springframework.data.relational.core.sql.Condition; -import org.springframework.data.relational.core.sql.NestedCondition; 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.NestedCondition; import org.springframework.data.relational.core.sql.OrCondition; import org.springframework.lang.Nullable; @@ -75,6 +76,10 @@ class ConditionVisitor extends TypedSubtreeVisitor implements PartRen return new IsNullVisitor(context, builder::append); } + if (segment instanceof Between) { + return new BetweenVisitor((Between) segment, context, builder::append); + } + if (segment instanceof Comparison) { return new ComparisonVisitor(context, (Comparison) segment, builder::append); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java index 408da4df2..8cc95b234 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java @@ -21,6 +21,7 @@ import org.springframework.data.relational.core.sql.Condition; import org.springframework.data.relational.core.sql.Expression; import org.springframework.data.relational.core.sql.Literal; import org.springframework.data.relational.core.sql.Named; +import org.springframework.data.relational.core.sql.SimpleFunction; import org.springframework.data.relational.core.sql.SubselectExpression; import org.springframework.data.relational.core.sql.Visitable; import org.springframework.lang.Nullable; @@ -59,6 +60,13 @@ class ExpressionVisitor extends TypedSubtreeVisitor implements PartR return Delegation.delegateTo(visitor); } + if (segment instanceof SimpleFunction) { + + SimpleFunctionVisitor visitor = new SimpleFunctionVisitor(context); + partRenderer = visitor; + return Delegation.delegateTo(visitor); + } + if (segment instanceof Column) { Column column = (Column) 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 index e5a09e64c..afc9044aa 100644 --- 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 @@ -31,6 +31,7 @@ import org.springframework.lang.Nullable; */ class LikeVisitor extends FilteredSubtreeVisitor { + private final Like like; private final RenderContext context; private final RenderTarget target; private final StringBuilder part = new StringBuilder(); @@ -38,6 +39,7 @@ class LikeVisitor extends FilteredSubtreeVisitor { LikeVisitor(Like condition, RenderContext context, RenderTarget target) { super(it -> it == condition); + this.like = condition; this.context = context; this.target = target; } @@ -73,7 +75,14 @@ class LikeVisitor extends FilteredSubtreeVisitor { if (current != null) { if (part.length() != 0) { - part.append(" LIKE "); + + part.append(' '); + + if (like.isNegated()) { + part.append("NOT "); + } + + part.append("LIKE "); } part.append(current.getRenderedPart()); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SimpleFunctionVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SimpleFunctionVisitor.java new file mode 100644 index 000000000..1e3e62e6b --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SimpleFunctionVisitor.java @@ -0,0 +1,93 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.sql.render; + +import org.springframework.data.relational.core.sql.SimpleFunction; +import org.springframework.data.relational.core.sql.Visitable; + +/** + * Renderer for {@link org.springframework.data.relational.core.sql.SimpleFunction}. Uses a {@link RenderTarget} to call + * back for render results. + * + * @author Mark Paluch + * @author Jens Schauder + * @since 1.1 + */ +class SimpleFunctionVisitor extends TypedSingleConditionRenderSupport implements PartRenderer { + + private final StringBuilder part = new StringBuilder(); + private boolean needsComma = false; + private String functionName; + + SimpleFunctionVisitor(RenderContext context) { + super(context); + } + + /* + * (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()) { + + if (needsComma) { + part.append(", "); + } + + if (part.length() == 0) { + part.append(functionName).append("("); + } + part.append(consumeRenderedPart()); + needsComma = true; + } + + return super.leaveNested(segment); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterMatched(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation enterMatched(SimpleFunction segment) { + + functionName = segment.getFunctionName(); + 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(SimpleFunction segment) { + + part.append(")"); + + return super.leaveMatched(segment); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.PartRenderer#getRenderedPart() + */ + @Override + public CharSequence getRenderedPart() { + return part; + } +} 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 index e5c6a118c..6b27b8ea6 100644 --- 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 @@ -28,7 +28,7 @@ import org.springframework.util.Assert; * @author Mark Paluch * @since 1.1 */ -abstract class TypedSingleConditionRenderSupport extends TypedSubtreeVisitor { +abstract class TypedSingleConditionRenderSupport extends TypedSubtreeVisitor { private final RenderContext context; private @Nullable PartRenderer current; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/CriteriaFactory.java b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/CriteriaFactory.java index 686a8e019..708162dc6 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/CriteriaFactory.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/CriteriaFactory.java @@ -66,8 +66,7 @@ class CriteriaFactory { case BETWEEN: { ParameterMetadata geParamMetadata = parameterMetadataProvider.next(part); ParameterMetadata leParamMetadata = parameterMetadataProvider.next(part); - return criteriaStep.greaterThanOrEquals(geParamMetadata.getValue()).and(propertyName) - .lessThanOrEquals(leParamMetadata.getValue()); + return criteriaStep.between(geParamMetadata.getValue(), leParamMetadata.getValue()); } case AFTER: case GREATER_THAN: { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/ParameterMetadataProvider.java b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/ParameterMetadataProvider.java index a2413ab5d..1f27d58ed 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/ParameterMetadataProvider.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/ParameterMetadataProvider.java @@ -20,6 +20,7 @@ import java.util.Iterator; import java.util.List; import org.springframework.data.relational.core.dialect.Escaper; +import org.springframework.data.relational.core.query.ValueFunction; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.parser.Part; @@ -28,40 +29,26 @@ import org.springframework.util.Assert; /** * Helper class to allow easy creation of {@link ParameterMetadata}s. - *

- * This class is an adapted version of {@code org.springframework.data.jpa.repository.query.ParameterMetadataProvider} - * from Spring Data JPA project. * * @author Roman Chigvintsev + * @author Mark Paluch * @since 2.0 */ -public class ParameterMetadataProvider implements Iterable { +class ParameterMetadataProvider implements Iterable { private static final Object VALUE_PLACEHOLDER = new Object(); private final Iterator bindableParameterIterator; private final Iterator bindableParameterValueIterator; private final List parameterMetadata = new ArrayList<>(); - private final Escaper escaper; /** * Creates new instance of this class with the given {@link RelationalParameterAccessor} and {@link Escaper}. * * @param accessor relational parameter accessor (must not be {@literal null}). - * @param escaper escaper for LIKE operator parameters (must not be {@literal null}) */ - public ParameterMetadataProvider(RelationalParameterAccessor accessor, Escaper escaper) { - this(accessor.getBindableParameters(), accessor.iterator(), escaper); - } - - /** - * Creates new instance of this class with the given {@link Parameters} and {@link Escaper}. - * - * @param parameters method parameters (must not be {@literal null}) - * @param escaper escaper for LIKE operator parameters (must not be {@literal null}) - */ - public ParameterMetadataProvider(Parameters parameters, Escaper escaper) { - this(parameters, null, escaper); + public ParameterMetadataProvider(RelationalParameterAccessor accessor) { + this(accessor.getBindableParameters(), accessor.iterator()); } /** @@ -70,16 +57,14 @@ public class ParameterMetadataProvider implements Iterable { * * @param bindableParameterValueIterator iterator over bindable parameter values * @param parameters method parameters (must not be {@literal null}) - * @param escaper escaper for LIKE operator parameters (must not be {@literal null}) */ private ParameterMetadataProvider(Parameters parameters, - @Nullable Iterator bindableParameterValueIterator, Escaper escaper) { + @Nullable Iterator bindableParameterValueIterator) { + Assert.notNull(parameters, "Parameters must not be null!"); - Assert.notNull(escaper, "Like escaper must not be null!"); this.bindableParameterIterator = parameters.getBindableParameters().iterator(); this.bindableParameterValueIterator = bindableParameterValueIterator; - this.escaper = escaper; } @Override @@ -91,8 +76,10 @@ public class ParameterMetadataProvider implements Iterable { * Creates new instance of {@link ParameterMetadata} for the given {@link Part} and next {@link Parameter}. */ public ParameterMetadata next(Part part) { + Assert.isTrue(bindableParameterIterator.hasNext(), () -> String.format("No parameter available for part %s.", part)); + Parameter parameter = bindableParameterIterator.next(); String parameterName = getParameterName(parameter, part.getProperty().getSegment()); Object parameterValue = getParameterValue(); @@ -104,10 +91,12 @@ public class ParameterMetadataProvider implements Iterable { ParameterMetadata metadata = new ParameterMetadata(parameterName, preparedParameterValue, parameterType); parameterMetadata.add(metadata); + return metadata; } private String getParameterName(Parameter parameter, String defaultName) { + if (parameter.isExplicitlyNamed()) { return parameter.getName().orElseThrow(() -> new IllegalArgumentException("Parameter needs to be named")); } @@ -144,18 +133,20 @@ public class ParameterMetadataProvider implements Iterable { @Nullable protected Object prepareParameterValue(@Nullable Object value, Class valueType, Part.Type partType) { - if (value != null && String.class == valueType) { - switch (partType) { - case STARTING_WITH: - return escaper.escape(value.toString()) + "%"; - case ENDING_WITH: - return "%" + escaper.escape(value.toString()); - case CONTAINING: - case NOT_CONTAINING: - return "%" + escaper.escape(value.toString()) + "%"; - } + if (value == null || !CharSequence.class.isAssignableFrom(valueType)) { + return value; } - return value; + switch (partType) { + case STARTING_WITH: + return (ValueFunction) escaper -> escaper.escape(value.toString()) + "%"; + case ENDING_WITH: + return (ValueFunction) escaper -> "%" + escaper.escape(value.toString()); + case CONTAINING: + case NOT_CONTAINING: + return (ValueFunction) escaper -> "%" + escaper.escape(value.toString()) + "%"; + default: + return value; + } } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalQueryCreator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalQueryCreator.java index 1a86462be..79206b3ab 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalQueryCreator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalQueryCreator.java @@ -26,9 +26,10 @@ import org.springframework.data.util.Streamable; import org.springframework.util.Assert; /** - * Implementation of {@link AbstractQueryCreator} that creates {@link PreparedOperation} from a {@link PartTree}. + * Implementation of {@link AbstractQueryCreator} that creates a query from a {@link PartTree}. * * @author Roman Chigvintsev + * @author Mark Paluch * @since 2.0 */ public abstract class RelationalQueryCreator extends AbstractQueryCreator { @@ -39,20 +40,21 @@ public abstract class RelationalQueryCreator extends AbstractQueryCreator extends AbstractQueryCreator extends AbstractQueryCreator extends AbstractQueryCreator parts = () -> tree.stream().flatMap(Streamable::stream).iterator(); for (Part part : parts) { int numberOfArguments = part.getNumberOfArguments(); diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/query/CriteriaTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/query/CriteriaTests.java deleted file mode 100644 index 5a2de5cbb..000000000 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/query/CriteriaTests.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.relational.core.query; - -import static org.junit.Assert.*; - -/** - * @author Mark Paluch - */ -public class CriteriaTests { - -} 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 index 3739be6aa..28b7c0bdc 100644 --- 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 @@ -21,6 +21,7 @@ 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.StatementBuilder; import org.springframework.data.relational.core.sql.Table; @@ -34,6 +35,7 @@ public class ConditionRendererUnitTests { Table table = Table.create("my_table"); Column left = table.column("left"); Column right = table.column("right"); + Column other = table.column("other"); @Test // DATAJDBC-309 public void shouldRenderEquals() { @@ -43,6 +45,15 @@ public class ConditionRendererUnitTests { assertThat(sql).endsWith("WHERE my_table.left = my_table.right"); } + @Test // DATAJDBC-514 + public void shouldRenderEqualsCaseInsensitive() { + + String sql = SqlRenderer.toString(StatementBuilder.select(left).from(table) + .where(Conditions.isEqual(Functions.upper(left), Functions.upper(right))).build()); + + assertThat(sql).endsWith("WHERE UPPER(my_table.left) = UPPER(my_table.right)"); + } + @Test // DATAJDBC-490 public void shouldRenderEqualsNested() { @@ -104,6 +115,24 @@ public class ConditionRendererUnitTests { assertThat(sql).endsWith("WHERE my_table.left < my_table.right"); } + @Test // DATAJDBC-513 + public void shouldRenderBetween() { + + String sql = SqlRenderer + .toString(StatementBuilder.select(left).from(table).where(left.between(right, other)).build()); + + assertThat(sql).endsWith("WHERE my_table.left BETWEEN my_table.right AND my_table.other"); + } + + @Test // DATAJDBC-513 + public void shouldRenderNotBetween() { + + String sql = SqlRenderer + .toString(StatementBuilder.select(left).from(table).where(left.notBetween(right, other)).build()); + + assertThat(sql).endsWith("WHERE my_table.left NOT BETWEEN my_table.right AND my_table.other"); + } + @Test // DATAJDBC-309 public void shouldRenderIsLessOrEqualTo() { @@ -146,6 +175,14 @@ public class ConditionRendererUnitTests { assertThat(sql).endsWith("WHERE my_table.left LIKE my_table.right"); } + @Test // DATAJDBC-513 + public void shouldRenderNotLike() { + + String sql = SqlRenderer.toString(StatementBuilder.select(left).from(table).where(left.notLike(right)).build()); + + assertThat(sql).endsWith("WHERE my_table.left NOT LIKE my_table.right"); + } + @Test // DATAJDBC-309 public void shouldRenderIsNull() { diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/degraph/DependencyTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/degraph/DependencyTests.java index f5b5c1f91..5af2d83ee 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/degraph/DependencyTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/degraph/DependencyTests.java @@ -51,6 +51,7 @@ public class DependencyTests { // include only Spring Data related classes (for example no JDK code) .including("org.springframework.data.**") // .excluding("org.springframework.data.relational.core.sql.**") // + .excluding("org.springframework.data.repository.query.parser.**") // .filterClasspath(new AbstractFunction1() { @Override public Object apply(String s) { // diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/repository/query/ParameterMetadataProviderUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/repository/query/ParameterMetadataProviderUnitTests.java new file mode 100644 index 000000000..e38007bf6 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/repository/query/ParameterMetadataProviderUnitTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.repository.query; + +import static org.assertj.core.api.Assertions.*; + +import java.lang.reflect.Method; + +import org.junit.Test; + +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.relational.core.dialect.Escaper; +import org.springframework.data.relational.core.query.ValueFunction; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.parser.PartTree; + +/** + * Unit tests for {@link ParameterMetadataProvider}. + * + * @author Mark Paluch + */ +public class ParameterMetadataProviderUnitTests { + + @Test // DATAJDBC-514 + public void shouldCreateValueFunctionForContains() throws Exception { + + ParameterMetadata metadata = getParameterMetadata("findByNameContains", "hell%o"); + + assertThat(metadata.getValue()).isInstanceOf(ValueFunction.class); + ValueFunction function = (ValueFunction) metadata.getValue(); + assertThat(function.apply(Escaper.DEFAULT)).isEqualTo("%hell\\%o%"); + } + + @Test // DATAJDBC-514 + public void shouldCreateValueFunctionForStartingWith() throws Exception { + + ParameterMetadata metadata = getParameterMetadata("findByNameStartingWith", "hell%o"); + + assertThat(metadata.getValue()).isInstanceOf(ValueFunction.class); + ValueFunction function = (ValueFunction) metadata.getValue(); + assertThat(function.apply(Escaper.DEFAULT)).isEqualTo("hell\\%o%"); + } + + @Test // DATAJDBC-514 + public void shouldCreateValue() throws Exception { + + ParameterMetadata metadata = getParameterMetadata("findByName", "hell%o"); + + assertThat(metadata.getValue()).isEqualTo("hell%o"); + } + + private ParameterMetadata getParameterMetadata(String methodName, Object value) throws Exception { + + Method method = UserRepository.class.getMethod(methodName, String.class); + ParameterMetadataProvider provider = new ParameterMetadataProvider(new RelationalParametersParameterAccessor( + new RelationalQueryMethod(method, new DefaultRepositoryMetadata(UserRepository.class), + new SpelAwareProxyProjectionFactory()), + new Object[] { value })); + + PartTree tree = new PartTree(methodName, User.class); + + return provider.next(tree.getParts().iterator().next()); + } + + static class RelationalQueryMethod extends QueryMethod { + + public RelationalQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { + super(method, metadata, factory); + } + } + + interface UserRepository extends Repository { + + String findByNameStartingWith(String prefix); + + String findByNameContains(String substring); + + String findByName(String substring); + } + + static class User { + String name; + } +}