diff --git a/pom.xml b/pom.xml
index 07101c166..a230dab8b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,6 +28,7 @@
42.2.19
3.0.0-SNAPSHOT
0.10.3
+ 4.3
org.hibernate
diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml
index 5960d72cc..e4f17e4ac 100644
--- a/spring-data-jpa/pom.xml
+++ b/spring-data-jpa/pom.xml
@@ -226,6 +226,14 @@
test
+
+ com.github.jsqlparser
+ jsqlparser
+ ${jsqlparser.version}
+ provided
+ true
+
+
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java
index 02f4ee9cc..7f950b018 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java
@@ -15,14 +15,6 @@
*/
package org.springframework.data.jpa.repository.query;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
import jakarta.persistence.EntityManager;
import jakarta.persistence.LockModeType;
import jakarta.persistence.Query;
@@ -31,6 +23,14 @@ import jakarta.persistence.Tuple;
import jakarta.persistence.TupleElement;
import jakarta.persistence.TypedQuery;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.EntityGraph;
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java
index 66f388667..2294ee18f 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java
@@ -34,6 +34,7 @@ import org.springframework.util.Assert;
* @author Tom Hombergs
* @author David Madden
* @author Mark Paluch
+ * @author Diego Krupitza
*/
abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
@@ -55,8 +56,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
* @param parser must not be {@literal null}.
*/
public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
- @Nullable String countQueryString,
- QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {
+ @Nullable String countQueryString, QueryMethodEvaluationContextProvider evaluationContextProvider,
+ SpelExpressionParser parser) {
super(method, em);
@@ -65,10 +66,12 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
Assert.notNull(parser, "Parser must not be null!");
this.evaluationContextProvider = evaluationContextProvider;
- this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation(), parser);
+ this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation(), parser,
+ method.isNativeQuery());
DeclaredQuery countQuery = query.deriveCountQuery(countQueryString, method.getCountQueryProjection());
- this.countQuery = ExpressionBasedStringQuery.from(countQuery, method.getEntityInformation(), parser);
+ this.countQuery = ExpressionBasedStringQuery.from(countQuery, method.getEntityInformation(), parser,
+ method.isNativeQuery());
this.parser = parser;
@@ -79,7 +82,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
@Override
public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
- String sortedQueryString = QueryUtils.applySorting(query.getQueryString(), accessor.getSort(), query.getAlias());
+ String sortedQueryString = QueryEnhancerFactory.forQuery(query) //
+ .applySorting(accessor.getSort(), query.getAlias());
ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
Query query = createJpaQuery(sortedQueryString, processor.getReturnedType());
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java
index 9aa455554..2359260da 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java
@@ -24,6 +24,7 @@ import org.springframework.util.ObjectUtils;
* A wrapper for a String representation of a query offering information about the query.
*
* @author Jens Schauder
+ * @author Diego Krupitza
* @since 2.0.3
*/
interface DeclaredQuery {
@@ -32,10 +33,11 @@ interface DeclaredQuery {
* Creates a {@literal DeclaredQuery} from a query {@literal String}.
*
* @param query might be {@literal null} or empty.
+ * @param nativeQuery is a given query is native or not
* @return a {@literal DeclaredQuery} instance even for a {@literal null} or empty argument.
*/
- static DeclaredQuery of(@Nullable String query) {
- return ObjectUtils.isEmpty(query) ? EmptyDeclaredQuery.EMPTY_QUERY : new StringQuery(query);
+ static DeclaredQuery of(@Nullable String query, boolean nativeQuery) {
+ return ObjectUtils.isEmpty(query) ? EmptyDeclaredQuery.EMPTY_QUERY : new StringQuery(query, nativeQuery);
}
/**
@@ -100,4 +102,13 @@ interface DeclaredQuery {
* @since 2.0.6
*/
boolean usesJdbcStyleParameters();
+
+ /**
+ * Return whether the query is a native query of not.
+ *
+ * @return true if native query otherwise false
+ */
+ default boolean isNativeQuery() {
+ return false;
+ }
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java
new file mode 100644
index 000000000..7504d8135
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2022 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 org.springframework.data.domain.Sort;
+
+import java.util.Set;
+
+/**
+ * The implementation of {@link QueryEnhancer} using {@link QueryUtils}.
+ *
+ * @author Diego Krupitza
+ */
+public class DefaultQueryEnhancer implements QueryEnhancer {
+
+ private final DeclaredQuery query;
+
+ public DefaultQueryEnhancer(DeclaredQuery query) {
+ this.query = query;
+ }
+
+ @Override
+ public String getExistsQueryString(String entityName, String countQueryPlaceHolder, Iterable idAttributes) {
+ return QueryUtils.getExistsQueryString(entityName, countQueryPlaceHolder, idAttributes);
+ }
+
+ @Override
+ public String getQueryString(String template, String entityName) {
+ return QueryUtils.getQueryString(template, entityName);
+ }
+
+ @Override
+ public String applySorting(Sort sort, String alias) {
+ return QueryUtils.applySorting(this.query.getQueryString(), sort, alias);
+ }
+
+ @Override
+ public String detectAlias() {
+ return QueryUtils.detectAlias(this.query.getQueryString());
+ }
+
+ @Override
+ public String createCountQueryFor(String countProjection) {
+ return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection);
+ }
+
+ @Override
+ public String getProjection() {
+ return QueryUtils.getProjection(this.query.getQueryString());
+ }
+
+ @Override
+ public Set getJoinAliases() {
+ return QueryUtils.getOuterJoinAliases(this.query.getQueryString());
+ }
+
+ @Override
+ public DeclaredQuery getQuery() {
+ return this.query;
+ }
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java
index cb4e18bda..725719785 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java
@@ -69,7 +69,7 @@ class EmptyDeclaredQuery implements DeclaredQuery {
Assert.hasText(countQuery, "CountQuery must not be empty!");
- return DeclaredQuery.of(countQuery);
+ return DeclaredQuery.of(countQuery, false);
}
@Override
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java
index eb773c198..74fffaa86 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java
@@ -36,6 +36,7 @@ import org.springframework.util.Assert;
* @author Oliver Gierke
* @author Tom Hombergs
* @author Michael J. Simons
+ * @author Diego Krupitza
*/
class ExpressionBasedStringQuery extends StringQuery {
@@ -55,9 +56,11 @@ class ExpressionBasedStringQuery extends StringQuery {
* @param query must not be {@literal null} or empty.
* @param metadata must not be {@literal null}.
* @param parser must not be {@literal null}.
+ * @param nativeQuery is a given query is native or not
*/
- public ExpressionBasedStringQuery(String query, JpaEntityMetadata> metadata, SpelExpressionParser parser) {
- super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser));
+ public ExpressionBasedStringQuery(String query, JpaEntityMetadata> metadata, SpelExpressionParser parser,
+ boolean nativeQuery) {
+ super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery && !containsExpression(query));
}
/**
@@ -66,11 +69,12 @@ class ExpressionBasedStringQuery extends StringQuery {
* @param query the original query. Must not be {@literal null}.
* @param metadata the {@link JpaEntityMetadata} for the given entity. Must not be {@literal null}.
* @param parser Parser for resolving SpEL expressions. Must not be {@literal null}.
+ * @param nativeQuery
* @return A query supporting SpEL expressions.
*/
- static ExpressionBasedStringQuery from(DeclaredQuery query, JpaEntityMetadata metadata,
- SpelExpressionParser parser) {
- return new ExpressionBasedStringQuery(query.getQueryString(), metadata, parser);
+ static ExpressionBasedStringQuery from(DeclaredQuery query, JpaEntityMetadata metadata, SpelExpressionParser parser,
+ boolean nativeQuery) {
+ return new ExpressionBasedStringQuery(query.getQueryString(), metadata, parser, nativeQuery);
}
/**
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java
new file mode 100644
index 000000000..220ddc868
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright 2022 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 net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.Function;
+import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
+import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.schema.Column;
+import net.sf.jsqlparser.schema.Table;
+import net.sf.jsqlparser.statement.select.*;
+import net.sf.jsqlparser.util.SelectUtils;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.util.Streamable;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static org.springframework.data.jpa.repository.query.JSqlParserUtils.*;
+import static org.springframework.data.jpa.repository.query.QueryUtils.checkSortExpression;
+
+/**
+ * The implementation of {@link QueryEnhancer} using JSqlParser.
+ *
+ * @author Diego Krupitza
+ */
+public class JSqlParserQueryEnhancer implements QueryEnhancer {
+
+ private static final String DEFAULT_TABLE_ALIAS = "x";
+
+ private final DeclaredQuery query;
+
+ /**
+ * @param query the query we want to enhance. Must not be {@literal null}.
+ */
+ public JSqlParserQueryEnhancer(DeclaredQuery query) {
+ this.query = query;
+ }
+
+ @Override
+ public String getExistsQueryString(String entityName, String countQueryPlaceHolder, Iterable idAttributes) {
+ final Table tableNameWithAlias = getTableWithAlias(entityName, DEFAULT_TABLE_ALIAS);
+ Function jSqlCount = getJSqlCount(Collections.singletonList(countQueryPlaceHolder), false);
+
+ Select select = SelectUtils.buildSelectFromTableAndSelectItems(tableNameWithAlias,
+ new SelectExpressionItem(jSqlCount));
+
+ PlainSelect selectBody = (PlainSelect) select.getSelectBody();
+
+ List equalityExpressions = Streamable.of(idAttributes).stream() //
+ .map(field -> {
+ Expression tableNameField = new Column().withTable(tableNameWithAlias).withColumnName(field);
+ Expression inputField = new Column(":".concat(field));
+ return new EqualsTo(tableNameField, inputField);
+ }).collect(Collectors.toList());
+
+ if (equalityExpressions.size() > 1) {
+ AndExpression rootOfWhereClause = concatenateWithAndExpression(equalityExpressions);
+ selectBody.setWhere(rootOfWhereClause);
+ } else if (equalityExpressions.size() == 1) {
+ selectBody.setWhere(equalityExpressions.get(0));
+ }
+
+ return selectBody.toString();
+ }
+
+ @Override
+ public String getQueryString(String template, String entityName) {
+ Assert.hasText(entityName, "Entity name must not be null or empty!");
+ return String.format(template, entityName);
+ }
+
+ @Override
+ public String applySorting(Sort sort, String alias) {
+ String queryString = query.getQueryString();
+ Assert.hasText(queryString, "Query must not be null or empty!");
+
+ if (sort.isUnsorted()) {
+ return queryString;
+ }
+
+ Select selectStatement = parseSelectStatement(queryString);
+ PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
+
+ final Set joinAliases = getJoinAliases(selectBody);
+
+ final Set selectionAliases = getSelectionAliases(selectBody);
+
+ List orderByElements = sort.stream() //
+ .map(order -> getOrderClause(joinAliases, selectionAliases, alias, order)) //
+ .collect(Collectors.toList());
+
+ if (CollectionUtils.isEmpty(selectBody.getOrderByElements())) {
+ selectBody.setOrderByElements(new ArrayList<>());
+ }
+
+ selectBody.getOrderByElements().addAll(orderByElements);
+
+ return selectBody.toString();
+
+ }
+
+ /**
+ * Returns the aliases used inside the selection part in the query.
+ *
+ * @param selectBody a {@link PlainSelect} containing a query. Must not be {@literal null}.
+ * @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}.
+ */
+ private Set getSelectionAliases(PlainSelect selectBody) {
+
+ if (CollectionUtils.isEmpty(selectBody.getSelectItems())) {
+ return new HashSet<>();
+ }
+
+ return selectBody.getSelectItems().stream() //
+ .filter(SelectExpressionItem.class::isInstance) //
+ .map(item -> ((SelectExpressionItem) item).getAlias()) //
+ .filter(Objects::nonNull) //
+ .map(Alias::getName) //
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Returns the aliases used inside the selection part in the query.
+ *
+ * @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}.
+ */
+ Set getSelectionAliases() {
+ Select selectStatement = parseSelectStatement(this.query.getQueryString());
+ PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
+ return this.getSelectionAliases(selectBody);
+ }
+
+ /**
+ * Returns the aliases used for {@code join}s.
+ *
+ * @param query a query string to extract the aliases of joins from. Must not be {@literal null}.
+ * @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}.
+ */
+ private Set getJoinAliases(String query) {
+ return getJoinAliases((PlainSelect) parseSelectStatement(query).getSelectBody());
+ }
+
+ /**
+ * Returns the aliases used for {@code join}s.
+ *
+ * @param selectBody the selection body to extract the aliases of joins from. Must not be {@literal null}.
+ * @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}.
+ */
+ private Set getJoinAliases(PlainSelect selectBody) {
+
+ if (CollectionUtils.isEmpty(selectBody.getJoins())) {
+ return new HashSet<>();
+ }
+
+ return selectBody.getJoins().stream() //
+ .map(join -> join.getRightItem().getAlias()) //
+ .filter(Objects::nonNull) //
+ .map(Alias::getName) //
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Returns the order clause for the given {@link Sort.Order}. Will prefix the clause with the given alias if the
+ * referenced property refers to a join alias, i.e. starts with {@code $alias.}.
+ *
+ * @param joinAliases the join aliases of the original query. Must not be {@literal null}.
+ * @param alias the alias for the root entity. May be {@literal null}.
+ * @param order the order object to build the clause for. Must not be {@literal null}.
+ * @return a {@link OrderByElement} containing an order clause. Guaranteed to be not {@literal null}.
+ */
+ private OrderByElement getOrderClause(final Set joinAliases, final Set selectionAliases,
+ final String alias, final Sort.Order order) {
+
+ final OrderByElement orderByElement = new OrderByElement();
+ orderByElement.setAsc(order.getDirection().isAscending());
+ orderByElement.setAscDescPresent(true);
+
+ final String property = order.getProperty();
+
+ checkSortExpression(order);
+
+ if (selectionAliases.contains(property)) {
+ Expression orderExpression = order.isIgnoreCase() ? getJSqlLower(property) : new Column(property);
+
+ orderByElement.setExpression(orderExpression);
+ return orderByElement;
+ }
+
+ boolean qualifyReference = joinAliases //
+ .parallelStream() //
+ .map(joinAlias -> joinAlias.concat(".")) //
+ .noneMatch(property::startsWith);
+
+ boolean functionIndicator = property.contains("(");
+
+ String reference = qualifyReference && !functionIndicator && StringUtils.hasText(alias)
+ ? String.format("%s.%s", alias, property)
+ : property;
+ Expression orderExpression = order.isIgnoreCase() ? getJSqlLower(reference) : new Column(reference);
+ orderByElement.setExpression(orderExpression);
+ return orderByElement;
+ }
+
+ @Override
+ public String detectAlias() {
+ return detectAlias(this.query.getQueryString());
+ }
+
+ /**
+ * Resolves the alias for the entity to be retrieved from the given JPA query. Note that you only provide valid Query
+ * strings. Things such as from User u will throw an {@link IllegalArgumentException}.
+ *
+ * @param query must not be {@literal null}.
+ * @return Might return {@literal null}.
+ */
+ private String detectAlias(String query) {
+ Select selectStatement = parseSelectStatement(query);
+ PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
+ return detectAlias(selectBody);
+ }
+
+ /**
+ * Resolves the alias for the entity to be retrieved from the given {@link PlainSelect}. Note that you only provide
+ * valid Query strings. Things such as from User u will throw an {@link IllegalArgumentException}.
+ *
+ * @param selectBody must not be {@literal null}.
+ * @return Might return {@literal null}.
+ */
+ private static String detectAlias(PlainSelect selectBody) {
+ Alias alias = selectBody.getFromItem().getAlias();
+ return alias == null ? null : alias.getName();
+ }
+
+ @Override
+ public String createCountQueryFor(String countProjection) {
+
+ Assert.hasText(this.query.getQueryString(), "OriginalQuery must not be null or empty!");
+
+ Select selectStatement = parseSelectStatement(this.query.getQueryString());
+ PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
+
+ // remove order by
+ selectBody.setOrderByElements(null);
+
+ if (StringUtils.hasText(countProjection)) {
+ Function jSqlCount = getJSqlCount(Collections.singletonList(countProjection), false);
+ selectBody.setSelectItems(Collections.singletonList(new SelectExpressionItem(jSqlCount)));
+ return selectBody.toString();
+ }
+
+ boolean distinct = selectBody.getDistinct() != null;
+ selectBody.setDistinct(null); // reset possible distinct
+
+ String tableAlias = detectAlias(selectBody);
+
+ // is never null
+ List selectItems = selectBody.getSelectItems();
+
+ if (onlyASingleColumnProjection(selectItems)) {
+ SelectExpressionItem singleProjection = (SelectExpressionItem) selectItems.get(0);
+
+ Column column = (Column) singleProjection.getExpression();
+ String countProp = column.getFullyQualifiedName();
+
+ Function jSqlCount = getJSqlCount(Collections.singletonList(countProp), distinct);
+ selectBody.setSelectItems(Collections.singletonList(new SelectExpressionItem(jSqlCount)));
+ return selectBody.toString();
+ }
+
+ String countProp = tableAlias == null ? "*" : tableAlias;
+
+ Function jSqlCount = getJSqlCount(Collections.singletonList(countProp), distinct);
+ selectBody.setSelectItems(Collections.singletonList(new SelectExpressionItem(jSqlCount)));
+
+ return selectBody.toString();
+
+ }
+
+ @Override
+ public String getProjection() {
+ Assert.hasText(query.getQueryString(), "Query must not be null or empty!");
+
+ Select selectStatement = parseSelectStatement(query.getQueryString());
+ PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
+
+ return selectBody.getSelectItems() //
+ .stream() //
+ .map(Object::toString) //
+ .collect(Collectors.joining(", ")).trim();
+ }
+
+ @Override
+ public Set getJoinAliases() {
+ return this.getJoinAliases(this.query.getQueryString());
+ }
+
+ /**
+ * Parses a query string with JSqlParser.
+ *
+ * @param query the query to parse
+ * @return the parsed query
+ */
+ private static Select parseSelectStatement(String query) {
+ try {
+ return (Select) CCJSqlParserUtil.parse(query);
+ } catch (JSQLParserException e) {
+ throw new IllegalArgumentException("The query you provided is not a valid SQL Query!", e);
+ }
+ }
+
+ /**
+ * Checks whether a given projection only contains a single column definition (aka without functions, etc)
+ *
+ * @param projection the projection to analyse
+ * @return true when the projection only contains a single column definition otherwise false
+ */
+ private boolean onlyASingleColumnProjection(List projection) {
+ // this is unfortunately the only way to check without any hacky & hard string regex magic
+ return projection.size() == 1 && projection.get(0) instanceof SelectExpressionItem
+ && (((SelectExpressionItem) projection.get(0)).getExpression()) instanceof Column;
+ }
+
+ @Override
+ public DeclaredQuery getQuery() {
+ return this.query;
+ }
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserUtils.java
new file mode 100644
index 000000000..1cc4eb1d6
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserUtils.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2022 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 net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.Function;
+import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
+import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
+import net.sf.jsqlparser.schema.Column;
+import net.sf.jsqlparser.schema.Table;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * A utility class for JSqlParser.
+ *
+ * @author Diego Krupitza
+ */
+public final class JSqlParserUtils {
+
+ private JSqlParserUtils() {
+ }
+
+ /**
+ * Generates a JSqlParser table from an entity name and an optional alias name
+ *
+ * @param entityName the name of the table
+ * @param alias the optional alias. Might be {@literal null} or empty
+ * @return the newly generated table
+ */
+ public static Table getTableWithAlias(String entityName, String alias) {
+ Table table = new Table(entityName);
+ return StringUtils.hasText(alias) ? table.withAlias(new Alias(alias)) : table;
+ }
+
+ /**
+ * Concatenates a list of expression with AND.
+ *
+ * @param expressions the list of expressions to concatenate. Has to be non empty and with size >= 2
+ * @return the root of the concatenated expression
+ */
+ public static AndExpression concatenateWithAndExpression(List expressions) {
+
+ if (CollectionUtils.isEmpty(expressions) || expressions.size() == 1) {
+ throw new IllegalArgumentException(
+ "The list of expression has to be at least of length 2! Otherwise it is not possible to concatinate with an");
+ }
+
+ AndExpression rootAndExpression = new AndExpression();
+ AndExpression currentLocation = rootAndExpression;
+
+ // traverse the list with looking 1 element ahead
+ for (int i = 0; i < expressions.size(); i++) {
+ Expression currentExpression = expressions.get(i);
+ if (currentLocation.getLeftExpression() == null) {
+ currentLocation.setLeftExpression(currentExpression);
+ } else if (currentLocation.getRightExpression() == null && i == expressions.size() - 1) {
+ currentLocation.setRightExpression(currentExpression);
+ } else {
+ AndExpression nextAndExpression = new AndExpression();
+ nextAndExpression.setLeftExpression(currentExpression);
+
+ currentLocation.setRightExpression(nextAndExpression);
+ currentLocation = (AndExpression) currentLocation.getRightExpression();
+ }
+ }
+
+ return rootAndExpression;
+ }
+
+ /**
+ * Generates a count function call, based on the {@code countFields}.
+ *
+ * @param countFields the non-empty list of fields that are used for counting
+ * @param distinct if it should be a distinct count
+ * @return the generated count function call
+ */
+ public static Function getJSqlCount(final List countFields, final boolean distinct) {
+ List countColumns = countFields //
+ .stream() //
+ .map(Column::new) //
+ .collect(Collectors.toList());
+
+ ExpressionList countExpression = new ExpressionList(countColumns);
+ return new Function() //
+ .withName("count") //
+ .withParameters(countExpression) //
+ .withDistinct(distinct);
+ }
+
+ /**
+ * Generates a lower function call, based on the {@code column}.
+ *
+ * @param column the non-empty column to use as param for lower
+ * @return the generated lower function call
+ */
+ public static Function getJSqlLower(String column) {
+ List expressions = Collections.singletonList(new Column(column));
+ ExpressionList lowerParamExpression = new ExpressionList(expressions);
+ return new Function() //
+ .withName("lower") //
+ .withParameters(lowerParamExpression);
+ }
+
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java
index ad61f599f..4afd0a79a 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java
@@ -166,10 +166,8 @@ public class JpaQueryCreator extends AbstractQueryCreator typeToRead = returnedType.getReturnedType();
- query = typeToRead.isInterface()
- ? query.multiselect(selections)
- : query.select((Selection) builder.construct(typeToRead,
- selections.toArray(new Selection[0])));
+ query = typeToRead.isInterface() ? query.multiselect(selections)
+ : query.select((Selection) builder.construct(typeToRead, selections.toArray(new Selection[0])));
} else if (tree.isExistsProjection()) {
@@ -241,83 +239,83 @@ public class JpaQueryCreator extends AbstractQueryCreator first = provider.next(part);
- ParameterMetadata second = provider.next(part);
- return builder.between(getComparablePath(root, part), first.getExpression(), second.getExpression());
- case AFTER:
- case GREATER_THAN:
- return builder.greaterThan(getComparablePath(root, part),
- provider.next(part, Comparable.class).getExpression());
- case GREATER_THAN_EQUAL:
- return builder.greaterThanOrEqualTo(getComparablePath(root, part),
- provider.next(part, Comparable.class).getExpression());
- case BEFORE:
- case LESS_THAN:
- return builder.lessThan(getComparablePath(root, part), provider.next(part, Comparable.class).getExpression());
- case LESS_THAN_EQUAL:
- return builder.lessThanOrEqualTo(getComparablePath(root, part),
- provider.next(part, Comparable.class).getExpression());
- case IS_NULL:
- return getTypedPath(root, part).isNull();
- case IS_NOT_NULL:
- return getTypedPath(root, part).isNotNull();
- case NOT_IN:
- // cast required for eclipselink workaround, see DATAJPA-433
- return upperIfIgnoreCase(getTypedPath(root, part))
- .in((Expression>) provider.next(part, Collection.class).getExpression()).not();
- case IN:
- // cast required for eclipselink workaround, see DATAJPA-433
- return upperIfIgnoreCase(getTypedPath(root, part))
- .in((Expression>) provider.next(part, Collection.class).getExpression());
- case STARTING_WITH:
- case ENDING_WITH:
- case CONTAINING:
- case NOT_CONTAINING:
+ case BETWEEN:
+ ParameterMetadata first = provider.next(part);
+ ParameterMetadata second = provider.next(part);
+ return builder.between(getComparablePath(root, part), first.getExpression(), second.getExpression());
+ case AFTER:
+ case GREATER_THAN:
+ return builder.greaterThan(getComparablePath(root, part),
+ provider.next(part, Comparable.class).getExpression());
+ case GREATER_THAN_EQUAL:
+ return builder.greaterThanOrEqualTo(getComparablePath(root, part),
+ provider.next(part, Comparable.class).getExpression());
+ case BEFORE:
+ case LESS_THAN:
+ return builder.lessThan(getComparablePath(root, part), provider.next(part, Comparable.class).getExpression());
+ case LESS_THAN_EQUAL:
+ return builder.lessThanOrEqualTo(getComparablePath(root, part),
+ provider.next(part, Comparable.class).getExpression());
+ case IS_NULL:
+ return getTypedPath(root, part).isNull();
+ case IS_NOT_NULL:
+ return getTypedPath(root, part).isNotNull();
+ case NOT_IN:
+ // cast required for eclipselink workaround, see DATAJPA-433
+ return upperIfIgnoreCase(getTypedPath(root, part))
+ .in((Expression>) provider.next(part, Collection.class).getExpression()).not();
+ case IN:
+ // cast required for eclipselink workaround, see DATAJPA-433
+ return upperIfIgnoreCase(getTypedPath(root, part))
+ .in((Expression>) provider.next(part, Collection.class).getExpression());
+ case STARTING_WITH:
+ case ENDING_WITH:
+ case CONTAINING:
+ case NOT_CONTAINING:
- if (property.getLeafProperty().isCollection()) {
+ if (property.getLeafProperty().isCollection()) {
- Expression> propertyExpression = traversePath(root, property);
- ParameterExpression