diff --git a/pom.xml b/pom.xml
index 906fa1e40..d659532f1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,6 +27,7 @@
42.2.19
2.7.0-SNAPSHOT
0.10.3
+ 4.3
org.hibernate
@@ -372,6 +373,13 @@
test
+
+ com.github.jsqlparser
+ jsqlparser
+ ${jsqlparser.version}
+ provided
+ true
+
diff --git a/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java
index 870937550..89cca6e03 100644
--- a/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java
+++ b/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java
@@ -69,7 +69,7 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
private final PersistenceProvider provider;
private final Lazy execution;
- final Lazy parameterBinder = Lazy.of(this::createBinder);
+ final Lazy parameterBinder = new Lazy<>(this::createBinder);
/**
* Creates a new {@link AbstractJpaQuery} from the given {@link JpaQueryMethod}.
diff --git a/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java
index f53fa2eab..50cce6eed 100644
--- a/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java
+++ b/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;
@@ -83,7 +86,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/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java
index 9aa455554..2359260da 100644
--- a/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java
+++ b/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/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java b/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java
new file mode 100644
index 000000000..7504d8135
--- /dev/null
+++ b/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/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java
index 09bf8cbfb..0a3a11359 100644
--- a/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java
+++ b/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java
@@ -97,7 +97,7 @@ class EmptyDeclaredQuery implements DeclaredQuery {
Assert.hasText(countQuery, "CountQuery must not be empty!");
- return DeclaredQuery.of(countQuery);
+ return DeclaredQuery.of(countQuery, false);
}
/*
diff --git a/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java
index eb773c198..74fffaa86 100644
--- a/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java
+++ b/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/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java b/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java
new file mode 100644
index 000000000..220ddc868
--- /dev/null
+++ b/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/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserUtils.java b/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserUtils.java
new file mode 100644
index 000000000..1cc4eb1d6
--- /dev/null
+++ b/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/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java
index 08716576d..fccdc093f 100644
--- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java
+++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java
@@ -178,10 +178,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()) {
@@ -253,83 +251,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 parameterExpression = provider.next(part).getExpression();
+ Expression> propertyExpression = traversePath(root, property);
+ ParameterExpression parameterExpression = provider.next(part).getExpression();
- // Can't just call .not() in case of negation as EclipseLink chokes on that.
- return type.equals(NOT_CONTAINING) ? isNotMember(builder, parameterExpression, propertyExpression)
- : isMember(builder, parameterExpression, propertyExpression);
- }
+ // Can't just call .not() in case of negation as EclipseLink chokes on that.
+ return type.equals(NOT_CONTAINING) ? isNotMember(builder, parameterExpression, propertyExpression)
+ : isMember(builder, parameterExpression, propertyExpression);
+ }
- case LIKE:
- case NOT_LIKE:
- Expression stringPath = getTypedPath(root, part);
- Expression propertyExpression = upperIfIgnoreCase(stringPath);
- Expression parameterExpression = upperIfIgnoreCase(provider.next(part, String.class).getExpression());
- Predicate like = builder.like(propertyExpression, parameterExpression, escape.getEscapeCharacter());
- return type.equals(NOT_LIKE) || type.equals(NOT_CONTAINING) ? like.not() : like;
- case TRUE:
- Expression truePath = getTypedPath(root, part);
- return builder.isTrue(truePath);
- case FALSE:
- Expression falsePath = getTypedPath(root, part);
- return builder.isFalse(falsePath);
- case SIMPLE_PROPERTY:
- ParameterMetadata expression = provider.next(part);
- Expression path = getTypedPath(root, part);
- return expression.isIsNullParameter() ? path.isNull()
- : builder.equal(upperIfIgnoreCase(path), upperIfIgnoreCase(expression.getExpression()));
- case NEGATING_SIMPLE_PROPERTY:
- return builder.notEqual(upperIfIgnoreCase(getTypedPath(root, part)),
- upperIfIgnoreCase(provider.next(part).getExpression()));
- case IS_EMPTY:
- case IS_NOT_EMPTY:
+ case LIKE:
+ case NOT_LIKE:
+ Expression stringPath = getTypedPath(root, part);
+ Expression propertyExpression = upperIfIgnoreCase(stringPath);
+ Expression parameterExpression = upperIfIgnoreCase(provider.next(part, String.class).getExpression());
+ Predicate like = builder.like(propertyExpression, parameterExpression, escape.getEscapeCharacter());
+ return type.equals(NOT_LIKE) || type.equals(NOT_CONTAINING) ? like.not() : like;
+ case TRUE:
+ Expression truePath = getTypedPath(root, part);
+ return builder.isTrue(truePath);
+ case FALSE:
+ Expression falsePath = getTypedPath(root, part);
+ return builder.isFalse(falsePath);
+ case SIMPLE_PROPERTY:
+ ParameterMetadata expression = provider.next(part);
+ Expression path = getTypedPath(root, part);
+ return expression.isIsNullParameter() ? path.isNull()
+ : builder.equal(upperIfIgnoreCase(path), upperIfIgnoreCase(expression.getExpression()));
+ case NEGATING_SIMPLE_PROPERTY:
+ return builder.notEqual(upperIfIgnoreCase(getTypedPath(root, part)),
+ upperIfIgnoreCase(provider.next(part).getExpression()));
+ case IS_EMPTY:
+ case IS_NOT_EMPTY:
- if (!property.getLeafProperty().isCollection()) {
- throw new IllegalArgumentException("IsEmpty / IsNotEmpty can only be used on collection properties!");
- }
+ if (!property.getLeafProperty().isCollection()) {
+ throw new IllegalArgumentException("IsEmpty / IsNotEmpty can only be used on collection properties!");
+ }
- Expression> collectionPath = traversePath(root, property);
- return type.equals(IS_NOT_EMPTY) ? builder.isNotEmpty(collectionPath) : builder.isEmpty(collectionPath);
+ Expression> collectionPath = traversePath(root, property);
+ return type.equals(IS_NOT_EMPTY) ? builder.isNotEmpty(collectionPath) : builder.isEmpty(collectionPath);
- default:
- throw new IllegalArgumentException("Unsupported keyword " + type);
+ default:
+ throw new IllegalArgumentException("Unsupported keyword " + type);
}
}
@@ -354,22 +352,22 @@ public class JpaQueryCreator extends AbstractQueryCreator) builder.upper((Expression) expression);
+
+ case WHEN_POSSIBLE:
+
+ if (canUpperCase(expression)) {
return (Expression) builder.upper((Expression) expression);
+ }
- case WHEN_POSSIBLE:
+ case NEVER:
+ default:
- if (canUpperCase(expression)) {
- return (Expression) builder.upper((Expression) expression);
- }
-
- case NEVER:
- default:
-
- return (Expression) expression;
+ return (Expression) expression;
}
}
diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java
index 2c08ecede..240b8e0ed 100644
--- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java
+++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java
@@ -155,7 +155,7 @@ public class JpaQueryMethod extends QueryMethod {
String annotatedQuery = getAnnotatedQuery();
- if (!DeclaredQuery.of(annotatedQuery).hasNamedParameter()) {
+ if (!DeclaredQuery.of(annotatedQuery, this.isNativeQuery.get()).hasNamedParameter()) {
return;
}
diff --git a/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java
index 02efd9078..6e56b33b4 100644
--- a/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java
+++ b/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java
@@ -78,7 +78,7 @@ final class NamedQuery extends AbstractJpaQuery {
Query query = em.createNamedQuery(queryName);
String queryString = extractor.extractQueryString(query);
- this.declaredQuery = DeclaredQuery.of(queryString);
+ this.declaredQuery = DeclaredQuery.of(queryString, false);
boolean weNeedToCreateCountQuery = !namedCountQueryIsPresent && method.getParameters().hasPageableParameter();
boolean cantExtractQuery = !this.extractor.canExtractQuery();
diff --git a/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java b/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java
new file mode 100644
index 000000000..b8fba6e99
--- /dev/null
+++ b/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java
@@ -0,0 +1,116 @@
+/*
+ * 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 java.util.Set;
+
+import org.springframework.data.domain.Sort;
+import org.springframework.lang.Nullable;
+
+/**
+ * This interface describes the API for enhancing a given Query.
+ *
+ * @author Diego Krupitza
+ */
+public interface QueryEnhancer {
+
+ /**
+ * Returns the query string to execute an exists query for the given id attributes.
+ *
+ * @param entityName the name of the entity to create the query for, must not be {@literal null}.
+ * @param countQueryPlaceHolder the placeholder for the count clause, must not be {@literal null}.
+ * @param idAttributes the id attributes for the entity, must not be {@literal null}.
+ */
+ String getExistsQueryString(String entityName, String countQueryPlaceHolder, Iterable idAttributes);
+
+ /**
+ * Returns the query string for the given class name.
+ *
+ * @param template must not be {@literal null}.
+ * @param entityName must not be {@literal null}.
+ * @return the template with placeholders replaced by the {@literal entityName}. Guaranteed to be not {@literal null}.
+ */
+ String getQueryString(String template, String entityName);
+
+ /**
+ * Adds {@literal order by} clause to the JPQL query. Uses the first alias to bind the sorting property to.
+ *
+ * @param sort the sort specification to apply.
+ * @return the modified query string.
+ */
+ default String applySorting(Sort sort) {
+ return applySorting(sort, detectAlias());
+ }
+
+ /**
+ * Adds {@literal order by} clause to the JPQL query.
+ *
+ * @param sort the sort specification to apply.
+ * @param alias the alias to be used in the order by clause. May be {@literal null} or empty.
+ * @return the modified query string.
+ */
+ String applySorting(Sort sort, @Nullable String alias);
+
+ /**
+ * Resolves the alias for the entity to be retrieved from the given JPA query.
+ *
+ * @return Might return {@literal null}.
+ */
+ @Nullable
+ String detectAlias();
+
+ /**
+ * Creates a count projected query from the given original query.
+ *
+ * @return Guaranteed to be not {@literal null}.
+ */
+ default String createCountQueryFor() {
+ return createCountQueryFor(null);
+ }
+
+ /**
+ * Creates a count projected query from the given original query using the provided countProjection.
+ *
+ * @param countProjection may be {@literal null}.
+ * @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}.
+ */
+ String createCountQueryFor(@Nullable String countProjection);
+
+ /**
+ * Returns whether the given JPQL query contains a constructor expression.
+ *
+ * @return whether the given JPQL query contains a constructor expression.
+ */
+ default boolean hasConstructorExpression() {
+ return QueryUtils.hasConstructorExpression(getQuery().getQueryString());
+ }
+
+ /**
+ * Returns the projection part of the query, i.e. everything between {@code select} and {@code from}.
+ *
+ * @return the projection part of the query.
+ */
+ String getProjection();
+
+ Set getJoinAliases();
+
+ /**
+ * Gets the query we want to use for enhancements.
+ *
+ * @return non null {@link DeclaredQuery} that wraps the query
+ */
+ DeclaredQuery getQuery();
+}
diff --git a/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactory.java b/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactory.java
new file mode 100644
index 000000000..d6123ddee
--- /dev/null
+++ b/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactory.java
@@ -0,0 +1,75 @@
+/*
+ * 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.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Encapsulates different strategies for the creation of a {@link QueryEnhancer} from a {@link DeclaredQuery}.
+ *
+ * @author Diego Krupitza
+ */
+public final class QueryEnhancerFactory {
+
+ private static final Log LOG = LogFactory.getLog(QueryEnhancerFactory.class);
+
+ private static final boolean JSQLPARSER_IN_CLASSPATH = isJSqlParserInClassPath();
+
+ private QueryEnhancerFactory() {
+ }
+
+ /**
+ * Creates a new {@link QueryEnhancer} for the given {@link DeclaredQuery}.
+ *
+ * @param query must not be {@literal null}.
+ * @return an implementation of {@link QueryEnhancer} that suits the query the most
+ */
+ public static QueryEnhancer forQuery(DeclaredQuery query) {
+ if (qualifiesForJSqlParserUsage(query)) {
+ return new JSqlParserQueryEnhancer(query);
+ } else {
+ return new DefaultQueryEnhancer(query);
+ }
+ }
+
+ /**
+ * Checks if a given query can be process with the JSqlParser under the condition that the parser is in the classpath.
+ *
+ * @param query the query we want to check
+ * @return true if JSqlParser is in the classpath and the query is classified as a native query otherwise
+ * false
+ */
+ private static boolean qualifiesForJSqlParserUsage(DeclaredQuery query) {
+ return JSQLPARSER_IN_CLASSPATH && query.isNativeQuery();
+ }
+
+ /**
+ * Checks whether JSqlParser is in classpath or not.
+ *
+ * @return true when in classpath otherwise false
+ */
+ private static boolean isJSqlParserInClassPath() {
+ try {
+ Class.forName("net.sf.jsqlparser.parser.JSqlParser", false, QueryEnhancerFactory.class.getClassLoader());
+ LOG.info("JSqlParser is in classpath. If applicable JSqlParser will be used.");
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java b/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java
index 720cb77d4..1c8abc35a 100644
--- a/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java
+++ b/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java
@@ -49,7 +49,8 @@ interface QueryParameterSetter {
void setParameter(BindableQuery query, JpaParametersParameterAccessor accessor, ErrorHandling errorHandling);
/** Noop implementation */
- QueryParameterSetter NOOP = (query, values, errorHandling) -> {};
+ QueryParameterSetter NOOP = (query, values, errorHandling) -> {
+ };
/**
* {@link QueryParameterSetter} for named or indexed parameters that might have a {@link TemporalType} specified.
diff --git a/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java b/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java
index ac7fbf304..3d6730c61 100644
--- a/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java
+++ b/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java
@@ -56,7 +56,7 @@ import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
- * Simple utility class to create JPA queries.
+ * Simple utility class to create JPA queries using the default implementation of a custom parser.
*
* @author Oliver Gierke
* @author Kevin Raymond
@@ -74,6 +74,7 @@ import org.springframework.util.StringUtils;
* @author Andriy Redko
* @author Peter Großmann
* @author Greg Turnquist
+ * @author Diego Krupitza
*/
public abstract class QueryUtils {
@@ -282,7 +283,7 @@ public abstract class QueryUtils {
* @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 String containing a order clause. Guaranteed to be not {@literal null}.
+ * @return a String containing an order clause. Guaranteed to be not {@literal null}.
*/
private static String getOrderClause(Set joinAliases, Set selectionAlias, @Nullable String alias,
Order order) {
@@ -812,7 +813,7 @@ public abstract class QueryUtils {
*
* @param order
*/
- private static void checkSortExpression(Order order) {
+ static void checkSortExpression(Order order) {
if (order instanceof JpaOrder && ((JpaOrder) order).isUnsafe()) {
return;
diff --git a/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java
index 847e4a2ac..217b43e76 100644
--- a/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java
+++ b/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java
@@ -46,6 +46,7 @@ import org.springframework.util.StringUtils;
* @author Oliver Wehrens
* @author Mark Paluch
* @author Jens Schauder
+ * @author Diego Krupitza
*/
class StringQuery implements DeclaredQuery {
@@ -55,6 +56,8 @@ class StringQuery implements DeclaredQuery {
private final boolean hasConstructorExpression;
private final boolean containsPageableInSpel;
private final boolean usesJdbcStyleParameters;
+ private final boolean isNative;
+ private final QueryEnhancer queryEnhancer;
/**
* Creates a new {@link StringQuery} from the given JPQL query.
@@ -62,10 +65,11 @@ class StringQuery implements DeclaredQuery {
* @param query must not be {@literal null} or empty.
*/
@SuppressWarnings("deprecation")
- StringQuery(String query) {
+ StringQuery(String query, boolean isNative) {
Assert.hasText(query, "Query must not be null or empty!");
+ this.isNative = isNative;
this.bindings = new ArrayList<>();
this.containsPageableInSpel = query.contains("#pageable");
@@ -74,8 +78,10 @@ class StringQuery implements DeclaredQuery {
this.bindings, queryMeta);
this.usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters;
- this.alias = QueryUtils.detectAlias(query);
- this.hasConstructorExpression = QueryUtils.hasConstructorExpression(query);
+
+ this.queryEnhancer = QueryEnhancerFactory.forQuery(this);
+ this.alias = this.queryEnhancer.detectAlias();
+ this.hasConstructorExpression = this.queryEnhancer.hasConstructorExpression();
}
/**
@@ -86,7 +92,7 @@ class StringQuery implements DeclaredQuery {
}
String getProjection() {
- return QueryUtils.getProjection(query);
+ return this.queryEnhancer.getProjection();
}
/*
@@ -106,8 +112,9 @@ class StringQuery implements DeclaredQuery {
@SuppressWarnings("deprecation")
public DeclaredQuery deriveCountQuery(@Nullable String countQuery, @Nullable String countQueryProjection) {
- return DeclaredQuery
- .of(countQuery != null ? countQuery : QueryUtils.createCountQueryFor(query, countQueryProjection));
+ return DeclaredQuery.of(
+ countQuery != null ? countQuery : this.queryEnhancer.createCountQueryFor(countQueryProjection), //
+ this.isNative);
}
/*
@@ -174,6 +181,11 @@ class StringQuery implements DeclaredQuery {
return containsPageableInSpel;
}
+ @Override
+ public boolean isNativeQuery() {
+ return isNative;
+ }
+
/**
* A parser that extracts the parameter bindings from a given query string.
*
@@ -263,7 +275,8 @@ class StringQuery implements DeclaredQuery {
Integer parameterIndex = getParameterIndex(parameterIndexString);
String typeSource = matcher.group(COMPARISION_TYPE_GROUP);
- Assert.isTrue(parameterIndexString != null || parameterName != null, () -> String.format("We need either a name or an index! Offending query string: %s", query));
+ Assert.isTrue(parameterIndexString != null || parameterName != null,
+ () -> String.format("We need either a name or an index! Offending query string: %s", query));
String expression = spelExtractor.getParameter(parameterName == null ? parameterIndexString : parameterName);
String replacement = null;
@@ -282,36 +295,36 @@ class StringQuery implements DeclaredQuery {
switch (ParameterBindingType.of(typeSource)) {
- case LIKE:
+ case LIKE:
- Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
- replacement = matcher.group(3);
+ Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
+ replacement = matcher.group(3);
- if (parameterIndex != null) {
- checkAndRegister(new LikeParameterBinding(parameterIndex, likeType, expression), bindings);
- } else {
- checkAndRegister(new LikeParameterBinding(parameterName, likeType, expression), bindings);
+ if (parameterIndex != null) {
+ checkAndRegister(new LikeParameterBinding(parameterIndex, likeType, expression), bindings);
+ } else {
+ checkAndRegister(new LikeParameterBinding(parameterName, likeType, expression), bindings);
- replacement = ":" + parameterName;
- }
+ replacement = ":" + parameterName;
+ }
- break;
+ break;
- case IN:
+ case IN:
- if (parameterIndex != null) {
- checkAndRegister(new InParameterBinding(parameterIndex, expression), bindings);
- } else {
- checkAndRegister(new InParameterBinding(parameterName, expression), bindings);
- }
+ if (parameterIndex != null) {
+ checkAndRegister(new InParameterBinding(parameterIndex, expression), bindings);
+ } else {
+ checkAndRegister(new InParameterBinding(parameterName, expression), bindings);
+ }
- break;
+ break;
- case AS_IS: // fall-through we don't need a special parameter binding for the given parameter.
- default:
+ case AS_IS: // fall-through we don't need a special parameter binding for the given parameter.
+ default:
- bindings.add(parameterIndex != null ? new ParameterBinding(null, parameterIndex, expression)
- : new ParameterBinding(parameterName, null, expression));
+ bindings.add(parameterIndex != null ? new ParameterBinding(null, parameterIndex, expression)
+ : new ParameterBinding(parameterName, null, expression));
}
if (replacement != null) {
diff --git a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java
index 92382022a..9424285b4 100644
--- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java
+++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java
@@ -103,10 +103,12 @@ import org.springframework.transaction.annotation.Transactional;
@Transactional
public class UserRepositoryTests {
- @PersistenceContext EntityManager em;
+ @PersistenceContext
+ EntityManager em;
// CUT
- @Autowired UserRepository repository;
+ @Autowired
+ UserRepository repository;
// Test fixture
private User firstUser;
diff --git a/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryUtilsUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryUtilsUnitTests.java
new file mode 100644
index 000000000..ec08e3ea9
--- /dev/null
+++ b/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryUtilsUnitTests.java
@@ -0,0 +1,530 @@
+/*
+ * Copyright 2008-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 static org.assertj.core.api.Assertions.*;
+import static org.springframework.data.jpa.repository.query.QueryUtils.*;
+
+import java.util.Collections;
+import java.util.Set;
+
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.dao.InvalidDataAccessApiUsageException;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Sort.Order;
+import org.springframework.data.jpa.domain.JpaSort;
+
+/**
+ * Unit test for {@link QueryUtils}.
+ *
+ * @author Oliver Gierke
+ * @author Thomas Darimont
+ * @author Komi Innocent
+ * @author Christoph Strobl
+ * @author Jens Schauder
+ * @author Florian Lüdiger
+ * @author Grégoire Druant
+ * @author Mohammad Hewedy
+ * @author Greg Turnquist
+ */
+class DefaultQueryUtilsUnitTests {
+
+ private static final String QUERY = "select u from User u";
+ private static final String FQ_QUERY = "select u from org.acme.domain.User$Foo_Bar u";
+ private static final String SIMPLE_QUERY = "from User u";
+ private static final String COUNT_QUERY = "select count(u) from User u";
+
+ private static final String QUERY_WITH_AS = "select u from User as u where u.username = ?";
+
+ @Test
+ void createsCountQueryCorrectly() {
+ assertCountQuery(QUERY, COUNT_QUERY);
+ }
+
+ @Test
+ void createsCountQueriesCorrectlyForCapitalLetterJPQL() {
+
+ assertCountQuery("FROM User u WHERE u.foo.bar = ?", "select count(u) FROM User u WHERE u.foo.bar = ?");
+
+ assertCountQuery("SELECT u FROM User u where u.foo.bar = ?", "select count(u) FROM User u where u.foo.bar = ?");
+ }
+
+ @Test
+ void createsCountQueryForDistinctQueries() {
+
+ assertCountQuery("select distinct u from User u where u.foo = ?",
+ "select count(distinct u) from User u where u.foo = ?");
+ }
+
+ @Test
+ void createsCountQueryForConstructorQueries() {
+
+ assertCountQuery("select distinct new User(u.name) from User u where u.foo = ?",
+ "select count(distinct u) from User u where u.foo = ?");
+ }
+
+ @Test
+ void createsCountQueryForJoins() {
+
+ assertCountQuery("select distinct new User(u.name) from User u left outer join u.roles r WHERE r = ?",
+ "select count(distinct u) from User u left outer join u.roles r WHERE r = ?");
+ }
+
+ @Test
+ void createsCountQueryForQueriesWithSubSelects() {
+
+ assertCountQuery("select u from User u left outer join u.roles r where r in (select r from Role)",
+ "select count(u) from User u left outer join u.roles r where r in (select r from Role)");
+ }
+
+ @Test
+ void createsCountQueryForAliasesCorrectly() {
+
+ assertCountQuery("select u from User as u", "select count(u) from User as u");
+ }
+
+ @Test
+ void allowsShortJpaSyntax() {
+
+ assertCountQuery(SIMPLE_QUERY, COUNT_QUERY);
+ }
+
+ @Test
+ void detectsAliasCorrectly() {
+
+ assertThat(detectAlias(QUERY)).isEqualTo("u");
+ assertThat(detectAlias(SIMPLE_QUERY)).isEqualTo("u");
+ assertThat(detectAlias(COUNT_QUERY)).isEqualTo("u");
+ assertThat(detectAlias(QUERY_WITH_AS)).isEqualTo("u");
+ assertThat(detectAlias("SELECT FROM USER U")).isEqualTo("U");
+ assertThat(detectAlias("select u from User u")).isEqualTo("u");
+ assertThat(detectAlias("select u from com.acme.User u")).isEqualTo("u");
+ assertThat(detectAlias("select u from T05User u")).isEqualTo("u");
+ }
+
+ @Test
+ void allowsFullyQualifiedEntityNamesInQuery() {
+
+ assertThat(detectAlias(FQ_QUERY)).isEqualTo("u");
+ assertCountQuery(FQ_QUERY, "select count(u) from org.acme.domain.User$Foo_Bar u");
+ }
+
+ @Test // DATAJPA-252
+ void detectsJoinAliasesCorrectly() {
+
+ Set aliases = getOuterJoinAliases("select p from Person p left outer join x.foo b2_$ar where …");
+ assertThat(aliases).hasSize(1);
+ assertThat(aliases).contains("b2_$ar");
+
+ aliases = getOuterJoinAliases("select p from Person p left join x.foo b2_$ar where …");
+ assertThat(aliases).hasSize(1);
+ assertThat(aliases).contains("b2_$ar");
+
+ aliases = getOuterJoinAliases(
+ "select p from Person p left outer join x.foo as b2_$ar, left join x.bar as foo where …");
+ assertThat(aliases).hasSize(2);
+ assertThat(aliases).contains("b2_$ar", "foo");
+
+ aliases = getOuterJoinAliases(
+ "select p from Person p left join x.foo as b2_$ar, left outer join x.bar foo where …");
+ assertThat(aliases).hasSize(2);
+ assertThat(aliases).contains("b2_$ar", "foo");
+ }
+
+ @Test // DATAJPA-252
+ void doesNotPrefixOrderReferenceIfOuterJoinAliasDetected() {
+
+ String query = "select p from Person p left join p.address address";
+ assertThat(applySorting(query, Sort.by("address.city"))).endsWith("order by address.city asc");
+ assertThat(applySorting(query, Sort.by("address.city", "lastname"), "p"))
+ .endsWith("order by address.city asc, p.lastname asc");
+ }
+
+ @Test // DATAJPA-252
+ void extendsExistingOrderByClausesCorrectly() {
+
+ String query = "select p from Person p order by p.lastname asc";
+ assertThat(applySorting(query, Sort.by("firstname"), "p")).endsWith("order by p.lastname asc, p.firstname asc");
+ }
+
+ @Test // DATAJPA-296
+ void appliesIgnoreCaseOrderingCorrectly() {
+
+ Sort sort = Sort.by(Order.by("firstname").ignoreCase());
+
+ String query = "select p from Person p";
+ assertThat(applySorting(query, sort, "p")).endsWith("order by lower(p.firstname) asc");
+ }
+
+ @Test // DATAJPA-296
+ void appendsIgnoreCaseOrderingCorrectly() {
+
+ Sort sort = Sort.by(Order.by("firstname").ignoreCase());
+
+ String query = "select p from Person p order by p.lastname asc";
+ assertThat(applySorting(query, sort, "p")).endsWith("order by p.lastname asc, lower(p.firstname) asc");
+ }
+
+ @Test // DATAJPA-342
+ void usesReturnedVariableInCOuntProjectionIfSet() {
+
+ assertCountQuery("select distinct m.genre from Media m where m.user = ?1 order by m.genre asc",
+ "select count(distinct m.genre) from Media m where m.user = ?1");
+ }
+
+ @Test // DATAJPA-343
+ void projectsCOuntQueriesForQueriesWithSubselects() {
+
+ assertCountQuery("select o from Foo o where cb.id in (select b from Bar b)",
+ "select count(o) from Foo o where cb.id in (select b from Bar b)");
+ }
+
+ @Test // DATAJPA-148
+ void doesNotPrefixSortsIfFunction() {
+
+ Sort sort = Sort.by("sum(foo)");
+ assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
+ .isThrownBy(() -> applySorting("select p from Person p", sort, "p"));
+ }
+
+ @Test // DATAJPA-377
+ void removesOrderByInGeneratedCountQueryFromOriginalQueryIfPresent() {
+
+ assertCountQuery("select distinct m.genre from Media m where m.user = ?1 OrDer By m.genre ASC",
+ "select count(distinct m.genre) from Media m where m.user = ?1");
+ }
+
+ @Test // DATAJPA-375
+ void findsExistingOrderByIndependentOfCase() {
+
+ Sort sort = Sort.by("lastname");
+ String query = applySorting("select p from Person p ORDER BY p.firstname", sort, "p");
+ assertThat(query).endsWith("ORDER BY p.firstname, p.lastname asc");
+ }
+
+ @Test // DATAJPA-409
+ void createsCountQueryForNestedReferenceCorrectly() {
+ assertCountQuery("select a.b from A a", "select count(a.b) from A a");
+ }
+
+ @Test // DATAJPA-420
+ void createsCountQueryForScalarSelects() {
+ assertCountQuery("select p.lastname,p.firstname from Person p", "select count(p) from Person p");
+ }
+
+ @Test // DATAJPA-456
+ void createCountQueryFromTheGivenCountProjection() {
+ assertThat(createCountQueryFor("select p.lastname,p.firstname from Person p", "p.lastname"))
+ .isEqualTo("select count(p.lastname) from Person p");
+ }
+
+ @Test // DATAJPA-726
+ void detectsAliassesInPlainJoins() {
+
+ String query = "select p from Customer c join c.productOrder p where p.delayed = true";
+ Sort sort = Sort.by("p.lineItems");
+
+ assertThat(applySorting(query, sort, "c")).endsWith("order by p.lineItems asc");
+ }
+
+ @Test // DATAJPA-736
+ void supportsNonAsciiCharactersInEntityNames() {
+ assertThat(createCountQueryFor("select u from Usèr u")).isEqualTo("select count(u) from Usèr u");
+ }
+
+ @Test // DATAJPA-798
+ void detectsAliasInQueryContainingLineBreaks() {
+ assertThat(detectAlias("select \n u \n from \n User \nu")).isEqualTo("u");
+ }
+
+ @Test // DATAJPA-815
+ void doesPrefixPropertyWith() {
+
+ String query = "from Cat c join Dog d";
+ Sort sort = Sort.by("dPropertyStartingWithJoinAlias");
+
+ assertThat(applySorting(query, sort, "c")).endsWith("order by c.dPropertyStartingWithJoinAlias asc");
+ }
+
+ @Test // DATAJPA-938
+ void detectsConstructorExpressionInDistinctQuery() {
+ assertThat(hasConstructorExpression("select distinct new Foo() from Bar b")).isTrue();
+ }
+
+ @Test // DATAJPA-938
+ void detectsComplexConstructorExpression() {
+
+ assertThat(hasConstructorExpression("select new foo.bar.Foo(ip.id, ip.name, sum(lp.amount)) " //
+ + "from Bar lp join lp.investmentProduct ip " //
+ + "where (lp.toDate is null and lp.fromDate <= :now and lp.fromDate is not null) and lp.accountId = :accountId " //
+ + "group by ip.id, ip.name, lp.accountId " //
+ + "order by ip.name ASC")).isTrue();
+ }
+
+ @Test // DATAJPA-938
+ void detectsConstructorExpressionWithLineBreaks() {
+ assertThat(hasConstructorExpression("select new foo.bar.FooBar(\na.id) from DtoA a ")).isTrue();
+ }
+
+ @Test // DATAJPA-960
+ void doesNotQualifySortIfNoAliasDetected() {
+ assertThat(applySorting("from mytable where ?1 is null", Sort.by("firstname"))).endsWith("order by firstname asc");
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void doesNotAllowWhitespaceInSort() {
+
+ Sort sort = Sort.by("case when foo then bar");
+ assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
+ .isThrownBy(() -> applySorting("select p from Person p", sort, "p"));
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void doesNotPrefixUnsageJpaSortFunctionCalls() {
+
+ JpaSort sort = JpaSort.unsafe("sum(foo)");
+ assertThat(applySorting("select p from Person p", sort, "p")).endsWith("order by sum(foo) asc");
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void doesNotPrefixMultipleAliasedFunctionCalls() {
+
+ String query = "SELECT AVG(m.price) AS avgPrice, SUM(m.stocks) AS sumStocks FROM Magazine m";
+ Sort sort = Sort.by("avgPrice", "sumStocks");
+
+ assertThat(applySorting(query, sort, "m")).endsWith("order by avgPrice asc, sumStocks asc");
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void doesNotPrefixSingleAliasedFunctionCalls() {
+
+ String query = "SELECT AVG(m.price) AS avgPrice FROM Magazine m";
+ Sort sort = Sort.by("avgPrice");
+
+ assertThat(applySorting(query, sort, "m")).endsWith("order by avgPrice asc");
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void prefixesSingleNonAliasedFunctionCallRelatedSortProperty() {
+
+ String query = "SELECT AVG(m.price) AS avgPrice FROM Magazine m";
+ Sort sort = Sort.by("someOtherProperty");
+
+ assertThat(applySorting(query, sort, "m")).endsWith("order by m.someOtherProperty asc");
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void prefixesNonAliasedFunctionCallRelatedSortPropertyWhenSelectClauseContainesAliasedFunctionForDifferentProperty() {
+
+ String query = "SELECT m.name, AVG(m.price) AS avgPrice FROM Magazine m";
+ Sort sort = Sort.by("name", "avgPrice");
+
+ assertThat(applySorting(query, sort, "m")).endsWith("order by m.name asc, avgPrice asc");
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void doesNotPrefixAliasedFunctionCallNameWithMultipleNumericParameters() {
+
+ String query = "SELECT SUBSTRING(m.name, 2, 5) AS trimmedName FROM Magazine m";
+ Sort sort = Sort.by("trimmedName");
+
+ assertThat(applySorting(query, sort, "m")).endsWith("order by trimmedName asc");
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void doesNotPrefixAliasedFunctionCallNameWithMultipleStringParameters() {
+
+ String query = "SELECT CONCAT(m.name, 'foo') AS extendedName FROM Magazine m";
+ Sort sort = Sort.by("extendedName");
+
+ assertThat(applySorting(query, sort, "m")).endsWith("order by extendedName asc");
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void doesNotPrefixAliasedFunctionCallNameWithUnderscores() {
+
+ String query = "SELECT AVG(m.price) AS avg_price FROM Magazine m";
+ Sort sort = Sort.by("avg_price");
+
+ assertThat(applySorting(query, sort, "m")).endsWith("order by avg_price asc");
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void doesNotPrefixAliasedFunctionCallNameWithDots() {
+
+ String query = "SELECT AVG(m.price) AS m.avg FROM Magazine m";
+ Sort sort = Sort.by("m.avg");
+
+ assertThat(applySorting(query, sort, "m")).endsWith("order by m.avg asc");
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void doesNotPrefixAliasedFunctionCallNameWhenQueryStringContainsMultipleWhiteSpaces() {
+
+ String query = "SELECT AVG( m.price ) AS avgPrice FROM Magazine m";
+ Sort sort = Sort.by("avgPrice");
+
+ assertThat(applySorting(query, sort, "m")).endsWith("order by avgPrice asc");
+ }
+
+ @Test // DATAJPA-1000
+ void discoversCorrectAliasForJoinFetch() {
+
+ Set aliases = QueryUtils
+ .getOuterJoinAliases("SELECT DISTINCT user FROM User user LEFT JOIN FETCH user.authorities AS authority");
+
+ assertThat(aliases).containsExactly("authority");
+ }
+
+ @Test // DATAJPA-1171
+ void doesNotContainStaticClauseInExistsQuery() {
+
+ assertThat(QueryUtils.getExistsQueryString("entity", "x", Collections.singleton("id"))) //
+ .endsWith("WHERE x.id = :id");
+ }
+
+ @Test // DATAJPA-1363
+ void discoversAliasWithComplexFunction() {
+
+ assertThat(QueryUtils
+ .getFunctionAliases("select new MyDto(sum(case when myEntity.prop3=0 then 1 else 0 end) as myAlias")) //
+ .contains("myAlias");
+ }
+
+ @Test // DATAJPA-1506
+ void detectsAliasWithGroupAndOrderBy() {
+
+ assertThat(detectAlias("select * from User group by name")).isNull();
+ assertThat(detectAlias("select * from User order by name")).isNull();
+ assertThat(detectAlias("select * from User u group by name")).isEqualTo("u");
+ assertThat(detectAlias("select * from User u order by name")).isEqualTo("u");
+ }
+
+ @Test // DATAJPA-1500
+ void createCountQuerySupportsWhitespaceCharacters() {
+
+ assertThat(createCountQueryFor("select * from User user\n" + //
+ " where user.age = 18\n" + //
+ " order by user.name\n ")).isEqualTo("select count(user) from User user\n" + //
+ " where user.age = 18\n ");
+ }
+
+ @Test
+ void createCountQuerySupportsLineBreaksInSelectClause() {
+
+ assertThat(createCountQueryFor("select user.age,\n" + //
+ " user.name\n" + //
+ " from User user\n" + //
+ " where user.age = 18\n" + //
+ " order\nby\nuser.name\n ")).isEqualTo("select count(user) from User user\n" + //
+ " where user.age = 18\n ");
+ }
+
+ @Test // DATAJPA-1061
+ void appliesSortCorrectlyForFieldAliases() {
+
+ String query = "SELECT m.price, lower(m.title) AS title, a.name as authorName FROM Magazine m INNER JOIN m.author a";
+ Sort sort = Sort.by("authorName");
+
+ String fullQuery = applySorting(query, sort);
+
+ assertThat(fullQuery).endsWith("order by authorName asc");
+ }
+
+ @Test // GH-2280
+ void appliesOrderingCorrectlyForFieldAliasWithIgnoreCase() {
+
+ String query = "SELECT customer.id as id, customer.name as name FROM CustomerEntity customer";
+ Sort sort = Sort.by(Order.by("name").ignoreCase());
+
+ String fullQuery = applySorting(query, sort);
+
+ assertThat(fullQuery).isEqualTo(
+ "SELECT customer.id as id, customer.name as name FROM CustomerEntity customer order by lower(name) asc");
+ }
+
+ @Test // DATAJPA-1061
+ void appliesSortCorrectlyForFunctionAliases() {
+
+ String query = "SELECT m.price, lower(m.title) AS title, a.name as authorName FROM Magazine m INNER JOIN m.author a";
+ Sort sort = Sort.by("title");
+
+ String fullQuery = applySorting(query, sort);
+
+ assertThat(fullQuery).endsWith("order by title asc");
+ }
+
+ @Test // DATAJPA-1061
+ void appliesSortCorrectlyForSimpleField() {
+
+ String query = "SELECT m.price, lower(m.title) AS title, a.name as authorName FROM Magazine m INNER JOIN m.author a";
+ Sort sort = Sort.by("price");
+
+ String fullQuery = applySorting(query, sort);
+
+ assertThat(fullQuery).endsWith("order by m.price asc");
+ }
+
+ @Test
+ void createCountQuerySupportsLineBreakRightAfterDistinct() {
+
+ assertThat(createCountQueryFor("select\ndistinct\nuser.age,\n" + //
+ "user.name\n" + //
+ "from\nUser\nuser")).isEqualTo(createCountQueryFor("select\ndistinct user.age,\n" + //
+ "user.name\n" + //
+ "from\nUser\nuser"));
+ }
+
+ @Test
+ void detectsAliasWithGroupAndOrderByWithLineBreaks() {
+
+ assertThat(detectAlias("select * from User group\nby name")).isNull();
+ assertThat(detectAlias("select * from User order\nby name")).isNull();
+ assertThat(detectAlias("select * from User u group\nby name")).isEqualTo("u");
+ assertThat(detectAlias("select * from User u order\nby name")).isEqualTo("u");
+ assertThat(detectAlias("select * from User\nu\norder \n by name")).isEqualTo("u");
+ }
+
+ @Test // DATAJPA-1679
+ void findProjectionClauseWithDistinct() {
+
+ SoftAssertions.assertSoftly(sofly -> {
+ sofly.assertThat(QueryUtils.getProjection("select * from x")).isEqualTo("*");
+ sofly.assertThat(QueryUtils.getProjection("select a, b, c from x")).isEqualTo("a, b, c");
+ sofly.assertThat(QueryUtils.getProjection("select distinct a, b, c from x")).isEqualTo("a, b, c");
+ sofly.assertThat(QueryUtils.getProjection("select DISTINCT a, b, c from x")).isEqualTo("a, b, c");
+ });
+ }
+
+ @Test // DATAJPA-1696
+ void findProjectionClauseWithSubselect() {
+
+ // This is not a required behavior, in fact the opposite is,
+ // but it documents a current limitation.
+ // to fix this without breaking findProjectionClauseWithIncludedFrom we need a more sophisticated parser.
+ assertThat(QueryUtils.getProjection("select * from (select x from y)")).isNotEqualTo("*");
+ }
+
+ @Test // DATAJPA-1696
+ void findProjectionClauseWithIncludedFrom() {
+ assertThat(QueryUtils.getProjection("select x, frommage, y from t")).isEqualTo("x, frommage, y");
+ }
+
+ private static void assertCountQuery(String originalQuery, String countQuery) {
+ assertThat(createCountQueryFor(originalQuery)).isEqualTo(countQuery);
+ }
+}
diff --git a/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java
index 7a747cb98..ac8148850 100644
--- a/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java
+++ b/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java
@@ -35,13 +35,15 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
* @author Jens Schauder
* @author Mark Paluch
* @author Michael J. Simons
+ * @author Diego Krupitza
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class ExpressionBasedStringQueryUnitTests {
private static final SpelExpressionParser SPEL_PARSER = new SpelExpressionParser();
- @Mock JpaEntityMetadata> metadata;
+ @Mock
+ JpaEntityMetadata> metadata;
@Test // DATAJPA-170
void shouldReturnQueryWithDomainTypeExpressionReplacedWithSimpleDomainTypeName() {
@@ -49,7 +51,7 @@ class ExpressionBasedStringQueryUnitTests {
when(metadata.getEntityName()).thenReturn("User");
String source = "select from #{#entityName} u where u.firstname like :firstname";
- StringQuery query = new ExpressionBasedStringQuery(source, metadata, SPEL_PARSER);
+ StringQuery query = new ExpressionBasedStringQuery(source, metadata, SPEL_PARSER, false);
assertThat(query.getQueryString()).isEqualTo("select from User u where u.firstname like :firstname");
}
@@ -58,7 +60,7 @@ class ExpressionBasedStringQueryUnitTests {
when(metadata.getEntityName()).thenReturn("User");
- StringQuery query = new ExpressionBasedStringQuery("select u from #{#entityName} u", metadata, SPEL_PARSER);
+ StringQuery query = new ExpressionBasedStringQuery("select u from #{#entityName} u", metadata, SPEL_PARSER, true);
assertThat(query.getAlias()).isEqualTo("u");
assertThat(query.getQueryString()).isEqualTo("select u from User u");
}
@@ -71,7 +73,7 @@ class ExpressionBasedStringQueryUnitTests {
+ "+ \"AND (LOWER(n.server) LIKE LOWER(NULLIF(text(concat('%',:#{#networkRequest.server},'%')), '')) OR :#{#networkRequest.server} IS NULL)\"\n"
+ "+ \"AND (n.createdAt >= :#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=:#{#networkRequest.createdTime.endDateTime})\"\n"
+ "+ \"AND (n.updatedAt >= :#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=:#{#networkRequest.updatedTime.endDateTime})",
- metadata, SPEL_PARSER);
+ metadata, SPEL_PARSER, false);
assertThat(query.getParameterBindings()).hasSize(8);
}
@@ -80,13 +82,39 @@ class ExpressionBasedStringQueryUnitTests {
void shouldDetectBindParameterCountCorrectlyWithJDBCStyleParameters() {
StringQuery query = new ExpressionBasedStringQuery(
- "select n from #{#entityName} n where (LOWER(n.name) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.name},'%')), '')) OR ?#{#networkRequest.name} IS NULL )\"\n"
- + "+ \"AND (LOWER(n.server) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.server},'%')), '')) OR ?#{#networkRequest.server} IS NULL)\"\n"
- + "+ \"AND (n.createdAt >= ?#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=?#{#networkRequest.createdTime.endDateTime})\"\n"
- + "+ \"AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})",
- metadata, SPEL_PARSER);
+ "select n from #{#entityName} n where (LOWER(n.name) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.name},'%')), '')) OR ?#{#networkRequest.name} IS NULL )\"\n"
+ + "+ \"AND (LOWER(n.server) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.server},'%')), '')) OR ?#{#networkRequest.server} IS NULL)\"\n"
+ + "+ \"AND (n.createdAt >= ?#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=?#{#networkRequest.createdTime.endDateTime})\"\n"
+ + "+ \"AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})",
+ metadata, SPEL_PARSER, false);
assertThat(query.getParameterBindings()).hasSize(8);
}
+ @Test
+ void shouldDetectComplexNativeQueriesWithSpelAsNonNative() {
+ StringQuery query = new ExpressionBasedStringQuery(
+ "select n from #{#entityName} n where (LOWER(n.name) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.name},'%')), '')) OR ?#{#networkRequest.name} IS NULL )\"\n"
+ + "+ \"AND (LOWER(n.server) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.server},'%')), '')) OR ?#{#networkRequest.server} IS NULL)\"\n"
+ + "+ \"AND (n.createdAt >= ?#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=?#{#networkRequest.createdTime.endDateTime})\"\n"
+ + "+ \"AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})",
+ metadata, SPEL_PARSER, true);
+
+ assertThat(query.isNativeQuery()).isFalse();
+ }
+
+ @Test
+ void shouldDetectSimpleNativeQueriesWithSpelAsNonNative() {
+ StringQuery query = new ExpressionBasedStringQuery("select n from #{#entityName} n", metadata, SPEL_PARSER, true);
+
+ assertThat(query.isNativeQuery()).isFalse();
+ }
+
+ @Test
+ void shouldDetectSimpleNativeQueriesWithoutSpelAsNonNative() {
+ StringQuery query = new ExpressionBasedStringQuery("select u from User u", metadata, SPEL_PARSER, true);
+
+ assertThat(query.isNativeQuery()).isTrue();
+ }
+
}
diff --git a/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java
index 6890b1dd8..f0a2045f6 100644
--- a/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java
+++ b/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java
@@ -74,8 +74,10 @@ public class JpaQueryMethodUnitTests {
private static final String METHOD_NAME = "findByFirstname";
- @Mock QueryExtractor extractor;
- @Mock RepositoryMetadata metadata;
+ @Mock
+ QueryExtractor extractor;
+ @Mock
+ RepositoryMetadata metadata;
private ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
private Method invalidReturnType;
@@ -518,7 +520,7 @@ public class JpaQueryMethodUnitTests {
interface ValidRepository extends Repository {
- @Query(value = "query", nativeQuery = true)
+ @Query(value = "Select u from User u where u.lastname = ?1", nativeQuery = true)
List findByLastname(String lastname);
@Query(name = "HateoasAwareSpringDataWebConfiguration.bar")
diff --git a/src/test/java/org/springframework/data/jpa/repository/query/ParameterBindingParserUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/ParameterBindingParserUnitTests.java
index 7c4f764fe..742dcf045 100644
--- a/src/test/java/org/springframework/data/jpa/repository/query/ParameterBindingParserUnitTests.java
+++ b/src/test/java/org/springframework/data/jpa/repository/query/ParameterBindingParserUnitTests.java
@@ -23,6 +23,7 @@ import org.springframework.data.jpa.repository.query.StringQuery.ParameterBindin
* Unit tests for the {@link ParameterBindingParser}.
*
* @author Jens Schauder
+ * @author Diego Krupitza
*/
class ParameterBindingParserUnitTests {
@@ -65,7 +66,7 @@ class ParameterBindingParserUnitTests {
private void checkHasParameter(SoftAssertions softly, String query, boolean containsParameter, String label) {
- StringQuery stringQuery = new StringQuery(query);
+ StringQuery stringQuery = new StringQuery(query, false);
softly.assertThat(stringQuery.getParameterBindings().size()) //
.describedAs(String.format("<%s> (%s)", query, label)) //
diff --git a/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java
new file mode 100644
index 000000000..1eb04239e
--- /dev/null
+++ b/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java
@@ -0,0 +1,46 @@
+/*
+ * 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.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for {@link QueryEnhancerFactory}.
+ *
+ * @author Diego Krupitza
+ */
+class QueryEnhancerFactoryUnitTests {
+
+ @Test
+ void createsDefaultImplementationForNonNativeQuery() {
+ StringQuery query = new StringQuery("Select new User(u.firstname) from User u", false);
+
+ QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query);
+ assertThat(queryEnhancer) //
+ .isInstanceOf(DefaultQueryEnhancer.class);
+ }
+
+ @Test
+ void createsJSqlImplementationForNativeQuery() {
+ StringQuery query = new StringQuery("Select * from User", true);
+
+ QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query);
+ assertThat(queryEnhancer) //
+ .isInstanceOf(JSqlParserQueryEnhancer.class);
+ }
+}
diff --git a/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerUnitTests.java
new file mode 100644
index 000000000..7ba11d74b
--- /dev/null
+++ b/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerUnitTests.java
@@ -0,0 +1,707 @@
+/*
+ * 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.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.springframework.dao.InvalidDataAccessApiUsageException;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.jpa.domain.JpaSort;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.*;
+
+/**
+ * Unit test for {@link QueryEnhancer}.
+ *
+ * @author Diego Krupitza
+ */
+class QueryEnhancerUnitTests {
+
+ private static final String QUERY = "select u from User u";
+ private static final String FQ_QUERY = "select u from org.acme.domain.User$Foo_Bar u";
+ private static final String SIMPLE_QUERY = "from User u";
+ private static final String COUNT_QUERY = "select count(u) from User u";
+
+ private static final String QUERY_WITH_AS = "select u from User as u where u.username = ?";
+
+ @Test
+ void createsCountQueryCorrectly() {
+ assertCountQuery(QUERY, COUNT_QUERY, true);
+ }
+
+ @Test
+ void createsCountQueriesCorrectlyForCapitalLetterJPQL() {
+
+ assertCountQuery("FROM User u WHERE u.foo.bar = ?", "select count(u) FROM User u WHERE u.foo.bar = ?", false);
+
+ assertCountQuery("SELECT u FROM User u where u.foo.bar = ?", "select count(u) FROM User u where u.foo.bar = ?",
+ true);
+ }
+
+ @Test
+ void createsCountQueryForDistinctQueries() {
+
+ assertCountQuery("select distinct u from User u where u.foo = ?",
+ "select count(distinct u) from User u where u.foo = ?", true);
+ }
+
+ @Test
+ void createsCountQueryForConstructorQueries() {
+
+ assertCountQuery("select distinct new User(u.name) from User u where u.foo = ?",
+ "select count(distinct u) from User u where u.foo = ?", false);
+ }
+
+ @Test
+ void createsCountQueryForJoinsNoneNative() {
+
+ assertCountQuery("select distinct new User(u.name) from User u left outer join u.roles r WHERE r = ?",
+ "select count(distinct u) from User u left outer join u.roles r WHERE r = ?", false);
+ }
+
+ @Test
+ void createsCountQueryForJoinsNative() {
+
+ assertCountQuery("select distinct u.name from User u left outer join u.roles r WHERE r = ?",
+ "select count(distinct u.name) from User u left outer join u.roles r WHERE r = ?", true);
+ }
+
+ @Test
+ void createsCountQueryForQueriesWithSubSelects() {
+
+ assertCountQuery("select u from User u left outer join u.roles r where r in (select r from Role)",
+ "select count(u) from User u left outer join u.roles r where r in (select r from Role)", true);
+ }
+
+ @Test
+ void createsCountQueryForAliasesCorrectly() {
+
+ assertCountQuery("select u from User as u", "select count(u) from User as u", true);
+ }
+
+ @Test
+ void allowsShortJpaSyntax() {
+
+ assertCountQuery(SIMPLE_QUERY, COUNT_QUERY, false);
+ }
+
+ @ParameterizedTest
+ @MethodSource("detectsAliasWithUCorrectlySource")
+ void detectsAliasWithUCorrectly(DeclaredQuery query, String alias) {
+ assertThat(getEnhancer(query).detectAlias()).isEqualTo(alias);
+ }
+
+ public static Stream detectsAliasWithUCorrectlySource() {
+ return Stream.of( //
+ Arguments.of(new StringQuery(QUERY, true), "u"), //
+ Arguments.of(new StringQuery(SIMPLE_QUERY, false), "u"), //
+ Arguments.of(new StringQuery(COUNT_QUERY, true), "u"), //
+ Arguments.of(new StringQuery(QUERY_WITH_AS, true), "u"), //
+ Arguments.of(new StringQuery("SELECT FROM USER U", false), "U"), //
+ Arguments.of(new StringQuery("select u from User u", true), "u"), //
+ Arguments.of(new StringQuery("select u from com.acme.User u", true), "u"), //
+ Arguments.of(new StringQuery("select u from T05User u", true), "u") //
+ );
+ }
+
+ @Test
+ void allowsFullyQualifiedEntityNamesInQuery() {
+
+ StringQuery query = new StringQuery(FQ_QUERY, true);
+ assertThat(getEnhancer(query).detectAlias()).isEqualTo("u");
+ assertCountQuery(FQ_QUERY, "select count(u) from org.acme.domain.User$Foo_Bar u", true);
+ }
+
+ @Test // DATAJPA-252
+ void doesNotPrefixOrderReferenceIfOuterJoinAliasDetected() {
+
+ StringQuery query = new StringQuery("select p from Person p left join p.address address", true);
+ endsIgnoringCase(getEnhancer(query).applySorting(Sort.by("address.city")), "order by address.city asc");
+ endsIgnoringCase(getEnhancer(query).applySorting(Sort.by("address.city", "lastname"), "p"),
+ "order by address.city asc, p.lastname asc");
+ }
+
+ @Test // DATAJPA-252
+ void extendsExistingOrderByClausesCorrectly() {
+
+ StringQuery query = new StringQuery("select p from Person p order by p.lastname asc", true);
+ endsIgnoringCase(getEnhancer(query).applySorting(Sort.by("firstname"), "p"),
+ "order by p.lastname asc, p.firstname asc");
+ }
+
+ @Test // DATAJPA-296
+ void appliesIgnoreCaseOrderingCorrectly() {
+
+ Sort sort = Sort.by(Sort.Order.by("firstname").ignoreCase());
+
+ StringQuery query = new StringQuery("select p from Person p", true);
+ endsIgnoringCase(getEnhancer(query).applySorting(sort, "p"), "order by lower(p.firstname) asc");
+ }
+
+ @Test // DATAJPA-296
+ void appendsIgnoreCaseOrderingCorrectly() {
+
+ Sort sort = Sort.by(Sort.Order.by("firstname").ignoreCase());
+
+ StringQuery query = new StringQuery("select p from Person p order by p.lastname asc", true);
+ endsIgnoringCase(getEnhancer(query).applySorting(sort, "p"), "order by p.lastname asc, lower(p.firstname) asc");
+ }
+
+ @Test // DATAJPA-342
+ void usesReturnedVariableInCOuntProjectionIfSet() {
+
+ assertCountQuery("select distinct m.genre from Media m where m.user = ?1 order by m.genre asc",
+ "select count(distinct m.genre) from Media m where m.user = ?1", true);
+ }
+
+ @Test // DATAJPA-343
+ void projectsCountQueriesForQueriesWithSubSelects() {
+
+ assertCountQuery("select o from Foo o where cb.id in (select b from Bar b)",
+ "select count(o) from Foo o where cb.id in (select b from Bar b)", true);
+ }
+
+ @Test // DATAJPA-148
+ void doesNotPrefixSortsIfFunction() {
+ StringQuery query = new StringQuery("select p from Person p", true);
+ Sort sort = Sort.by("sum(foo)");
+
+ QueryEnhancer enhancer = getEnhancer(query);
+
+ assertThatThrownBy(() -> enhancer.applySorting(sort, "p")) //
+ .isInstanceOf(InvalidDataAccessApiUsageException.class);
+ }
+
+ @Test // DATAJPA-377
+ void removesOrderByInGeneratedCountQueryFromOriginalQueryIfPresent() {
+
+ assertCountQuery("select distinct m.genre from Media m where m.user = ?1 OrDer By m.genre ASC",
+ "select count(distinct m.genre) from Media m where m.user = ?1", true);
+ }
+
+ @Test // DATAJPA-375
+ void findsExistingOrderByIndependentOfCase() {
+
+ Sort sort = Sort.by("lastname");
+ StringQuery originalQuery = new StringQuery("select p from Person p ORDER BY p.firstname", true);
+ String query = getEnhancer(originalQuery).applySorting(sort, "p");
+ endsIgnoringCase(query, "ORDER BY p.firstname, p.lastname asc");
+ }
+
+ @Test // DATAJPA-409
+ void createsCountQueryForNestedReferenceCorrectly() {
+ assertCountQuery("select a.b from A a", "select count(a.b) from A a", true);
+ }
+
+ @Test // DATAJPA-420
+ void createsCountQueryForScalarSelects() {
+ assertCountQuery("select p.lastname,p.firstname from Person p", "select count(p) from Person p", true);
+ }
+
+ @Test // DATAJPA-456
+ void createCountQueryFromTheGivenCountProjection() {
+ StringQuery query = new StringQuery("select p.lastname,p.firstname from Person p", true);
+ assertThat(getEnhancer(query).createCountQueryFor("p.lastname"))
+ .isEqualToIgnoringCase("select count(p.lastname) from Person p");
+ }
+
+ @Test // DATAJPA-726
+ void detectsAliassesInPlainJoins() {
+
+ StringQuery query = new StringQuery("select p from Customer c join c.productOrder p where p.delaye = true", true);
+ Sort sort = Sort.by("p.lineItems");
+
+ endsIgnoringCase(getEnhancer(query).applySorting(sort, "c"), "order by p.lineItems asc");
+ }
+
+ @Test // DATAJPA-736
+ void supportsNonAsciiCharactersInEntityNames() {
+ StringQuery query = new StringQuery("select u from Usèr u", true);
+ assertThat(getEnhancer(query).createCountQueryFor()).isEqualToIgnoringCase("select count(u) from Usèr u");
+ }
+
+ @Test // DATAJPA-798
+ void detectsAliasInQueryContainingLineBreaks() {
+ StringQuery query = new StringQuery("select \n u \n from \n User \nu", true);
+ assertThat(getEnhancer(query).detectAlias()).isEqualTo("u");
+ }
+
+ @Test // DATAJPA-815
+ void doesPrefixPropertyWithNonNative() {
+
+ StringQuery query = new StringQuery("from Cat c join Dog d", false);
+ Sort sort = Sort.by("dPropertyStartingWithJoinAlias");
+
+ assertThat(getEnhancer(query).applySorting(sort, "c")).endsWith("order by c.dPropertyStartingWithJoinAlias asc");
+ }
+
+ @Test // DATAJPA-815
+ void doesPrefixPropertyWithNative() {
+ StringQuery query = new StringQuery("Select * from Cat c join Dog d", true);
+ Sort sort = Sort.by("dPropertyStartingWithJoinAlias");
+
+ endsIgnoringCase(getEnhancer(query).applySorting(sort, "c"), "order by c.dPropertyStartingWithJoinAlias asc");
+ }
+
+ @Test // DATAJPA-938
+ void detectsConstructorExpressionInDistinctQuery() {
+ StringQuery query = new StringQuery("select distinct new Foo() from Bar b", false);
+ assertThat(getEnhancer(query).hasConstructorExpression()).isTrue();
+ }
+
+ @Test // DATAJPA-938
+ void detectsComplexConstructorExpression() {
+
+ StringQuery query = new StringQuery("select new foo.bar.Foo(ip.id, ip.name, sum(lp.amount)) " //
+ + "from Bar lp join lp.investmentProduct ip " //
+ + "where (lp.toDate is null and lp.fromDate <= :now and lp.fromDate is not null) and lp.accountId = :accountId "
+ //
+ + "group by ip.id, ip.name, lp.accountId " //
+ + "order by ip.name ASC", false);
+
+ assertThat(getEnhancer(query).hasConstructorExpression()).isTrue();
+ }
+
+ @Test // DATAJPA-938
+ void detectsConstructorExpressionWithLineBreaks() {
+ StringQuery query = new StringQuery("select new foo.bar.FooBar(\na.id) from DtoA a ", false);
+ assertThat(getEnhancer(query).hasConstructorExpression()).isTrue();
+ }
+
+ @Test // DATAJPA-960
+ void doesNotQualifySortIfNoAliasDetectedNonNative() {
+ StringQuery query = new StringQuery("from mytable where ?1 is null", false);
+ assertThat(getEnhancer(query).applySorting(Sort.by("firstname"))).endsWith("order by firstname asc");
+ }
+
+ @Test // DATAJPA-960
+ void doesNotQualifySortIfNoAliasDetectedNative() {
+ StringQuery query = new StringQuery("Select * from mytable where ?1 is null", true);
+ endsIgnoringCase(getEnhancer(query).applySorting(Sort.by("firstname")), "order by firstname asc");
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void doesNotAllowWhitespaceInSort() {
+
+ StringQuery query = new StringQuery("select p from Person p", true);
+
+ Sort sort = Sort.by("case when foo then bar");
+ assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
+ .isThrownBy(() -> getEnhancer(query).applySorting(sort, "p"));
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void doesNotPrefixUnsageJpaSortFunctionCalls() {
+
+ JpaSort sort = JpaSort.unsafe("sum(foo)");
+ StringQuery query = new StringQuery("select p from Person p", true);
+ endsIgnoringCase(getEnhancer(query).applySorting(sort, "p"), "order by sum(foo) asc");
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void doesNotPrefixMultipleAliasedFunctionCalls() {
+
+ StringQuery query = new StringQuery("SELECT AVG(m.price) AS avgPrice, SUM(m.stocks) AS sumStocks FROM Magazine m",
+ true);
+ Sort sort = Sort.by("avgPrice", "sumStocks");
+
+ endsIgnoringCase(getEnhancer(query).applySorting(sort, "m"), "order by avgPrice asc, sumStocks asc");
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void doesNotPrefixSingleAliasedFunctionCalls() {
+
+ StringQuery query = new StringQuery("SELECT AVG(m.price) AS avgPrice FROM Magazine m", true);
+ Sort sort = Sort.by("avgPrice");
+
+ endsIgnoringCase(getEnhancer(query).applySorting(sort, "m"), "order by avgPrice asc");
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void prefixesSingleNonAliasedFunctionCallRelatedSortProperty() {
+
+ StringQuery query = new StringQuery("SELECT AVG(m.price) AS avgPrice FROM Magazine m", true);
+ Sort sort = Sort.by("someOtherProperty");
+
+ endsIgnoringCase(getEnhancer(query).applySorting(sort, "m"), "order by m.someOtherProperty asc");
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void prefixesNonAliasedFunctionCallRelatedSortPropertyWhenSelectClauseContainesAliasedFunctionForDifferentProperty() {
+
+ StringQuery query = new StringQuery("SELECT m.name, AVG(m.price) AS avgPrice FROM Magazine m", true);
+ Sort sort = Sort.by("name", "avgPrice");
+
+ endsIgnoringCase(getEnhancer(query).applySorting(sort, "m"), "order by m.name asc, avgPrice asc");
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void doesNotPrefixAliasedFunctionCallNameWithMultipleNumericParameters() {
+
+ StringQuery query = new StringQuery("SELECT SUBSTRING(m.name, 2, 5) AS trimmedName FROM Magazine m", true);
+ Sort sort = Sort.by("trimmedName");
+
+ endsIgnoringCase(getEnhancer(query).applySorting(sort, "m"), "order by trimmedName asc");
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void doesNotPrefixAliasedFunctionCallNameWithMultipleStringParameters() {
+
+ StringQuery query = new StringQuery("SELECT CONCAT(m.name, 'foo') AS extendedName FROM Magazine m", true);
+ Sort sort = Sort.by("extendedName");
+
+ endsIgnoringCase(getEnhancer(query).applySorting(sort, "m"), "order by extendedName asc");
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void doesNotPrefixAliasedFunctionCallNameWithUnderscores() {
+
+ StringQuery query = new StringQuery("SELECT AVG(m.price) AS avg_price FROM Magazine m", true);
+ Sort sort = Sort.by("avg_price");
+
+ endsIgnoringCase(getEnhancer(query).applySorting(sort, "m"), "order by avg_price asc");
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void doesNotPrefixAliasedFunctionCallNameWithDots() {
+
+ StringQuery query = new StringQuery("SELECT AVG(m.price) AS m.avg FROM Magazine m", false);
+ Sort sort = Sort.by("m.avg");
+
+ assertThat(getEnhancer(query).applySorting(sort, "m")).endsWith("order by m.avg asc");
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void doesNotPrefixAliasedFunctionCallNameWithDotsNativeQuery() {
+
+ // this is invalid since the '.' character is not allowed. Not in sql nor in JPQL.
+ assertThatThrownBy(() -> new StringQuery("SELECT AVG(m.price) AS m.avg FROM Magazine m", true)) //
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test // DATAJPA-965, DATAJPA-970
+ void doesNotPrefixAliasedFunctionCallNameWhenQueryStringContainsMultipleWhiteSpaces() {
+
+ StringQuery query = new StringQuery("SELECT AVG( m.price ) AS avgPrice FROM Magazine m", true);
+ Sort sort = Sort.by("avgPrice");
+
+ endsIgnoringCase(getEnhancer(query).applySorting(sort, "m"), "order by avgPrice asc");
+ }
+
+ @Test // DATAJPA-1000
+ void discoversCorrectAliasForJoinFetch() {
+
+ String queryString = "SELECT DISTINCT user FROM User user LEFT JOIN user.authorities AS authority";
+ Set aliases = QueryUtils.getOuterJoinAliases(queryString);
+
+ StringQuery nativeQuery = new StringQuery(queryString, true);
+ Set joinAliases = new JSqlParserQueryEnhancer(nativeQuery).getJoinAliases();
+
+ assertThat(aliases).containsExactly("authority");
+ assertThat(joinAliases).containsExactly("authority");
+ }
+
+ @Test // DATAJPA-1171
+ void doesNotContainStaticClauseInExistsQuery() {
+ endsIgnoringCase(QueryUtils.getExistsQueryString("entity", "x", Collections.singleton("id")), "WHERE x.id = :id");
+ }
+
+ @Test // DATAJPA-1363
+ void discoversAliasWithComplexFunction() {
+
+ assertThat(
+ QueryUtils.getFunctionAliases("select new MyDto(sum(case when myEntity.prop3=0 then 1 else 0 end) as myAlias")) //
+ .contains("myAlias");
+ }
+
+ @Test // DATAJPA-1506
+ void detectsAliasWithGroupAndOrderBy() {
+
+ StringQuery queryWithGroupNoAlias = new StringQuery("select * from User group by name", true);
+ StringQuery queryWithGroupAlias = new StringQuery("select * from User u group by name", true);
+
+ StringQuery queryWithOrderNoAlias = new StringQuery("select * from User order by name", true);
+ StringQuery queryWithOrderAlias = new StringQuery("select * from User u order by name", true);
+
+ assertThat(getEnhancer(queryWithGroupNoAlias).detectAlias()).isNull();
+ assertThat(getEnhancer(queryWithOrderNoAlias).detectAlias()).isNull();
+ assertThat(getEnhancer(queryWithGroupAlias).detectAlias()).isEqualTo("u");
+ assertThat(getEnhancer(queryWithOrderAlias).detectAlias()).isEqualTo("u");
+ }
+
+ @Test // DATAJPA-1500
+ void createCountQuerySupportsWhitespaceCharacters() {
+
+ StringQuery query = new StringQuery("select * from User user\n" + //
+ " where user.age = 18\n" + //
+ " order by user.name\n ", true);
+
+ assertThat(getEnhancer(query).createCountQueryFor())
+ .isEqualToIgnoringCase("select count(user) from User user where user.age = 18");
+ }
+
+ @Test
+ void createCountQuerySupportsLineBreaksInSelectClause() {
+ StringQuery query = new StringQuery("select user.age,\n" + //
+ " user.name\n" + //
+ " from User user\n" + //
+ " where user.age = 18\n" + //
+ " order\nby\nuser.name\n ", true);
+ assertThat(getEnhancer(query).createCountQueryFor())
+ .isEqualToIgnoringCase("select count(user) from User user where user.age = 18");
+ }
+
+ @Test // DATAJPA-1061
+ void appliesSortCorrectlyForFieldAliases() {
+
+ StringQuery query = new StringQuery(
+ "SELECT m.price, lower(m.title) AS title, a.name as authorName FROM Magazine m INNER JOIN m.author a",
+ true);
+ Sort sort = Sort.by("authorName");
+
+ String fullQuery = getEnhancer(query).applySorting(sort);
+
+ endsIgnoringCase(fullQuery, "order by authorName asc");
+ }
+
+ @Test // GH-2280
+ void appliesOrderingCorrectlyForFieldAliasWithIgnoreCase() {
+
+ StringQuery query = new StringQuery("SELECT customer.id as id, customer.name as name FROM CustomerEntity customer",
+ true);
+ Sort sort = Sort.by(Sort.Order.by("name").ignoreCase());
+
+ String fullQuery = getEnhancer(query).applySorting(sort);
+
+ assertThat(fullQuery).isEqualToIgnoringCase(
+ "SELECT customer.id as id, customer.name as name FROM CustomerEntity customer order by lower(name) asc");
+ }
+
+ @Test // DATAJPA-1061
+ void appliesSortCorrectlyForFunctionAliases() {
+
+ StringQuery query = new StringQuery(
+ "SELECT m.price, lower(m.title) AS title, a.name as authorName FROM Magazine m INNER JOIN m.author a",
+ true);
+ Sort sort = Sort.by("title");
+
+ String fullQuery = getEnhancer(query).applySorting(sort);
+
+ endsIgnoringCase(fullQuery, "order by title asc");
+ }
+
+ @Test // DATAJPA-1061
+ void appliesSortCorrectlyForSimpleField() {
+
+ StringQuery query = new StringQuery(
+ "SELECT m.price, lower(m.title) AS title, a.name as authorName FROM Magazine m INNER JOIN m.author a",
+ true);
+ Sort sort = Sort.by("price");
+
+ String fullQuery = getEnhancer(query).applySorting(sort);
+
+ endsIgnoringCase(fullQuery, "order by m.price asc");
+ }
+
+ @Test
+ void createCountQuerySupportsLineBreakRightAfterDistinct() {
+
+ StringQuery query1 = new StringQuery("select\ndistinct\nuser.age,\n" + //
+ "user.name\n" + //
+ "from\nUser\nuser", true);
+
+ StringQuery query2 = new StringQuery("select\ndistinct user.age,\n" + //
+ "user.name\n" + //
+ "from\nUser\nuser", true);
+
+ assertThat(getEnhancer(query1).createCountQueryFor()).isEqualTo(getEnhancer(query2).createCountQueryFor());
+ }
+
+ @Test
+ void detectsAliasWithGroupAndOrderByWithLineBreaks() {
+
+ StringQuery queryWithGroupAndLineBreak = new StringQuery("select * from User group\nby name", true);
+ StringQuery queryWithGroupAndLineBreakAndAlias = new StringQuery("select * from User u group\nby name", true);
+
+ assertThat(getEnhancer(queryWithGroupAndLineBreak).detectAlias()).isNull();
+ assertThat(getEnhancer(queryWithGroupAndLineBreakAndAlias).detectAlias()).isEqualTo("u");
+
+ StringQuery queryWithOrderAndLineBreak = new StringQuery("select * from User order\nby name", true);
+ StringQuery queryWithOrderAndLineBreakAndAlias = new StringQuery("select * from User u order\nby name", true);
+ StringQuery queryWithOrderAndMultipleLineBreakAndAlias = new StringQuery("select * from User\nu\norder \n by name",
+ true);
+
+ assertThat(getEnhancer(queryWithOrderAndLineBreak).detectAlias()).isNull();
+ assertThat(getEnhancer(queryWithOrderAndLineBreakAndAlias).detectAlias()).isEqualTo("u");
+ assertThat(getEnhancer(queryWithOrderAndMultipleLineBreakAndAlias).detectAlias()).isEqualTo("u");
+ }
+
+ @ParameterizedTest // DATAJPA-1679
+ @MethodSource("findProjectionClauseWithDistinctSource")
+ void findProjectionClauseWithDistinct(DeclaredQuery query, String expected) {
+
+ SoftAssertions.assertSoftly(sofly -> {
+ sofly.assertThat(getEnhancer(query).getProjection()).isEqualTo(expected);
+ });
+ }
+
+ public static Stream findProjectionClauseWithDistinctSource() {
+ return Stream.of( //
+ Arguments.of(new StringQuery("select * from x", true), "*"), //
+ Arguments.of(new StringQuery("select a, b, c from x", true), "a, b, c"), //
+ Arguments.of(new StringQuery("select distinct a, b, c from x", true), "a, b, c"), //
+ Arguments.of(new StringQuery("select DISTINCT a, b, c from x", true), "a, b, c") //
+ );
+ }
+
+ @Test // DATAJPA-1696
+ void findProjectionClauseWithSubselect() {
+
+ // This is not a required behavior, in fact the opposite is,
+ // but it documents a current limitation.
+ // to fix this without breaking findProjectionClauseWithIncludedFrom we need a more sophisticated parser.
+ assertThat(QueryUtils.getProjection("select * from (select x from y)")).isNotEqualTo("*");
+ }
+
+ @Test // DATAJPA-1696
+ void findProjectionClauseWithSubselectNative() {
+
+ // This is a required behavior the testcase in #findProjectionClauseWithSubselect tells why
+ String queryString = "select * from (select x from y)";
+ StringQuery query = new StringQuery(queryString, true);
+ assertThat(getEnhancer(query).getProjection()).isEqualTo("*");
+ }
+
+ @Test // DATAJPA-1696
+ void findProjectionClauseWithIncludedFrom() {
+ StringQuery query = new StringQuery("select x, frommage, y from t", true);
+ assertThat(getEnhancer(query).getProjection()).isEqualTo("x, frommage, y");
+ }
+
+ @Test
+ void countProjectionDistrinctQueryIncludesNewLineAfterFromAndBeforeJoin() {
+ StringQuery originalQuery = new StringQuery(
+ "SELECT DISTINCT entity1\nFROM Entity1 entity1\nLEFT JOIN Entity2 entity2 ON entity1.key = entity2.key", true);
+
+ assertCountQuery(originalQuery,
+ "select count(DISTINCT entity1) FROM Entity1 entity1 LEFT JOIN Entity2 entity2 ON entity1.key = entity2.key");
+ }
+
+ @Test
+ void countProjectionDistinctQueryIncludesNewLineAfterEntity() {
+ StringQuery originalQuery = new StringQuery(
+ "SELECT DISTINCT entity1\nFROM Entity1 entity1 LEFT JOIN Entity2 entity2 ON entity1.key = entity2.key", true);
+ assertCountQuery(originalQuery,
+ "select count(DISTINCT entity1) FROM Entity1 entity1 LEFT JOIN Entity2 entity2 ON entity1.key = entity2.key");
+ }
+
+ @Test
+ void countProjectionDistinctQueryIncludesNewLineAfterEntityAndBeforeWhere() {
+ StringQuery originalQuery = new StringQuery(
+ "SELECT DISTINCT entity1\nFROM Entity1 entity1 LEFT JOIN Entity2 entity2 ON entity1.key = entity2.key\nwhere entity1.id = 1799",
+ true);
+ assertCountQuery(originalQuery,
+ "select count(DISTINCT entity1) FROM Entity1 entity1 LEFT JOIN Entity2 entity2 ON entity1.key = entity2.key where entity1.id = 1799");
+ }
+
+ @Test
+ void createsCountQueriesCorrectlyForCapitalLetter() {
+ assertCountQuery("SELECT u FROM User u where u.foo.bar = ?", "select count(u) FROM User u where u.foo.bar = ?",
+ true);
+ }
+
+ @ParameterizedTest // DATAJPA-252
+ @MethodSource("detectsJoinAliasesCorrectlySource")
+ void detectsJoinAliasesCorrectly(String queryString, List aliases) {
+
+ StringQuery nativeQuery = new StringQuery(queryString, true);
+ StringQuery nonNativeQuery = new StringQuery(queryString, false);
+
+ Set nativeJoinAliases = getEnhancer(nativeQuery).getJoinAliases();
+ Set nonNativeJoinAliases = getEnhancer(nonNativeQuery).getJoinAliases();
+
+ assertThat(nonNativeJoinAliases).containsAll(nativeJoinAliases);
+ assertThat(nativeJoinAliases) //
+ .hasSize(aliases.size()) //
+ .containsAll(aliases);
+
+ }
+
+ @Test // GH-2441
+ void correctFunctionAliasWithComplexNestedFunctions() {
+ String queryString = "\nSELECT \nCAST(('{' || string_agg(distinct array_to_string(c.institutes_ids, ','), ',') || '}') AS bigint[]) as institutesIds\nFROM\ncity c";
+ StringQuery nativeQuery = new StringQuery(queryString, true);
+
+ JSqlParserQueryEnhancer queryEnhancer = (JSqlParserQueryEnhancer) getEnhancer(nativeQuery);
+
+ assertThat(queryEnhancer.getSelectionAliases()).contains("institutesIds");
+ }
+
+ @Test // GH-2441
+ void correctApplySortOnComplexNestedFunctionQuery() {
+ String queryString = "SELECT dd.institutesIds FROM (\n" + " SELECT\n"
+ + " CAST(('{' || string_agg(distinct array_to_string(c.institutes_ids, ','), ',') || '}') AS bigint[]) as institutesIds\n"
+ + " FROM\n" + " city c\n"
+ + " ) dd";
+
+ StringQuery nativeQuery = new StringQuery(queryString, true);
+
+ QueryEnhancer queryEnhancer = getEnhancer(nativeQuery);
+
+ String result = queryEnhancer.applySorting(Sort.by(new Sort.Order(Sort.Direction.ASC, "institutesIds")));
+ assertThat(result).containsIgnoringCase("order by dd.institutesIds");
+ }
+
+ public static Stream detectsJoinAliasesCorrectlySource() {
+ return Stream.of( //
+ Arguments.of("select p from Person p left outer join x.foo b2_$ar", Collections.singletonList("b2_$ar")), //
+ Arguments.of("select p from Person p left join x.foo b2_$ar", Collections.singletonList("b2_$ar")), //
+ Arguments.of("select p from Person p left outer join x.foo as b2_$ar, left join x.bar as foo",
+ Arrays.asList("b2_$ar", "foo")), //
+ Arguments.of("select p from Person p left join x.foo as b2_$ar, left outer join x.bar foo",
+ Arrays.asList("b2_$ar", "foo")) //
+
+ );
+ }
+
+ private static void assertCountQuery(String originalQuery, String countQuery, boolean nativeQuery) {
+ assertCountQuery(new StringQuery(originalQuery, nativeQuery), countQuery);
+ }
+
+ private static void assertCountQuery(StringQuery originalQuery, String countQuery) {
+ assertThat(getEnhancer(originalQuery).createCountQueryFor()).isEqualToIgnoringCase(countQuery);
+ }
+
+ private static void endsIgnoringCase(String original, String endWithIgnoreCase) {
+ // https://github.com/assertj/assertj-core/pull/2451
+ // can be removed when upgrading to version 3.23.0 assertJ
+ assertThat(original.toUpperCase()).endsWith(endWithIgnoreCase.toUpperCase());
+ }
+
+ private static QueryEnhancer getEnhancer(DeclaredQuery query) {
+ return QueryEnhancerFactory.forQuery(query);
+ }
+
+}
diff --git a/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java
index 68e38e492..a0081a413 100644
--- a/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java
+++ b/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java
@@ -53,14 +53,14 @@ class QueryParameterSetterFactoryUnitTests {
@Test // DATAJPA-1058
void noExceptionWhenQueryDoesNotContainNamedParameters() {
- setterFactory.create(binding, DeclaredQuery.of("QueryStringWithOutNamedParameter"));
+ setterFactory.create(binding, DeclaredQuery.of("QueryStringWithOutNamedParameter", false));
}
@Test // DATAJPA-1058
void exceptionWhenQueryContainNamedParametersAndMethodParametersAreNotNamed() {
assertThatExceptionOfType(IllegalStateException.class) //
- .isThrownBy(() -> setterFactory.create(binding, DeclaredQuery.of("QueryStringWith :NamedParameter"))) //
+ .isThrownBy(() -> setterFactory.create(binding, DeclaredQuery.of("QueryStringWith :NamedParameter", false))) //
.withMessageContaining("Java 8") //
.withMessageContaining("@Param") //
.withMessageContaining("-parameters");
@@ -77,7 +77,7 @@ class QueryParameterSetterFactoryUnitTests {
when(binding.getRequiredPosition()).thenReturn(1);
assertThatExceptionOfType(IllegalArgumentException.class) //
- .isThrownBy(() -> setterFactory.create(binding, DeclaredQuery.of("QueryStringWith :NamedParameter"))) //
+ .isThrownBy(() -> setterFactory.create(binding, DeclaredQuery.of("QueryStringWith :NamedParameter", false))) //
.withMessage("At least 1 parameter(s) provided but only 0 parameter(s) present in query.");
}
@@ -91,7 +91,7 @@ class QueryParameterSetterFactoryUnitTests {
when(binding.getRequiredPosition()).thenReturn(1);
assertThatExceptionOfType(IllegalArgumentException.class) //
- .isThrownBy(() -> setterFactory.create(binding, DeclaredQuery.of("QueryStringWith ?1"))) //
+ .isThrownBy(() -> setterFactory.create(binding, DeclaredQuery.of("QueryStringWith ?1", false))) //
.withMessage("At least 1 parameter(s) provided but only 0 parameter(s) present in query.");
}
}
diff --git a/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java b/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java
index 47f1a1750..367bdd2b1 100644
--- a/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java
+++ b/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java
@@ -71,7 +71,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
@ContextConfiguration("classpath:infrastructure.xml")
public class QueryUtilsIntegrationTests {
- @PersistenceContext EntityManager em;
+ @PersistenceContext
+ EntityManager em;
@Test // DATAJPA-403
void reusesExistingJoinForExpression() {
@@ -139,8 +140,8 @@ public class QueryUtilsIntegrationTests {
CriteriaQuery query = builder.createQuery(InvoiceItem.class);
Root root = query.from(InvoiceItem.class);
- QueryUtils
- .toExpressionRecursively(root, PropertyPath.from("invoice.order.customer.name", InvoiceItem.class), false);
+ QueryUtils.toExpressionRecursively(root, PropertyPath.from("invoice.order.customer.name", InvoiceItem.class),
+ false);
assertThat(getInnerJoins(root)).hasSize(1); // join invoice
Join, ?> rootInnerJoin = getInnerJoins(root).iterator().next();
@@ -162,8 +163,8 @@ public class QueryUtilsIntegrationTests {
root.join("invoice", JoinType.LEFT).join("order", JoinType.LEFT);
// when navigating through a path with nested optionals
- QueryUtils
- .toExpressionRecursively(root, PropertyPath.from("invoice.order.customer.name", InvoiceItem.class), false);
+ QueryUtils.toExpressionRecursively(root, PropertyPath.from("invoice.order.customer.name", InvoiceItem.class),
+ false);
// assert that existing joins are reused and no additional joins are created
assertThat(getInnerJoins(root)).isEmpty(); // no inner join invoice
@@ -185,8 +186,8 @@ public class QueryUtilsIntegrationTests {
// given an existing inner join an nested optional
root.join("invoice").join("order");
- QueryUtils
- .toExpressionRecursively(root, PropertyPath.from("invoice.order.customer.name", InvoiceItem.class), false);
+ QueryUtils.toExpressionRecursively(root, PropertyPath.from("invoice.order.customer.name", InvoiceItem.class),
+ false);
// assert that no useless left joins are created
assertThat(getInnerJoins(root)).hasSize(1); // join invoice
@@ -347,32 +348,40 @@ public class QueryUtilsIntegrationTests {
@SuppressWarnings("unused")
static class Merchant {
- @Id String id;
- @OneToMany Set employees;
+ @Id
+ String id;
+ @OneToMany
+ Set employees;
- @OneToOne Address address;
+ @OneToOne
+ Address address;
}
@Entity
@SuppressWarnings("unused")
static class Address {
- @Id String id;
- @OneToOne(mappedBy = "address") Merchant merchant;
+ @Id
+ String id;
+ @OneToOne(mappedBy = "address")
+ Merchant merchant;
}
@Entity
@SuppressWarnings("unused")
static class Employee {
- @Id String id;
- @OneToMany Set credentials;
+ @Id
+ String id;
+ @OneToMany
+ Set credentials;
}
@Entity
@SuppressWarnings("unused")
static class Credential {
- @Id String id;
+ @Id
+ String id;
String uid;
}
@@ -390,7 +399,8 @@ public class QueryUtilsIntegrationTests {
}
@Override
- public void clearCachedProviders() {}
+ public void clearCachedProviders() {
+ }
}
}
diff --git a/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsUnitTests.java
index b2640fea2..cf7cb8614 100644
--- a/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsUnitTests.java
+++ b/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsUnitTests.java
@@ -51,7 +51,7 @@ class QueryUtilsUnitTests {
private static final String QUERY_WITH_AS = "select u from User as u where u.username = ?";
@Test
- void createsCountQueryCorrectly() {
+ void createsCountQueryCorrectly() throws Exception {
assertCountQuery(QUERY, COUNT_QUERY);
}
@@ -64,47 +64,47 @@ class QueryUtilsUnitTests {
}
@Test
- void createsCountQueryForDistinctQueries() {
+ void createsCountQueryForDistinctQueries() throws Exception {
assertCountQuery("select distinct u from User u where u.foo = ?",
"select count(distinct u) from User u where u.foo = ?");
}
@Test
- void createsCountQueryForConstructorQueries() {
+ void createsCountQueryForConstructorQueries() throws Exception {
assertCountQuery("select distinct new User(u.name) from User u where u.foo = ?",
"select count(distinct u) from User u where u.foo = ?");
}
@Test
- void createsCountQueryForJoins() {
+ void createsCountQueryForJoins() throws Exception {
assertCountQuery("select distinct new User(u.name) from User u left outer join u.roles r WHERE r = ?",
"select count(distinct u) from User u left outer join u.roles r WHERE r = ?");
}
@Test
- void createsCountQueryForQueriesWithSubSelects() {
+ void createsCountQueryForQueriesWithSubSelects() throws Exception {
assertCountQuery("select u from User u left outer join u.roles r where r in (select r from Role)",
"select count(u) from User u left outer join u.roles r where r in (select r from Role)");
}
@Test
- void createsCountQueryForAliasesCorrectly() {
+ void createsCountQueryForAliasesCorrectly() throws Exception {
assertCountQuery("select u from User as u", "select count(u) from User as u");
}
@Test
- void allowsShortJpaSyntax() {
+ void allowsShortJpaSyntax() throws Exception {
assertCountQuery(SIMPLE_QUERY, COUNT_QUERY);
}
@Test
- void detectsAliasCorrectly() {
+ void detectsAliasCorrectly() throws Exception {
assertThat(detectAlias(QUERY)).isEqualTo("u");
assertThat(detectAlias(SIMPLE_QUERY)).isEqualTo("u");
@@ -400,8 +400,8 @@ class QueryUtilsUnitTests {
@Test // DATAJPA-1363
void discoversAliasWithComplexFunction() {
- assertThat(
- QueryUtils.getFunctionAliases("select new MyDto(sum(case when myEntity.prop3=0 then 1 else 0 end) as myAlias")) //
+ assertThat(QueryUtils
+ .getFunctionAliases("select new MyDto(sum(case when myEntity.prop3=0 then 1 else 0 end) as myAlias")) //
.contains("myAlias");
}
diff --git a/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java
index adec631c1..926047477 100644
--- a/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java
+++ b/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java
@@ -37,6 +37,7 @@ import org.springframework.data.repository.query.parser.Part.Type;
* @author Jens Schauder
* @author Nils Borrmann
* @author Andriy Redko
+ * @author Diego Krupitza
*/
class StringQueryUnitTests {
@@ -46,7 +47,7 @@ class StringQueryUnitTests {
void doesNotConsiderPlainLikeABinding() {
String source = "select from User u where u.firstname like :firstname";
- StringQuery query = new StringQuery(source);
+ StringQuery query = new StringQuery(source, false);
assertThat(query.hasParameterBindings()).isTrue();
assertThat(query.getQueryString()).isEqualTo(source);
@@ -63,7 +64,8 @@ class StringQueryUnitTests {
@Test // DATAJPA-292
void detectsPositionalLikeBindings() {
- StringQuery query = new StringQuery("select u from User u where u.firstname like %?1% or u.lastname like %?2");
+ StringQuery query = new StringQuery("select u from User u where u.firstname like %?1% or u.lastname like %?2",
+ true);
assertThat(query.hasParameterBindings()).isTrue();
assertThat(query.getQueryString())
@@ -86,7 +88,7 @@ class StringQueryUnitTests {
@Test // DATAJPA-292
void detectsNamedLikeBindings() {
- StringQuery query = new StringQuery("select u from User u where u.firstname like %:firstname");
+ StringQuery query = new StringQuery("select u from User u where u.firstname like %:firstname", true);
assertThat(query.hasParameterBindings()).isTrue();
assertThat(query.getQueryString()).isEqualTo("select u from User u where u.firstname like :firstname");
@@ -104,7 +106,7 @@ class StringQueryUnitTests {
void detectsNamedInParameterBindings() {
String queryString = "select u from User u where u.id in :ids";
- StringQuery query = new StringQuery(queryString);
+ StringQuery query = new StringQuery(queryString, true);
assertThat(query.hasParameterBindings()).isTrue();
assertThat(query.getQueryString()).isEqualTo(queryString);
@@ -121,7 +123,7 @@ class StringQueryUnitTests {
void detectsMultipleNamedInParameterBindings() {
String queryString = "select u from User u where u.id in :ids and u.name in :names and foo = :bar";
- StringQuery query = new StringQuery(queryString);
+ StringQuery query = new StringQuery(queryString, true);
assertThat(query.hasParameterBindings()).isTrue();
assertThat(query.getQueryString()).isEqualTo(queryString);
@@ -140,7 +142,7 @@ class StringQueryUnitTests {
void detectsPositionalInParameterBindings() {
String queryString = "select u from User u where u.id in ?1";
- StringQuery query = new StringQuery(queryString);
+ StringQuery query = new StringQuery(queryString, true);
assertThat(query.hasParameterBindings()).isTrue();
assertThat(query.getQueryString()).isEqualTo(queryString);
@@ -157,7 +159,7 @@ class StringQueryUnitTests {
void detectsMultiplePositionalInParameterBindings() {
String queryString = "select u from User u where u.id in ?1 and u.names in ?2 and foo = ?3";
- StringQuery query = new StringQuery(queryString);
+ StringQuery query = new StringQuery(queryString, true);
assertThat(query.hasParameterBindings()).isTrue();
assertThat(query.getQueryString()).isEqualTo(queryString);
@@ -174,19 +176,19 @@ class StringQueryUnitTests {
@Test // DATAJPA-373
void handlesMultipleNamedLikeBindingsCorrectly() {
- new StringQuery("select u from User u where u.firstname like %:firstname or foo like :bar");
+ new StringQuery("select u from User u where u.firstname like %:firstname or foo like :bar", true);
}
@Test // DATAJPA-292, DATAJPA-362
void rejectsDifferentBindingsForRepeatedParameter() {
- assertThatIllegalArgumentException()
- .isThrownBy(() -> new StringQuery("select u from User u where u.firstname like %?1 and u.lastname like ?1%"));
+ assertThatIllegalArgumentException().isThrownBy(
+ () -> new StringQuery("select u from User u where u.firstname like %?1 and u.lastname like ?1%", true));
}
@Test // DATAJPA-461
void treatsGreaterThanBindingAsSimpleBinding() {
- StringQuery query = new StringQuery("select u from User u where u.createdDate > ?1");
+ StringQuery query = new StringQuery("select u from User u where u.createdDate > ?1", true);
List bindings = query.getParameterBindings();
assertThat(bindings).hasSize(1);
@@ -199,7 +201,7 @@ class StringQueryUnitTests {
void removesLikeBindingsFromQueryIfQueryContainsSimpleBinding() {
StringQuery query = new StringQuery("SELECT a FROM Article a WHERE a.overview LIKE %:escapedWord% ESCAPE '~'"
- + " OR a.content LIKE %:escapedWord% ESCAPE '~' OR a.title = :word ORDER BY a.articleId DESC");
+ + " OR a.content LIKE %:escapedWord% ESCAPE '~' OR a.title = :word ORDER BY a.articleId DESC", true);
List bindings = query.getParameterBindings();
@@ -217,7 +219,7 @@ class StringQueryUnitTests {
@Test // DATAJPA-483
void detectsInBindingWithParentheses() {
- StringQuery query = new StringQuery("select count(we) from MyEntity we where we.status in (:statuses)");
+ StringQuery query = new StringQuery("select count(we) from MyEntity we where we.status in (:statuses)", true);
List bindings = query.getParameterBindings();
@@ -230,7 +232,7 @@ class StringQueryUnitTests {
@Test // DATAJPA-545
void detectsInBindingWithSpecialFrenchCharactersInParentheses() {
- StringQuery query = new StringQuery("select * from MyEntity where abonnés in (:abonnés)");
+ StringQuery query = new StringQuery("select * from MyEntity where abonnés in (:abonnés)", true);
List bindings = query.getParameterBindings();
@@ -243,7 +245,7 @@ class StringQueryUnitTests {
@Test // DATAJPA-545
void detectsInBindingWithSpecialCharactersInParentheses() {
- StringQuery query = new StringQuery("select * from MyEntity where øre in (:øre)");
+ StringQuery query = new StringQuery("select * from MyEntity where øre in (:øre)", true);
List bindings = query.getParameterBindings();
@@ -256,7 +258,7 @@ class StringQueryUnitTests {
@Test // DATAJPA-545
void detectsInBindingWithSpecialAsianCharactersInParentheses() {
- StringQuery query = new StringQuery("select * from MyEntity where 생일 in (:생일)");
+ StringQuery query = new StringQuery("select * from MyEntity where 생일 in (:생일)", true);
List bindings = query.getParameterBindings();
@@ -269,7 +271,7 @@ class StringQueryUnitTests {
@Test // DATAJPA-545
void detectsInBindingWithSpecialCharactersAndWordCharactersMixedInParentheses() {
- StringQuery query = new StringQuery("select * from MyEntity where foo in (:ab1babc생일233)");
+ StringQuery query = new StringQuery("select * from MyEntity where foo in (:ab1babc생일233)", true);
List bindings = query.getParameterBindings();
@@ -281,14 +283,14 @@ class StringQueryUnitTests {
@Test // DATAJPA-362
void rejectsDifferentBindingsForRepeatedParameter2() {
- assertThatIllegalArgumentException()
- .isThrownBy(() -> new StringQuery("select u from User u where u.firstname like ?1 and u.lastname like %?1"));
+ assertThatIllegalArgumentException().isThrownBy(
+ () -> new StringQuery("select u from User u where u.firstname like ?1 and u.lastname like %?1", true));
}
@Test // DATAJPA-712
void shouldReplaceAllNamedExpressionParametersWithInClause() {
- StringQuery query = new StringQuery("select a from A a where a.b in :#{#bs} and a.c in :#{#cs}");
+ StringQuery query = new StringQuery("select a from A a where a.b in :#{#bs} and a.c in :#{#cs}", true);
String queryString = query.getQueryString();
assertThat(queryString).isEqualTo("select a from A a where a.b in :__$synthetic$__1 and a.c in :__$synthetic$__2");
@@ -297,7 +299,7 @@ class StringQueryUnitTests {
@Test // DATAJPA-712
void shouldReplaceAllPositionExpressionParametersWithInClause() {
- StringQuery query = new StringQuery("select a from A a where a.b in ?#{#bs} and a.c in ?#{#cs}");
+ StringQuery query = new StringQuery("select a from A a where a.b in ?#{#bs} and a.c in ?#{#cs}", true);
String queryString = query.getQueryString();
softly.assertThat(queryString).isEqualTo("select a from A a where a.b in ?1 and a.c in ?2");
@@ -310,9 +312,11 @@ class StringQueryUnitTests {
@Test // DATAJPA-864
void detectsConstructorExpressions() {
- softly.assertThat(new StringQuery("select new Dto(a.foo, a.bar) from A a").hasConstructorExpression()).isTrue();
- softly.assertThat(new StringQuery("select new Dto (a.foo, a.bar) from A a").hasConstructorExpression()).isTrue();
- softly.assertThat(new StringQuery("select a from A a").hasConstructorExpression()).isFalse();
+ softly.assertThat(new StringQuery("select new Dto(a.foo, a.bar) from A a", false).hasConstructorExpression())
+ .isTrue();
+ softly.assertThat(new StringQuery("select new Dto (a.foo, a.bar) from A a", false).hasConstructorExpression())
+ .isTrue();
+ softly.assertThat(new StringQuery("select a from A a", true).hasConstructorExpression()).isFalse();
softly.assertAll();
}
@@ -325,8 +329,8 @@ class StringQueryUnitTests {
void detectsConstructorExpressionForDefaultConstructor() {
// Parentheses required
- softly.assertThat(new StringQuery("select new Dto() from A a").hasConstructorExpression()).isTrue();
- softly.assertThat(new StringQuery("select new Dto from A a").hasConstructorExpression()).isFalse();
+ softly.assertThat(new StringQuery("select new Dto() from A a", false).hasConstructorExpression()).isTrue();
+ softly.assertThat(new StringQuery("select new Dto from A a", false).hasConstructorExpression()).isFalse();
softly.assertAll();
}
@@ -334,7 +338,7 @@ class StringQueryUnitTests {
@Test // DATAJPA-1179
void bindingsMatchQueryForIdenticalSpelExpressions() {
- StringQuery query = new StringQuery("select a from A a where a.first = :#{#exp} or a.second = :#{#exp}");
+ StringQuery query = new StringQuery("select a from A a where a.first = :#{#exp} or a.second = :#{#exp}", true);
List bindings = query.getParameterBindings();
softly.assertThat(bindings).isNotEmpty();
@@ -351,18 +355,18 @@ class StringQueryUnitTests {
@Test // DATAJPA-1235
void getProjection() {
- checkProjection("SELECT something FROM", "something", "uppercase is supported");
- checkProjection("select something from", "something", "single expression");
- checkProjection("select x, y, z from", "x, y, z", "tuple");
- checkProjection("sect x, y, z from", "", "missing select");
- checkProjection("select x, y, z fron", "", "missing from");
+ checkProjection("SELECT something FROM", "something", "uppercase is supported", false);
+ checkProjection("select something from", "something", "single expression", false);
+ checkProjection("select x, y, z from", "x, y, z", "tuple", false);
+ checkProjection("sect x, y, z from", "", "missing select", false);
+ checkProjection("select x, y, z fron", "", "missing from", false);
softly.assertAll();
}
- void checkProjection(String query, String expected, String description) {
+ void checkProjection(String query, String expected, String description, boolean nativeQuery) {
- softly.assertThat(new StringQuery(query).getProjection()) //
+ softly.assertThat(new StringQuery(query, nativeQuery).getProjection()) //
.as("%s (%s)", description, query) //
.isEqualTo(expected);
}
@@ -370,25 +374,25 @@ class StringQueryUnitTests {
@Test // DATAJPA-1235
void getAlias() {
- checkAlias("from User u", "u", "simple query");
- checkAlias("select count(u) from User u", "u", "count query");
- checkAlias("select u from User as u where u.username = ?", "u", "with as");
- checkAlias("SELECT FROM USER U", "U", "uppercase");
- checkAlias("select u from User u", "u", "simple query");
- checkAlias("select u from com.acme.User u", "u", "fully qualified package name");
- checkAlias("select u from T05User u", "u", "interesting entity name");
- checkAlias("from User ", null, "trailing space");
- checkAlias("from User", null, "no trailing space");
- checkAlias("from User as bs", "bs", "ignored as");
- checkAlias("from User as AS", "AS", "ignored as using the second");
- checkAlias("from User asas", "asas", "asas is weird but legal");
+ checkAlias("from User u", "u", "simple query", false);
+ checkAlias("select count(u) from User u", "u", "count query", true);
+ checkAlias("select u from User as u where u.username = ?", "u", "with as", true);
+ checkAlias("SELECT FROM USER U", "U", "uppercase", false);
+ checkAlias("select u from User u", "u", "simple query", true);
+ checkAlias("select u from com.acme.User u", "u", "fully qualified package name", true);
+ checkAlias("select u from T05User u", "u", "interesting entity name", true);
+ checkAlias("from User ", null, "trailing space", false);
+ checkAlias("from User", null, "no trailing space", false);
+ checkAlias("from User as bs", "bs", "ignored as", false);
+ checkAlias("from User as AS", "AS", "ignored as using the second", false);
+ checkAlias("from User asas", "asas", "asas is weird but legal", false);
softly.assertAll();
}
- private void checkAlias(String query, String expected, String description) {
+ private void checkAlias(String query, String expected, String description, boolean nativeQuery) {
- softly.assertThat(new StringQuery(query).getAlias()) //
+ softly.assertThat(new StringQuery(query, nativeQuery).getAlias()) //
.as("%s (%s)", description, query) //
.isEqualTo(expected);
}
@@ -396,32 +400,32 @@ class StringQueryUnitTests {
@Test // DATAJPA-1200
void testHasNamedParameter() {
- checkHasNamedParameter("select something from x where id = :id", true, "named parameter");
- checkHasNamedParameter("in the :id middle", true, "middle");
- checkHasNamedParameter(":id start", true, "beginning");
- checkHasNamedParameter(":id", true, "alone");
- checkHasNamedParameter("select something from x where id = :id", true, "named parameter");
- checkHasNamedParameter(":UPPERCASE", true, "uppercase");
- checkHasNamedParameter(":lowercase", true, "lowercase");
- checkHasNamedParameter(":2something", true, "beginning digit");
- checkHasNamedParameter(":2", true, "only digit");
- checkHasNamedParameter(":.something", true, "dot");
- checkHasNamedParameter(":_something", true, "underscore");
- checkHasNamedParameter(":$something", true, "dollar");
- checkHasNamedParameter(":\uFE0F", true, "non basic latin emoji"); //
- checkHasNamedParameter(":\u4E01", true, "chinese japanese korean");
+ checkHasNamedParameter("select something from x where id = :id", true, "named parameter", true);
+ checkHasNamedParameter("in the :id middle", true, "middle", false);
+ checkHasNamedParameter(":id start", true, "beginning", false);
+ checkHasNamedParameter(":id", true, "alone", false);
+ checkHasNamedParameter("select something from x where id = :id", true, "named parameter", true);
+ checkHasNamedParameter(":UPPERCASE", true, "uppercase", false);
+ checkHasNamedParameter(":lowercase", true, "lowercase", false);
+ checkHasNamedParameter(":2something", true, "beginning digit", false);
+ checkHasNamedParameter(":2", true, "only digit", false);
+ checkHasNamedParameter(":.something", true, "dot", false);
+ checkHasNamedParameter(":_something", true, "underscore", false);
+ checkHasNamedParameter(":$something", true, "dollar", false);
+ checkHasNamedParameter(":\uFE0F", true, "non basic latin emoji", false); //
+ checkHasNamedParameter(":\u4E01", true, "chinese japanese korean", false);
- checkHasNamedParameter("no bind variable", false, "no bind variable");
- checkHasNamedParameter(":\u2004whitespace", false, "non basic latin whitespace");
- checkHasNamedParameter("select something from x where id = ?1", false, "indexed parameter");
- checkHasNamedParameter("::", false, "double colon");
- checkHasNamedParameter(":", false, "end of query");
- checkHasNamedParameter(":\u0003", false, "non-printable");
- checkHasNamedParameter(":*", false, "basic latin emoji");
- checkHasNamedParameter("\\:", false, "escaped colon");
- checkHasNamedParameter("::id", false, "double colon with identifier");
- checkHasNamedParameter("\\:id", false, "escaped colon with identifier");
- checkHasNamedParameter("select something from x where id = #something", false, "hash");
+ checkHasNamedParameter("no bind variable", false, "no bind variable", false);
+ checkHasNamedParameter(":\u2004whitespace", false, "non basic latin whitespace", false);
+ checkHasNamedParameter("select something from x where id = ?1", false, "indexed parameter", true);
+ checkHasNamedParameter("::", false, "double colon", false);
+ checkHasNamedParameter(":", false, "end of query", false);
+ checkHasNamedParameter(":\u0003", false, "non-printable", false);
+ checkHasNamedParameter(":*", false, "basic latin emoji", false);
+ checkHasNamedParameter("\\:", false, "escaped colon", false);
+ checkHasNamedParameter("::id", false, "double colon with identifier", false);
+ checkHasNamedParameter("\\:id", false, "escaped colon with identifier", false);
+ checkHasNamedParameter("select something from x where id = #something", false, "hash", true);
softly.assertAll();
}
@@ -429,11 +433,12 @@ class StringQueryUnitTests {
@Test // DATAJPA-1235
void ignoresQuotedNamedParameterLookAlike() {
- checkNumberOfNamedParameters("select something from blah where x = '0:name'", 0, "single quoted");
- checkNumberOfNamedParameters("select something from blah where x = \"0:name\"", 0, "double quoted");
- checkNumberOfNamedParameters("select something from blah where x = '\"0':name", 1, "double quote in single quotes");
- checkNumberOfNamedParameters("select something from blah where x = \"'0\":name", 1,
- "single quote in double quotes");
+ checkNumberOfNamedParameters("select something from blah where x = '0:name'", 0, "single quoted", false);
+ checkNumberOfNamedParameters("select something from blah where x = \"0:name\"", 0, "double quoted", false);
+ checkNumberOfNamedParameters("select something from blah where x = '\"0':name", 1, "double quote in single quotes",
+ false);
+ checkNumberOfNamedParameters("select something from blah where x = \"'0\":name", 1, "single quote in double quotes",
+ false);
softly.assertAll();
}
@@ -442,7 +447,7 @@ class StringQueryUnitTests {
void detectsMultiplePositionalParameterBindingsWithoutIndex() {
String queryString = "select u from User u where u.id in ? and u.names in ? and foo = ?";
- StringQuery query = new StringQuery(queryString);
+ StringQuery query = new StringQuery(queryString, false);
softly.assertThat(query.getQueryString()).isEqualTo(queryString);
softly.assertThat(query.hasParameterBindings()).isTrue();
@@ -464,14 +469,14 @@ class StringQueryUnitTests {
for (String testQuery : testQueries) {
Assertions.assertThatExceptionOfType(IllegalArgumentException.class) //
- .describedAs(testQuery).isThrownBy(() -> new StringQuery(testQuery));
+ .describedAs(testQuery).isThrownBy(() -> new StringQuery(testQuery, false));
}
}
@Test // DATAJPA-1307
void makesUsageOfJdbcStyleParameterAvailable() {
- softly.assertThat(new StringQuery("something = ?").usesJdbcStyleParameters()).isTrue();
+ softly.assertThat(new StringQuery("something = ?", false).usesJdbcStyleParameters()).isTrue();
List testQueries = Arrays.asList( //
"something = ?1", //
@@ -481,7 +486,7 @@ class StringQueryUnitTests {
for (String testQuery : testQueries) {
- softly.assertThat(new StringQuery(testQuery) //
+ softly.assertThat(new StringQuery(testQuery, false) //
.usesJdbcStyleParameters()) //
.describedAs(testQuery) //
.isFalse();
@@ -494,7 +499,7 @@ class StringQueryUnitTests {
void questionMarkInStringLiteral() {
String queryString = "select '? ' from dual";
- StringQuery query = new StringQuery(queryString);
+ StringQuery query = new StringQuery(queryString, false);
softly.assertThat(query.getQueryString()).isEqualTo(queryString);
softly.assertThat(query.hasParameterBindings()).isFalse();
@@ -515,7 +520,7 @@ class StringQueryUnitTests {
"select a, b from C");
for (String queryString : queriesWithoutDefaultProjection) {
- softly.assertThat(new StringQuery(queryString).isDefaultProjection()) //
+ softly.assertThat(new StringQuery(queryString, true).isDefaultProjection()) //
.describedAs(queryString) //
.isFalse();
}
@@ -532,7 +537,7 @@ class StringQueryUnitTests {
);
for (String queryString : queriesWithDefaultProjection) {
- softly.assertThat(new StringQuery(queryString).isDefaultProjection()) //
+ softly.assertThat(new StringQuery(queryString, true).isDefaultProjection()) //
.describedAs(queryString) //
.isTrue();
}
@@ -544,7 +549,7 @@ class StringQueryUnitTests {
void usingPipesWithNamedParameter() {
String queryString = "SELECT u FROM User u WHERE u.lastname LIKE '%'||:name||'%'";
- StringQuery query = new StringQuery(queryString);
+ StringQuery query = new StringQuery(queryString, true);
assertThat(query.getParameterBindings()) //
.extracting(ParameterBinding::getName) //
@@ -555,16 +560,16 @@ class StringQueryUnitTests {
void usingGreaterThanWithNamedParameter() {
String queryString = "SELECT u FROM User u WHERE :age>u.age";
- StringQuery query = new StringQuery(queryString);
+ StringQuery query = new StringQuery(queryString, true);
assertThat(query.getParameterBindings()) //
.extracting(ParameterBinding::getName) //
.containsExactly("age");
}
- void checkNumberOfNamedParameters(String query, int expectedSize, String label) {
+ void checkNumberOfNamedParameters(String query, int expectedSize, String label, boolean nativeQuery) {
- DeclaredQuery declaredQuery = DeclaredQuery.of(query);
+ DeclaredQuery declaredQuery = DeclaredQuery.of(query, nativeQuery);
softly.assertThat(declaredQuery.hasNamedParameter()) //
.describedAs("hasNamed Parameter " + label) //
@@ -574,9 +579,9 @@ class StringQueryUnitTests {
.hasSize(expectedSize);
}
- private void checkHasNamedParameter(String query, boolean expected, String label) {
+ private void checkHasNamedParameter(String query, boolean expected, String label, boolean nativeQuery) {
- softly.assertThat(new StringQuery(query).hasNamedParameter()) //
+ softly.assertThat(new StringQuery(query, nativeQuery).hasNamedParameter()) //
.describedAs(String.format("<%s> (%s)", query, label)) //
.isEqualTo(expected);
}