diff --git a/pom.xml b/pom.xml
index 94542470ab..ee0112a079 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-jpa-parent
- 3.2.0-SNAPSHOT
+ 3.2.0-gh-3172-SNAPSHOT
pom
Spring Data JPA Parent
diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml
index f239d6394b..96a80990d7 100755
--- a/spring-data-envers/pom.xml
+++ b/spring-data-envers/pom.xml
@@ -5,12 +5,12 @@
org.springframework.data
spring-data-envers
- 3.2.0-SNAPSHOT
+ 3.2.0-gh-3172-SNAPSHOT
org.springframework.data
spring-data-jpa-parent
- 3.2.0-SNAPSHOT
+ 3.2.0-gh-3172-SNAPSHOT
../pom.xml
diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml
index a458953182..99370f8100 100644
--- a/spring-data-jpa-distribution/pom.xml
+++ b/spring-data-jpa-distribution/pom.xml
@@ -14,7 +14,7 @@
org.springframework.data
spring-data-jpa-parent
- 3.2.0-SNAPSHOT
+ 3.2.0-gh-3172-SNAPSHOT
../pom.xml
diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml
index 2c9677329f..e8893cca43 100644
--- a/spring-data-jpa/pom.xml
+++ b/spring-data-jpa/pom.xml
@@ -6,7 +6,7 @@
org.springframework.data
spring-data-jpa
- 3.2.0-SNAPSHOT
+ 3.2.0-gh-3172-SNAPSHOT
Spring Data JPA
Spring Data module for JPA repositories.
@@ -15,7 +15,7 @@
org.springframework.data
spring-data-jpa-parent
- 3.2.0-SNAPSHOT
+ 3.2.0-gh-3172-SNAPSHOT
../pom.xml
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlOrderByExtractor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlOrderByExtractor.java
new file mode 100644
index 0000000000..18672a10f4
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlOrderByExtractor.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2023 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.jpa.repository.query;
+
+import jakarta.persistence.criteria.Expression;
+import jakarta.persistence.criteria.From;
+import jakarta.persistence.criteria.Path;
+
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.springframework.data.jpa.domain.JpaSort;
+
+/**
+ * Parses the content of {@link JpaSort#unsafe(String...)} as an EQL {@literal orderby_item} and renders that into a JPA
+ * Criteria {@link Expression}.
+ *
+ * @author Greg Turnquist
+ * @since 3.2
+ */
+class EqlOrderByExtractor extends EqlBaseVisitor {
+
+ private From, ?> from;
+
+ EqlOrderByExtractor(From, ?> from) {
+ this.from = from;
+ }
+
+ /**
+ * Extract the {@link JpaSort.JpaOrder}'s property and parse it as a EQL {@literal orderby_item}.
+ *
+ * @param jpaOrder
+ * @return criteriaExpression
+ * @since 3.2
+ */
+ Expression> extractCriteriaExpression(JpaSort.JpaOrder jpaOrder) {
+
+ EqlLexer jpaOrderLexer = new EqlLexer(CharStreams.fromString(jpaOrder.getProperty()));
+ EqlParser jpaOrderParser = new EqlParser(new CommonTokenStream(jpaOrderLexer));
+
+ return expression(visit(jpaOrderParser.orderby_item()));
+ }
+
+ /**
+ * Given a particular {@link JpaOrderByToken}, transform it into a Jakarta {@link Expression}.
+ *
+ * @param token
+ * @return Expression
+ */
+ private Expression> expression(JpaOrderByToken token) {
+
+ if (token instanceof JpaOrderByExpressionToken expressionToken) {
+ return expressionToken.expression();
+ } else if (token instanceof JpaOrderByNamedToken namedToken) {
+ return from.get(namedToken.token());
+ } else {
+ if (token != null) {
+ throw new IllegalArgumentException("We can't handle a " + token.getClass() + "!");
+ } else {
+ throw new IllegalArgumentException("We can't handle a null token!");
+ }
+ }
+ }
+
+ /**
+ * Convert a generic {@link JpaOrderByToken} token into a {@link JpaOrderByNamedToken} and then extract its string
+ * token value.
+ *
+ * @param token
+ * @return string value
+ * @since 3.2
+ */
+ private String token(JpaOrderByToken token) {
+
+ if (token instanceof JpaOrderByNamedToken namedToken) {
+ return namedToken.token();
+ } else {
+ if (token != null) {
+ throw new IllegalArgumentException("We can't handle a " + token.getClass() + "!");
+ } else {
+ throw new IllegalArgumentException("We can't handle a null token!");
+ }
+ }
+ }
+
+ @Override
+ public JpaOrderByToken visitOrderby_item(EqlParser.Orderby_itemContext ctx) {
+
+ if (ctx.state_field_path_expression() != null) {
+ return visit(ctx.state_field_path_expression());
+ } else if (ctx.general_identification_variable() != null) {
+ return visit(ctx.general_identification_variable());
+ } else if (ctx.result_variable() != null) {
+ return visit(ctx.result_variable());
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public JpaOrderByToken visitState_field_path_expression(EqlParser.State_field_path_expressionContext ctx) {
+
+ Path> path = (Path>) expression(visit(ctx.general_subpath()));
+
+ path = path.get(token(visit(ctx.state_field())));
+
+ return new JpaOrderByExpressionToken(path);
+ }
+
+ @Override
+ public JpaOrderByToken visitGeneral_identification_variable(EqlParser.General_identification_variableContext ctx) {
+
+ if (ctx.identification_variable() != null) {
+ return visit(ctx.identification_variable());
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public JpaOrderByToken visitSimple_subpath(EqlParser.Simple_subpathContext ctx) {
+
+ Path> path = (Path>) expression(visit(ctx.general_identification_variable()));
+
+ for (EqlParser.Single_valued_object_fieldContext singleValuedObjectFieldContext : ctx
+ .single_valued_object_field()) {
+ path = path.get(token(visit(singleValuedObjectFieldContext)));
+ }
+
+ return new JpaOrderByExpressionToken(path);
+ }
+
+ @Override
+ public JpaOrderByToken visitResult_variable(EqlParser.Result_variableContext ctx) {
+ return super.visitResult_variable(ctx);
+ }
+
+ @Override
+ public JpaOrderByToken visitIdentification_variable(EqlParser.Identification_variableContext ctx) {
+
+ if (ctx.IDENTIFICATION_VARIABLE() != null) {
+ return new JpaOrderByNamedToken(ctx.IDENTIFICATION_VARIABLE().getText());
+ } else {
+ return new JpaOrderByNamedToken(ctx.f.getText());
+ }
+ }
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderByExtractor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderByExtractor.java
new file mode 100644
index 0000000000..f5623c6d34
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderByExtractor.java
@@ -0,0 +1,526 @@
+/*
+ * Copyright 2023 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.jpa.repository.query;
+
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.Expression;
+import jakarta.persistence.criteria.From;
+import jakarta.persistence.criteria.Path;
+
+import java.util.HexFormat;
+import java.util.stream.Collectors;
+
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.hibernate.query.criteria.HibernateCriteriaBuilder;
+import org.springframework.data.jpa.domain.JpaSort;
+
+/**
+ * Parses the content of {@link JpaSort#unsafe(String...)} as an HQL {@literal sortExpression} and renders that into a
+ * JPA Criteria {@link Expression}.
+ *
+ * @author Greg Turnquist
+ * @since 3.2
+ */
+class HqlOrderByExtractor extends HqlBaseVisitor {
+
+ private CriteriaBuilder cb;
+ private From, ?> from;
+
+ private static String UNSUPPORTED_TEMPLATE = "We can't handle %s in an ORDER BY clause through JpaSort.unsafe";
+
+ HqlOrderByExtractor(CriteriaBuilder cb, From, ?> from) {
+
+ this.cb = cb;
+ this.from = from;
+ }
+
+ /**
+ * Extract the {@link org.springframework.data.jpa.domain.JpaSort.JpaOrder}'s property and parse it as an HQL
+ * {@literal sortExpression}.
+ *
+ * @param jpaOrder
+ * @return criteriaExpression
+ * @since 3.2
+ */
+ Expression> extractCriteriaExpression(JpaSort.JpaOrder jpaOrder) {
+
+ HqlLexer jpaOrderLexer = new HqlLexer(CharStreams.fromString(jpaOrder.getProperty()));
+ HqlParser jpaOrderParser = new HqlParser(new CommonTokenStream(jpaOrderLexer));
+
+ return expression(visit(jpaOrderParser.sortExpression()));
+ }
+
+ /**
+ * Given a particular {@link JpaOrderByToken}, transform it into a Jakarta {@link Expression}.
+ *
+ * @param token
+ * @return Expression
+ */
+ private Expression> expression(JpaOrderByToken token) {
+
+ if (token instanceof JpaOrderByExpressionToken expressionToken) {
+ return expressionToken.expression();
+ } else if (token instanceof JpaOrderByNamedToken namedToken) {
+ return from.get(namedToken.token());
+ } else {
+ throw new IllegalArgumentException("We can't handle a " + token.getClass() + "!");
+ }
+ }
+
+ /**
+ * Convert a generic {@link JpaOrderByToken} token into a {@link JpaOrderByNamedToken} and then extract its string
+ * token value.
+ *
+ * @param token
+ * @return string value
+ * @since 3.2
+ */
+ private String token(JpaOrderByToken token) {
+
+ if (token instanceof JpaOrderByNamedToken namedToken) {
+ return namedToken.token();
+ } else {
+ throw new IllegalArgumentException("We can't handle a " + token.getClass() + "!");
+ }
+ }
+
+ @Override
+ public JpaOrderByToken visitSortExpression(HqlParser.SortExpressionContext ctx) {
+
+ if (ctx.identifier() != null) {
+ return visit(ctx.identifier());
+ } else if (ctx.INTEGER_LITERAL() != null) {
+ return new JpaOrderByExpressionToken(cb.literal(Integer.valueOf(ctx.INTEGER_LITERAL().getText())));
+ } else if (ctx.expression() != null) {
+ return visit(ctx.expression());
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public JpaOrderByToken visitRelationalExpression(HqlParser.RelationalExpressionContext ctx) {
+
+ Expression left = (Expression) expression(visit(ctx.expression(0)));
+ Expression right = (Expression) expression(visit(ctx.expression(1)));
+
+ if (ctx.op.getText().equals("=")) {
+ return new JpaOrderByExpressionToken(cb.equal(left, right));
+ } else if (ctx.op.getText().equals(">")) {
+ return new JpaOrderByExpressionToken(cb.greaterThan(left, right));
+ } else if (ctx.op.getText().equals(">=")) {
+ return new JpaOrderByExpressionToken(cb.greaterThanOrEqualTo(left, right));
+ } else if (ctx.op.getText().equals("<")) {
+ return new JpaOrderByExpressionToken(cb.lessThan(left, right));
+ } else if (ctx.op.getText().equals("<=")) {
+ return new JpaOrderByExpressionToken(cb.lessThanOrEqualTo(left, right));
+ } else if (ctx.op.getText().equals("<>") || ctx.op.getText().equals("!=") || ctx.op.getText().equals("^=")) {
+ return new JpaOrderByExpressionToken(cb.notEqual(left, right));
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public JpaOrderByToken visitBetweenExpression(HqlParser.BetweenExpressionContext ctx) {
+
+ Expression condition = (Expression) expression(visit(ctx.expression(0)));
+ Expression lower = (Expression) expression(visit(ctx.expression(1)));
+ Expression upper = (Expression) expression(visit(ctx.expression(2)));
+
+ if (ctx.NOT() == null) {
+ return new JpaOrderByExpressionToken(cb.between(condition, lower, upper));
+ } else {
+ return new JpaOrderByExpressionToken(cb.between(condition, lower, upper).not());
+ }
+ }
+
+ @Override
+ public JpaOrderByToken visitDealingWithNullExpression(HqlParser.DealingWithNullExpressionContext ctx) {
+
+ if (ctx.NULL() != null) {
+
+ Expression> condition = expression(visit(ctx.expression(0)));
+
+ if (ctx.NOT() == null) {
+ return new JpaOrderByExpressionToken(cb.isNull(condition));
+ } else {
+ return new JpaOrderByExpressionToken(cb.isNotNull(condition));
+ }
+ } else {
+
+ Expression> left = expression(visit(ctx.expression(0)));
+ Expression> right = expression(visit(ctx.expression(1)));
+
+ HibernateCriteriaBuilder hcb = (HibernateCriteriaBuilder) cb;
+
+ if (ctx.NOT() == null) {
+ return new JpaOrderByExpressionToken(hcb.distinctFrom(left, right));
+ } else {
+ return new JpaOrderByExpressionToken(hcb.notDistinctFrom(left, right));
+ }
+ }
+ }
+
+ @Override
+ public JpaOrderByToken visitStringPatternMatching(HqlParser.StringPatternMatchingContext ctx) {
+
+ Expression condition = (Expression) expression(visit(ctx.expression(0)));
+ Expression match = (Expression) expression(visit(ctx.expression(1)));
+ Expression escape = ctx.ESCAPE() != null && ctx.stringLiteral() != null //
+ ? (Expression) expression(visit(ctx.stringLiteral())) //
+ : null;
+
+ if (ctx.LIKE() != null) {
+
+ if (ctx.NOT() == null) {
+ return escape == null //
+ ? new JpaOrderByExpressionToken(cb.like(condition, match)) //
+ : new JpaOrderByExpressionToken(cb.like(condition, match, escape));
+ } else {
+ return escape == null //
+ ? new JpaOrderByExpressionToken(cb.notLike(condition, match)) //
+ : new JpaOrderByExpressionToken(cb.notLike(condition, match, escape));
+ }
+ } else {
+
+ HibernateCriteriaBuilder hcb = (HibernateCriteriaBuilder) cb;
+
+ if (ctx.NOT() == null) {
+ return escape == null //
+ ? new JpaOrderByExpressionToken(hcb.ilike(condition, match)) //
+ : new JpaOrderByExpressionToken(hcb.ilike(condition, match, escape));
+ } else {
+ return escape == null //
+ ? new JpaOrderByExpressionToken(hcb.notIlike(condition, match)) //
+ : new JpaOrderByExpressionToken(hcb.notIlike(condition, match, escape));
+ }
+ }
+
+ }
+
+ @Override
+ public JpaOrderByToken visitInExpression(HqlParser.InExpressionContext ctx) {
+
+ if (ctx.inList().simplePath() != null) {
+ throw new UnsupportedOperationException(
+ String.format(UNSUPPORTED_TEMPLATE, "IN clause with ELEMENTS or INDICES argument"));
+ } else if (ctx.inList().subquery() != null) {
+ throw new UnsupportedOperationException(String.format(UNSUPPORTED_TEMPLATE, "IN clause with a subquery"));
+ } else if (ctx.inList().parameter() != null) {
+ throw new UnsupportedOperationException(String.format(UNSUPPORTED_TEMPLATE, "IN clause with a parameter"));
+ }
+
+ CriteriaBuilder.In