Polishing.
Refine temporal literal handling. Update documentation. See #3172 Original pull request: #3187
This commit is contained in:
@@ -18,10 +18,6 @@ package org.springframework.data.jpa.domain;
|
||||
import jakarta.persistence.metamodel.Attribute;
|
||||
import jakarta.persistence.metamodel.PluralAttribute;
|
||||
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -29,8 +25,15 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Sort option for queries that wraps JPA meta-model {@link Attribute}s for sorting.
|
||||
* Sort option for queries that wraps JPA metamodel {@link Attribute}s for sorting.
|
||||
* <p>
|
||||
* {@link JpaSort#unsafe} accepts unsafe sort expressions, i. e. the String provided is not necessarily a property but
|
||||
* can be an arbitrary expression piped into the query execution.
|
||||
*
|
||||
* @author Thomas Darimont
|
||||
* @author Oliver Gierke
|
||||
@@ -44,7 +47,7 @@ public class JpaSort extends Sort {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
|
||||
private JpaSort(Direction direction, List<Path<?, ?>> paths) {
|
||||
this(Collections.<Order>emptyList(), direction, paths);
|
||||
this(Collections.<Order> emptyList(), direction, paths);
|
||||
}
|
||||
|
||||
private JpaSort(List<Order> orders, @Nullable Direction direction, List<Path<?, ?>> paths) {
|
||||
@@ -76,7 +79,7 @@ public class JpaSort extends Sort {
|
||||
/**
|
||||
* Creates a new {@link JpaSort} for the given direction and attributes.
|
||||
*
|
||||
* @param direction the sorting direction.
|
||||
* @param direction the sorting direction.
|
||||
* @param attributes must not be {@literal null} or empty.
|
||||
*/
|
||||
public static JpaSort of(Direction direction, Attribute<?, ?>... attributes) {
|
||||
@@ -87,7 +90,7 @@ public class JpaSort extends Sort {
|
||||
* Creates a new {@link JpaSort} for the given direction and {@link Path}s.
|
||||
*
|
||||
* @param direction the sorting direction.
|
||||
* @param paths must not be {@literal null} or empty.
|
||||
* @param paths must not be {@literal null} or empty.
|
||||
*/
|
||||
public static JpaSort of(Direction direction, Path<?, ?>... paths) {
|
||||
return new JpaSort(direction, Arrays.asList(paths));
|
||||
@@ -96,7 +99,7 @@ public class JpaSort extends Sort {
|
||||
/**
|
||||
* Returns a new {@link JpaSort} with the given sorting criteria added to the current one.
|
||||
*
|
||||
* @param direction can be {@literal null}.
|
||||
* @param direction can be {@literal null}.
|
||||
* @param attributes must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
@@ -111,7 +114,7 @@ public class JpaSort extends Sort {
|
||||
* Returns a new {@link JpaSort} with the given sorting criteria added to the current one.
|
||||
*
|
||||
* @param direction can be {@literal null}.
|
||||
* @param paths must not be {@literal null}.
|
||||
* @param paths must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public JpaSort and(@Nullable Direction direction, Path<?, ?>... paths) {
|
||||
@@ -130,7 +133,7 @@ public class JpaSort extends Sort {
|
||||
/**
|
||||
* Returns a new {@link JpaSort} with the given sorting criteria added to the current one.
|
||||
*
|
||||
* @param direction can be {@literal null}.
|
||||
* @param direction can be {@literal null}.
|
||||
* @param properties must not be {@literal null} or empty.
|
||||
* @return
|
||||
*/
|
||||
@@ -148,7 +151,7 @@ public class JpaSort extends Sort {
|
||||
orders.add(new JpaOrder(direction, property));
|
||||
}
|
||||
|
||||
return new JpaSort(orders, direction, Collections.<Path<?, ?>>emptyList());
|
||||
return new JpaSort(orders, direction, Collections.<Path<?, ?>> emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,7 +222,7 @@ public class JpaSort extends Sort {
|
||||
/**
|
||||
* Creates new unsafe {@link JpaSort} based on given {@link Direction} and properties.
|
||||
*
|
||||
* @param direction must not be {@literal null}.
|
||||
* @param direction must not be {@literal null}.
|
||||
* @param properties must not be {@literal null} or empty.
|
||||
* @return
|
||||
*/
|
||||
@@ -235,7 +238,7 @@ public class JpaSort extends Sort {
|
||||
/**
|
||||
* Creates new unsafe {@link JpaSort} based on given {@link Direction} and properties.
|
||||
*
|
||||
* @param direction must not be {@literal null}.
|
||||
* @param direction must not be {@literal null}.
|
||||
* @param properties must not be {@literal null} or empty.
|
||||
* @return
|
||||
*/
|
||||
@@ -327,7 +330,7 @@ public class JpaSort extends Sort {
|
||||
* {@link Sort#DEFAULT_DIRECTION}
|
||||
*
|
||||
* @param direction can be {@literal null}, will default to {@link Sort#DEFAULT_DIRECTION}.
|
||||
* @param property must not be {@literal null}.
|
||||
* @param property must not be {@literal null}.
|
||||
*/
|
||||
private JpaOrder(@Nullable Direction direction, String property) {
|
||||
this(direction, property, NullHandling.NATIVE);
|
||||
@@ -337,8 +340,8 @@ public class JpaSort extends Sort {
|
||||
* Creates a new {@link Order} instance. if order is {@literal null} then order defaults to
|
||||
* {@link Sort#DEFAULT_DIRECTION}.
|
||||
*
|
||||
* @param direction can be {@literal null}, will default to {@link Sort#DEFAULT_DIRECTION}.
|
||||
* @param property must not be {@literal null}.
|
||||
* @param direction can be {@literal null}, will default to {@link Sort#DEFAULT_DIRECTION}.
|
||||
* @param property must not be {@literal null}.
|
||||
* @param nullHandlingHint can be {@literal null}, will default to {@link NullHandling#NATIVE}.
|
||||
*/
|
||||
private JpaOrder(@Nullable Direction direction, String property, NullHandling nullHandlingHint) {
|
||||
@@ -346,7 +349,7 @@ public class JpaSort extends Sort {
|
||||
}
|
||||
|
||||
private JpaOrder(@Nullable Direction direction, String property, boolean ignoreCase, NullHandling nullHandling,
|
||||
boolean unsafe) {
|
||||
boolean unsafe) {
|
||||
|
||||
super(direction, property, ignoreCase, nullHandling);
|
||||
this.unsafe = unsafe;
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import static java.time.format.DateTimeFormatter.*;
|
||||
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.Expression;
|
||||
import jakarta.persistence.criteria.From;
|
||||
@@ -24,9 +26,18 @@ import jakarta.persistence.criteria.TemporalField;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeFormatterBuilder;
|
||||
import java.time.temporal.Temporal;
|
||||
import java.util.Collection;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Locale;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.antlr.v4.runtime.CharStreams;
|
||||
import org.antlr.v4.runtime.CommonTokenStream;
|
||||
@@ -47,24 +58,48 @@ import org.springframework.util.Assert;
|
||||
* @author Mark Paluch
|
||||
* @since 4.0
|
||||
*/
|
||||
@SuppressWarnings("ConstantValue")
|
||||
@SuppressWarnings({ "unchecked", "rawtypes", "ConstantValue" })
|
||||
class HqlOrderExpressionVisitor extends HqlBaseVisitor<Expression<?>> {
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME = new DateTimeFormatterBuilder().parseCaseInsensitive()
|
||||
.append(ISO_LOCAL_DATE).optionalStart().appendLiteral(' ').optionalEnd().optionalStart().appendLiteral('T')
|
||||
.optionalEnd().append(ISO_LOCAL_TIME).optionalStart().appendLiteral(' ').optionalEnd().optionalStart()
|
||||
.appendZoneOrOffsetId().optionalEnd().toFormatter();
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd",
|
||||
Locale.ENGLISH);
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_TIME = DateTimeFormatter.ofPattern("HH:mm:ss",
|
||||
Locale.ENGLISH);
|
||||
|
||||
private static final String UNSUPPORTED_TEMPLATE = "We can't handle %s in an ORDER BY clause through JpaSort.unsafe(…)";
|
||||
|
||||
private final CriteriaBuilder cb;
|
||||
private final Path<?> from;
|
||||
private static String UNSUPPORTED_TEMPLATE = "We can't handle %s in an ORDER BY clause through JpaSort.unsafe";
|
||||
private final BiFunction<From<?, ?>, PropertyPath, Expression<?>> expressionFactory;
|
||||
|
||||
HqlOrderExpressionVisitor(CriteriaBuilder cb, Path<?> from) {
|
||||
/**
|
||||
* @param cb criteria builder.
|
||||
* @param from from path (i.e. root entity).
|
||||
* @param expressionFactory factory to create expressions such as
|
||||
* {@link QueryUtils#toExpressionRecursively(From, PropertyPath)}.
|
||||
*/
|
||||
HqlOrderExpressionVisitor(CriteriaBuilder cb, Path<?> from,
|
||||
BiFunction<From<?, ?>, PropertyPath, Expression<?>> expressionFactory) {
|
||||
this.cb = cb;
|
||||
this.from = from;
|
||||
this.expressionFactory = expressionFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the {@link org.springframework.data.jpa.domain.JpaSort.JpaOrder}'s property and parse it as an HQL
|
||||
* {@literal sortExpression}.
|
||||
*
|
||||
* @param jpaOrder
|
||||
* @param jpaOrder must not be {@literal null}.
|
||||
* @return criteriaExpression
|
||||
* @throws IllegalArgumentException thrown if the order yields no sort expression.
|
||||
* @throws UnsupportedOperationException thrown if the order contains an unsupported expression.
|
||||
* @throws BadJpqlGrammarException thrown if the order contains a syntax errors.
|
||||
*/
|
||||
Expression<?> createCriteriaExpression(Sort.Order jpaOrder) {
|
||||
|
||||
@@ -100,32 +135,24 @@ class HqlOrderExpressionVisitor extends HqlBaseVisitor<Expression<?>> {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("rawtypes")
|
||||
public Expression<?> visitRelationalExpression(HqlParser.RelationalExpressionContext ctx) {
|
||||
|
||||
Expression<Comparable> left = visitRequired(ctx.expression(0));
|
||||
Expression<Comparable> right = visitRequired(ctx.expression(1));
|
||||
String op = ctx.op.getText();
|
||||
|
||||
if (op.equals("=")) {
|
||||
return cb.equal(left, right);
|
||||
} else if (op.equals(">")) {
|
||||
return cb.greaterThan(left, right);
|
||||
} else if (op.equals(">=")) {
|
||||
return cb.greaterThanOrEqualTo(left, right);
|
||||
} else if (op.equals("<")) {
|
||||
return cb.lessThan(left, right);
|
||||
} else if (op.equals("<=")) {
|
||||
return cb.lessThanOrEqualTo(left, right);
|
||||
} else if (op.equals("<>") || op.equals("!=") || op.equals("^=")) {
|
||||
return cb.notEqual(left, right);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Unsupported comparison operator: " + op);
|
||||
}
|
||||
return switch (op) {
|
||||
case "=" -> cb.equal(left, right);
|
||||
case ">" -> cb.greaterThan(left, right);
|
||||
case ">=" -> cb.greaterThanOrEqualTo(left, right);
|
||||
case "<" -> cb.lessThan(left, right);
|
||||
case "<=" -> cb.lessThanOrEqualTo(left, right);
|
||||
case "<>", "!=", "^=" -> cb.notEqual(left, right);
|
||||
default -> throw new UnsupportedOperationException("Unsupported comparison operator: " + op);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("rawtypes")
|
||||
public Expression<?> visitBetweenExpression(HqlParser.BetweenExpressionContext ctx) {
|
||||
|
||||
Expression<Comparable> condition = visitRequired(ctx.expression(0));
|
||||
@@ -244,7 +271,7 @@ class HqlOrderExpressionVisitor extends HqlBaseVisitor<Expression<?>> {
|
||||
}
|
||||
|
||||
Expression<?>[] arguments = ctx.genericFunctionArguments().expressionOrPredicate().stream() //
|
||||
.map(expressionOrPredicateContext -> visitRequired(expressionOrPredicateContext)) //
|
||||
.map(this::visitRequired) //
|
||||
.toArray(Expression[]::new);
|
||||
return cb.function(functionName, Object.class, arguments);
|
||||
|
||||
@@ -371,7 +398,6 @@ class HqlOrderExpressionVisitor extends HqlBaseVisitor<Expression<?>> {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public Expression<?> visitTruncFunction(HqlParser.TruncFunctionContext ctx) {
|
||||
|
||||
Expression expr = visitRequired(ctx.expression().get(0));
|
||||
@@ -497,21 +523,6 @@ class HqlOrderExpressionVisitor extends HqlBaseVisitor<Expression<?>> {
|
||||
throw new UnsupportedOperationException("Unsupported literal: " + ctx.getText());
|
||||
}
|
||||
|
||||
static String getDecimals(TerminalNode input) {
|
||||
|
||||
String text = input.getText();
|
||||
StringBuilder result = new StringBuilder(text.length());
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
if (Character.isDigit(c) || c == '-' || c == '+' || c == '.') {
|
||||
result.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitDateTimeLiteral(HqlParser.DateTimeLiteralContext ctx) {
|
||||
|
||||
@@ -526,6 +537,97 @@ class HqlOrderExpressionVisitor extends HqlBaseVisitor<Expression<?>> {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitJdbcTimeLiteral(HqlParser.JdbcTimeLiteralContext ctx) {
|
||||
|
||||
if (ctx.time() != null) {
|
||||
return visitRequired(ctx.time());
|
||||
}
|
||||
|
||||
return cb.literal(DATE_TIME_FORMATTER_TIME.parse(unquoteTemporal(ctx.genericTemporalLiteralText())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitDate(HqlParser.DateContext ctx) {
|
||||
return cb.literal(LocalDate.from(DATE_TIME_FORMATTER_DATE.parse(unquoteTemporal(ctx))));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitTime(HqlParser.TimeContext ctx) {
|
||||
return cb.literal(LocalTime.from(DATE_TIME_FORMATTER_TIME.parse(unquoteTemporal(ctx))));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitJdbcDateLiteral(HqlParser.JdbcDateLiteralContext ctx) {
|
||||
|
||||
if (ctx.date() != null) {
|
||||
return visitRequired(ctx.date());
|
||||
}
|
||||
|
||||
return cb
|
||||
.literal(LocalDate.from(DATE_TIME_FORMATTER_DATE.parse(unquoteTemporal(ctx.genericTemporalLiteralText()))));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitJdbcTimestampLiteral(HqlParser.JdbcTimestampLiteralContext ctx) {
|
||||
|
||||
if (ctx.dateTime() != null) {
|
||||
return visitRequired(ctx.dateTime());
|
||||
}
|
||||
|
||||
return cb.literal(LocalDateTime.from(DATE_TIME.parse(unquoteTemporal(ctx.genericTemporalLiteralText()))));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitLocalDateTime(HqlParser.LocalDateTimeContext ctx) {
|
||||
return cb.literal(LocalDateTime.from(DATE_TIME.parse(unquoteTemporal(ctx.getText()))));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitZonedDateTime(HqlParser.ZonedDateTimeContext ctx) {
|
||||
return cb.literal(ZonedDateTime.parse(ctx.getText()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitOffsetDateTime(HqlParser.OffsetDateTimeContext ctx) {
|
||||
return cb.literal(OffsetDateTime.parse(ctx.getText()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitOffsetDateTimeWithMinutes(HqlParser.OffsetDateTimeWithMinutesContext ctx) {
|
||||
return cb.literal(OffsetDateTime.parse(ctx.getText()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitLocalDateTimeLiteral(HqlParser.LocalDateTimeLiteralContext ctx) {
|
||||
return visitRequired(ctx.localDateTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitZonedDateTimeLiteral(HqlParser.ZonedDateTimeLiteralContext ctx) {
|
||||
return visitRequired(ctx.zonedDateTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitOffsetDateTimeLiteral(HqlParser.OffsetDateTimeLiteralContext ctx) {
|
||||
return visitRequired(ctx.offsetDateTime() != null ? ctx.offsetDateTime() : ctx.offsetDateTimeWithMinutes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitDateLiteral(HqlParser.DateLiteralContext ctx) {
|
||||
return visitRequired(ctx.date());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitTimeLiteral(HqlParser.TimeLiteralContext ctx) {
|
||||
return visitRequired(ctx.time());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitDateTime(HqlParser.DateTimeContext ctx) {
|
||||
return super.visitDateTime(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitGroupedExpression(HqlParser.GroupedExpressionContext ctx) {
|
||||
return visit(ctx.expression());
|
||||
@@ -579,10 +681,67 @@ class HqlOrderExpressionVisitor extends HqlBaseVisitor<Expression<?>> {
|
||||
|
||||
@Override
|
||||
public Expression<?> visitSimplePath(HqlParser.SimplePathContext ctx) {
|
||||
return QueryUtils.toExpressionRecursively((From<?, ?>) from, PropertyPath.from(ctx.getText(), from.getJavaType()));
|
||||
return expressionFactory.apply((From<?, ?>) from, PropertyPath.from(ctx.getText(), from.getJavaType()));
|
||||
}
|
||||
|
||||
String getString(HqlParser.IdentifierContext context) {
|
||||
@Override
|
||||
public Expression<?> visitCaseList(HqlParser.CaseListContext ctx) {
|
||||
return visit(ctx.simpleCaseExpression() != null ? ctx.simpleCaseExpression() : ctx.searchedCaseExpression());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitSimpleCaseExpression(HqlParser.SimpleCaseExpressionContext ctx) {
|
||||
|
||||
CriteriaBuilder.SimpleCase<Object, Object> simpleCase = cb.selectCase(visit(ctx.expressionOrPredicate(0)));
|
||||
|
||||
ctx.caseWhenExpressionClause().forEach(caseWhenExpressionClauseContext -> {
|
||||
simpleCase.when( //
|
||||
visitRequired(caseWhenExpressionClauseContext.expression()), //
|
||||
visitRequired(caseWhenExpressionClauseContext.expressionOrPredicate()));
|
||||
});
|
||||
|
||||
if (ctx.expressionOrPredicate().size() == 2) {
|
||||
simpleCase.otherwise(visitRequired(ctx.expressionOrPredicate(1)));
|
||||
}
|
||||
|
||||
return simpleCase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitSearchedCaseExpression(HqlParser.SearchedCaseExpressionContext ctx) {
|
||||
|
||||
CriteriaBuilder.Case<Object> searchedCase = cb.selectCase();
|
||||
|
||||
ctx.caseWhenPredicateClause().forEach(caseWhenPredicateClauseContext -> {
|
||||
searchedCase.when( //
|
||||
visitRequired(caseWhenPredicateClauseContext.predicate()), //
|
||||
visit(caseWhenPredicateClauseContext.expressionOrPredicate()));
|
||||
});
|
||||
|
||||
if (ctx.expressionOrPredicate() != null) {
|
||||
searchedCase.otherwise(visit(ctx.expressionOrPredicate()));
|
||||
}
|
||||
|
||||
return searchedCase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitParameter(HqlParser.ParameterContext ctx) {
|
||||
throw new UnsupportedOperationException(String.format(UNSUPPORTED_TEMPLATE, "a parameter argument"));
|
||||
}
|
||||
|
||||
private <T> Expression<T> visitRequired(ParseTree ctx) {
|
||||
|
||||
Expression<?> expression = visit(ctx);
|
||||
|
||||
if (expression == null) {
|
||||
throw new UnsupportedOperationException("No result for expression: " + ctx.getText());
|
||||
}
|
||||
|
||||
return (Expression<T>) expression;
|
||||
}
|
||||
|
||||
private String getString(HqlParser.IdentifierContext context) {
|
||||
|
||||
HqlParser.NakedIdentifierContext ni = context.nakedIdentifier();
|
||||
|
||||
@@ -595,113 +754,85 @@ class HqlOrderExpressionVisitor extends HqlBaseVisitor<Expression<?>> {
|
||||
return text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitCaseList(HqlParser.CaseListContext ctx) {
|
||||
if (ctx.simpleCaseExpression() != null) {
|
||||
return visit(ctx.simpleCaseExpression());
|
||||
} else {
|
||||
return visit(ctx.searchedCaseExpression());
|
||||
}
|
||||
}
|
||||
private static String getDecimals(TerminalNode input) {
|
||||
|
||||
@Override
|
||||
public Expression<?> visitSimpleCaseExpression(HqlParser.SimpleCaseExpressionContext ctx) {
|
||||
CriteriaBuilder.SimpleCase<Object, Object> simpleCase = cb.selectCase(visit(ctx.expressionOrPredicate(0)));
|
||||
ctx.caseWhenExpressionClause().forEach(caseWhenExpressionClauseContext -> {
|
||||
simpleCase.when( //
|
||||
visitRequired(caseWhenExpressionClauseContext.expression()), //
|
||||
visitRequired(caseWhenExpressionClauseContext.expressionOrPredicate()));
|
||||
});
|
||||
if (ctx.expressionOrPredicate().size() == 2) {
|
||||
simpleCase.otherwise(visitRequired(ctx.expressionOrPredicate(1)));
|
||||
}
|
||||
return simpleCase;
|
||||
}
|
||||
String text = input.getText();
|
||||
StringBuilder result = new StringBuilder(text.length());
|
||||
|
||||
@Override
|
||||
public Expression<?> visitSearchedCaseExpression(HqlParser.SearchedCaseExpressionContext ctx) {
|
||||
CriteriaBuilder.Case<Object> searchedCase = cb.selectCase();
|
||||
ctx.caseWhenPredicateClause().forEach(caseWhenPredicateClauseContext -> {
|
||||
searchedCase.when( //
|
||||
visitRequired(caseWhenPredicateClauseContext.predicate()), //
|
||||
visit(caseWhenPredicateClauseContext.expressionOrPredicate()));
|
||||
});
|
||||
if (ctx.expressionOrPredicate() != null) {
|
||||
searchedCase.otherwise(visit(ctx.expressionOrPredicate()));
|
||||
}
|
||||
return searchedCase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression<?> visitParameter(HqlParser.ParameterContext ctx) {
|
||||
throw new UnsupportedOperationException(String.format(UNSUPPORTED_TEMPLATE, "a parameter argument"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> Expression<T> visitRequired(ParseTree ctx) {
|
||||
|
||||
Expression<?> expression = visit(ctx);
|
||||
|
||||
if (expression == null) {
|
||||
throw new UnsupportedOperationException("No result for expression: " + ctx.getText());
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
if (Character.isDigit(c) || c == '-' || c == '+' || c == '.') {
|
||||
result.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return (Expression<T>) expression;
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private static String unquoteTemporal(ParseTree node) {
|
||||
return unquoteTemporal(node.getText());
|
||||
}
|
||||
|
||||
private static String unquoteTemporal(String temporal) {
|
||||
if (temporal.startsWith("'") && temporal.endsWith("'")) {
|
||||
temporal = temporal.substring(1, temporal.length() - 1);
|
||||
}
|
||||
return temporal;
|
||||
}
|
||||
|
||||
private static String unquoteIdentifier(String text) {
|
||||
|
||||
int end = text.length() - 1;
|
||||
assert text.charAt(0) == '`' && text.charAt(end) == '`';
|
||||
|
||||
Assert.isTrue(text.charAt(0) == '`' && text.charAt(end) == '`',
|
||||
"Quoted identifier does not end with the same delimiter");
|
||||
|
||||
// Unquote a parsed quoted identifier and handle escape sequences
|
||||
final StringBuilder sb = new StringBuilder(text.length() - 2);
|
||||
StringBuilder sb = new StringBuilder(text.length() - 2);
|
||||
for (int i = 1; i < end; i++) {
|
||||
|
||||
char c = text.charAt(i);
|
||||
switch (c) {
|
||||
case '\\':
|
||||
if (i + 1 < end) {
|
||||
char nextChar = text.charAt(++i);
|
||||
switch (nextChar) {
|
||||
case 'b':
|
||||
c = '\b';
|
||||
break;
|
||||
case 't':
|
||||
c = '\t';
|
||||
break;
|
||||
case 'n':
|
||||
c = '\n';
|
||||
break;
|
||||
case 'f':
|
||||
c = '\f';
|
||||
break;
|
||||
case 'r':
|
||||
c = '\r';
|
||||
break;
|
||||
case '\\':
|
||||
c = '\\';
|
||||
break;
|
||||
case '\'':
|
||||
c = '\'';
|
||||
break;
|
||||
case '"':
|
||||
c = '"';
|
||||
break;
|
||||
case '`':
|
||||
c = '`';
|
||||
break;
|
||||
case 'u':
|
||||
c = (char) Integer.parseInt(text.substring(i + 1, i + 5), 16);
|
||||
i += 4;
|
||||
break;
|
||||
default:
|
||||
sb.append('\\');
|
||||
c = nextChar;
|
||||
break;
|
||||
}
|
||||
if (c == '\\') {
|
||||
if (i + 1 < end) {
|
||||
char nextChar = text.charAt(++i);
|
||||
switch (nextChar) {
|
||||
case 'b':
|
||||
c = '\b';
|
||||
break;
|
||||
case 't':
|
||||
c = '\t';
|
||||
break;
|
||||
case 'n':
|
||||
c = '\n';
|
||||
break;
|
||||
case 'f':
|
||||
c = '\f';
|
||||
break;
|
||||
case 'r':
|
||||
c = '\r';
|
||||
break;
|
||||
case '\\':
|
||||
c = '\\';
|
||||
break;
|
||||
case '\'':
|
||||
c = '\'';
|
||||
break;
|
||||
case '"':
|
||||
c = '"';
|
||||
break;
|
||||
case '`':
|
||||
c = '`';
|
||||
break;
|
||||
case 'u':
|
||||
c = (char) Integer.parseInt(text.substring(i + 1, i + 5), 16);
|
||||
i += 4;
|
||||
break;
|
||||
default:
|
||||
sb.append('\\');
|
||||
c = nextChar;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
sb.append(c);
|
||||
}
|
||||
@@ -715,7 +846,7 @@ class HqlOrderExpressionVisitor extends HqlBaseVisitor<Expression<?>> {
|
||||
Assert.isTrue(delimiter == text.charAt(end), "Quoted identifier does not end with the same delimiter");
|
||||
|
||||
// Unescape the parsed literal
|
||||
final StringBuilder sb = new StringBuilder(text.length() - 2);
|
||||
StringBuilder sb = new StringBuilder(text.length() - 2);
|
||||
for (int i = 1; i < end; i++) {
|
||||
char c = text.charAt(i);
|
||||
switch (c) {
|
||||
|
||||
@@ -725,8 +725,15 @@ public abstract class QueryUtils {
|
||||
@SuppressWarnings("unchecked")
|
||||
private static jakarta.persistence.criteria.Order toJpaOrder(Order order, From<?, ?> from, CriteriaBuilder cb) {
|
||||
|
||||
PropertyPath property = PropertyPath.from(order.getProperty(), from.getJavaType());
|
||||
Expression<?> expression = toExpressionRecursively(from, property);
|
||||
Expression<?> expression;
|
||||
|
||||
if (order instanceof JpaOrder jpaOrder && jpaOrder.isUnsafe()) {
|
||||
expression = new HqlOrderExpressionVisitor(cb, from, QueryUtils::toExpressionRecursively)
|
||||
.createCriteriaExpression(order);
|
||||
} else {
|
||||
PropertyPath property = PropertyPath.from(order.getProperty(), from.getJavaType());
|
||||
expression = toExpressionRecursively(from, property);
|
||||
}
|
||||
|
||||
Nulls nulls = toNulls(order.getNullHandling());
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ import org.springframework.data.domain.*;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.data.domain.Sort.Order;
|
||||
import org.springframework.data.jpa.domain.DeleteSpecification;
|
||||
import org.springframework.data.jpa.domain.JpaSort;
|
||||
import org.springframework.data.jpa.domain.PredicateSpecification;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.data.jpa.domain.UpdateSpecification;
|
||||
@@ -3227,6 +3228,38 @@ class UserRepositoryTests {
|
||||
assertThat(users).extracting(User::getId).containsExactly(expected.getId());
|
||||
}
|
||||
|
||||
@Test // GH-3172
|
||||
void specificationShouldApplyUnsafeSort() {
|
||||
|
||||
flushTestUsers();
|
||||
firstUser.setManager(firstUser);
|
||||
secondUser.setManager(firstUser);
|
||||
thirdUser.setManager(secondUser);
|
||||
fourthUser.setManager(secondUser);
|
||||
repository.saveAllAndFlush(List.of(firstUser, secondUser, thirdUser, fourthUser));
|
||||
|
||||
PredicateSpecification<User> spec = userHasFirstname("Oliver").or(userHasLastname("Matthews"));
|
||||
|
||||
List<User> result = repository.findBy(spec, q -> q.sortBy(JpaSort.unsafe("LENGTH(firstname)")).all());
|
||||
|
||||
assertThat(result).containsExactly(thirdUser, firstUser);
|
||||
}
|
||||
|
||||
@Test // GH-3172
|
||||
void findAllShouldApplyUnsafeSort() {
|
||||
|
||||
flushTestUsers();
|
||||
firstUser.setManager(firstUser);
|
||||
secondUser.setManager(firstUser);
|
||||
thirdUser.setManager(secondUser);
|
||||
fourthUser.setManager(secondUser);
|
||||
repository.saveAllAndFlush(List.of(firstUser, secondUser, thirdUser, fourthUser));
|
||||
|
||||
assertThat(
|
||||
repository.findAll(JpaSort.unsafe("case when firstname ilike 'O%' escape '^' then 'A' else firstname end")))
|
||||
.containsExactly(firstUser, thirdUser, secondUser, fourthUser);
|
||||
}
|
||||
|
||||
@Test // DATAJPA-1233, GH-3756
|
||||
void handlesCountQueriesWithLessParametersSingleParam() {
|
||||
|
||||
|
||||
@@ -120,6 +120,33 @@ class HqlOrderExpressionVisitorUnitTests {
|
||||
assertThat(renderOrderBy(JpaSort.unsafe("age + 0x12"), "u")).startsWithIgnoringCase("order by u.age + 18");
|
||||
}
|
||||
|
||||
@Test // GH-3172
|
||||
void temporalLiterals() {
|
||||
|
||||
// JDBC
|
||||
assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {ts '2024-01-01 12:34:56'}"), "u"))
|
||||
.startsWithIgnoringCase("order by u.createdAt + 2024-01-01T12:34:56");
|
||||
|
||||
assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {ts '2012-01-03 09:00:00.000000001'}"), "u"))
|
||||
.startsWithIgnoringCase("order by u.createdAt + 2012-01-03T09:00:00.000000001");
|
||||
|
||||
// Hibernate NPE
|
||||
assertThatNullPointerException().isThrownBy(() -> renderOrderBy(JpaSort.unsafe("createdAt + {t '12:34:56'}"), "u"));
|
||||
|
||||
assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {d '2024-01-01'}"), "u"))
|
||||
.startsWithIgnoringCase("order by u.createdAt + 2024-01-01");
|
||||
|
||||
// JPQL
|
||||
assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {ts 2024-01-01 12:34:56}"), "u"))
|
||||
.startsWithIgnoringCase("order by u.createdAt + 2024-01-01T12:34:56");
|
||||
|
||||
assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {t 12:34:56}"), "u"))
|
||||
.startsWithIgnoringCase("order by u.createdAt + 12:34:56");
|
||||
|
||||
assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {d 2024-01-01}"), "u"))
|
||||
.startsWithIgnoringCase("order by u.createdAt + 2024-01-01");
|
||||
}
|
||||
|
||||
@Test // GH-3172
|
||||
void arithmetic() {
|
||||
|
||||
@@ -221,7 +248,8 @@ class HqlOrderExpressionVisitorUnitTests {
|
||||
|
||||
CriteriaQuery<User> query = em.getCriteriaBuilder().createQuery(User.class);
|
||||
Selection<User> from = query.from(User.class).alias(alias);
|
||||
HqlOrderExpressionVisitor extractor = new HqlOrderExpressionVisitor(em.getCriteriaBuilder(), (Path<?>) from);
|
||||
HqlOrderExpressionVisitor extractor = new HqlOrderExpressionVisitor(em.getCriteriaBuilder(), (Path<?>) from,
|
||||
QueryUtils::toExpressionRecursively);
|
||||
|
||||
Expression<?> expression = extractor.createCriteriaExpression(sort.stream().findFirst().get());
|
||||
return query.select(from).orderBy(em.getCriteriaBuilder().asc(expression, Nulls.NONE));
|
||||
|
||||
@@ -383,6 +383,17 @@ Throws Exception.
|
||||
<4> Valid `Sort` expression pointing to aliased function.
|
||||
====
|
||||
|
||||
=== JpaSort.unsafe(…) limitations
|
||||
|
||||
`JpaSort.unsafe(…)` operates in two modes:
|
||||
|
||||
* When used with derived Queries or String-based Queries, the order string is appended to the query.
|
||||
* When used with Query by Example or Specifications (that use `CriteriaQuery`), order expressions are parsed and added to the `CriteriaQuery` as expressions.
|
||||
Query expressions can contain function calls, various clauses (such as `CASE WHEN`, arithmetic expressions) or property paths.
|
||||
Order translation does not support subquery expressions, `TREAT` and `CAST`.`
|
||||
|
||||
[[jpa.query-methods.paging]]
|
||||
|
||||
[[jpa.query-methods.scroll]]
|
||||
== Scrolling Large Query Results
|
||||
|
||||
|
||||
Reference in New Issue
Block a user