diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/JpaSort.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/JpaSort.java
index 771b5361a..89e4f35bf 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/JpaSort.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/JpaSort.java
@@ -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.
+ *
+ * {@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> paths) {
- this(Collections.emptyList(), direction, paths);
+ this(Collections. emptyList(), direction, paths);
}
private JpaSort(List orders, @Nullable Direction direction, List> 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.>emptyList());
+ return new JpaSort(orders, direction, Collections.> 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;
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitor.java
index ade370c0f..e5915f19e 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitor.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitor.java
@@ -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> {
+ 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, 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, 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> {
}
@Override
- @SuppressWarnings("rawtypes")
public Expression> visitRelationalExpression(HqlParser.RelationalExpressionContext ctx) {
Expression left = visitRequired(ctx.expression(0));
Expression 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 condition = visitRequired(ctx.expression(0));
@@ -244,7 +271,7 @@ class HqlOrderExpressionVisitor extends HqlBaseVisitor> {
}
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> {
}
@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> {
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> {
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> {
@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