Introduce JSqlParser as option for parsing native queries.

Introduce a library that makes it possible to parse native SQL statements.

See #2409.
This commit is contained in:
Diego Krupitza
2022-01-22 11:23:57 +01:00
committed by Greg L. Turnquist
parent 8c127f66b4
commit 1fe4cf598d
29 changed files with 2384 additions and 277 deletions

View File

@@ -28,6 +28,7 @@
<postgresql>42.2.19</postgresql>
<springdata.commons>3.0.0-SNAPSHOT</springdata.commons>
<vavr>0.10.3</vavr>
<jsqlparser.version>4.3</jsqlparser.version>
<hibernate.groupId>org.hibernate</hibernate.groupId>

View File

@@ -226,6 +226,14 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>${jsqlparser.version}</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
</dependencies>
<build>

View File

@@ -15,14 +15,6 @@
*/
package org.springframework.data.jpa.repository.query;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import jakarta.persistence.EntityManager;
import jakarta.persistence.LockModeType;
import jakarta.persistence.Query;
@@ -31,6 +23,14 @@ import jakarta.persistence.Tuple;
import jakarta.persistence.TupleElement;
import jakarta.persistence.TypedQuery;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.EntityGraph;

View File

@@ -34,6 +34,7 @@ import org.springframework.util.Assert;
* @author Tom Hombergs
* @author David Madden
* @author Mark Paluch
* @author Diego Krupitza
*/
abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
@@ -55,8 +56,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
* @param parser must not be {@literal null}.
*/
public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
@Nullable String countQueryString,
QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {
@Nullable String countQueryString, QueryMethodEvaluationContextProvider evaluationContextProvider,
SpelExpressionParser parser) {
super(method, em);
@@ -65,10 +66,12 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
Assert.notNull(parser, "Parser must not be null!");
this.evaluationContextProvider = evaluationContextProvider;
this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation(), parser);
this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation(), parser,
method.isNativeQuery());
DeclaredQuery countQuery = query.deriveCountQuery(countQueryString, method.getCountQueryProjection());
this.countQuery = ExpressionBasedStringQuery.from(countQuery, method.getEntityInformation(), parser);
this.countQuery = ExpressionBasedStringQuery.from(countQuery, method.getEntityInformation(), parser,
method.isNativeQuery());
this.parser = parser;
@@ -79,7 +82,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
@Override
public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
String sortedQueryString = QueryUtils.applySorting(query.getQueryString(), accessor.getSort(), query.getAlias());
String sortedQueryString = QueryEnhancerFactory.forQuery(query) //
.applySorting(accessor.getSort(), query.getAlias());
ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
Query query = createJpaQuery(sortedQueryString, processor.getReturnedType());

View File

@@ -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 <code>true</code> if native query otherwise <code>false</code>
*/
default boolean isNativeQuery() {
return false;
}
}

View File

@@ -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<String> 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<String> getJoinAliases() {
return QueryUtils.getOuterJoinAliases(this.query.getQueryString());
}
@Override
public DeclaredQuery getQuery() {
return this.query;
}
}

View File

@@ -69,7 +69,7 @@ class EmptyDeclaredQuery implements DeclaredQuery {
Assert.hasText(countQuery, "CountQuery must not be empty!");
return DeclaredQuery.of(countQuery);
return DeclaredQuery.of(countQuery, false);
}
@Override

View File

@@ -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);
}
/**

View File

@@ -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<String> 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<Expression> 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<String> joinAliases = getJoinAliases(selectBody);
final Set<String> selectionAliases = getSelectionAliases(selectBody);
List<OrderByElement> 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<String> 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<String> 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<String> 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<String> 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<String> joinAliases, final Set<String> 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 <code>from User u</code> 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 <code>from User u</code> 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<SelectItem> 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<String> 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 <code>true</code> when the projection only contains a single column definition otherwise <code>false</code>
*/
private boolean onlyASingleColumnProjection(List<SelectItem> 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;
}
}

View File

@@ -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 <code>AND</code>.
*
* @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<Expression> 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<String> countFields, final boolean distinct) {
List<Expression> 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<Expression> expressions = Collections.singletonList(new Column(column));
ExpressionList lowerParamExpression = new ExpressionList(expressions);
return new Function() //
.withName("lower") //
.withParameters(lowerParamExpression);
}
}

View File

@@ -166,10 +166,8 @@ public class JpaQueryCreator extends AbstractQueryCreator<CriteriaQuery<? extend
Class<?> typeToRead = returnedType.getReturnedType();
query = typeToRead.isInterface()
? query.multiselect(selections)
: query.select((Selection) builder.construct(typeToRead,
selections.toArray(new Selection[0])));
query = typeToRead.isInterface() ? query.multiselect(selections)
: query.select((Selection) builder.construct(typeToRead, selections.toArray(new Selection[0])));
} else if (tree.isExistsProjection()) {
@@ -241,83 +239,83 @@ public class JpaQueryCreator extends AbstractQueryCreator<CriteriaQuery<? extend
Type type = part.getType();
switch (type) {
case BETWEEN:
ParameterMetadata<Comparable> first = provider.next(part);
ParameterMetadata<Comparable> 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<Collection<?>>) provider.next(part, Collection.class).getExpression()).not();
case IN:
// cast required for eclipselink workaround, see DATAJPA-433
return upperIfIgnoreCase(getTypedPath(root, part))
.in((Expression<Collection<?>>) provider.next(part, Collection.class).getExpression());
case STARTING_WITH:
case ENDING_WITH:
case CONTAINING:
case NOT_CONTAINING:
case BETWEEN:
ParameterMetadata<Comparable> first = provider.next(part);
ParameterMetadata<Comparable> 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<Collection<?>>) provider.next(part, Collection.class).getExpression()).not();
case IN:
// cast required for eclipselink workaround, see DATAJPA-433
return upperIfIgnoreCase(getTypedPath(root, part))
.in((Expression<Collection<?>>) 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<Collection<Object>> propertyExpression = traversePath(root, property);
ParameterExpression<Object> parameterExpression = provider.next(part).getExpression();
Expression<Collection<Object>> propertyExpression = traversePath(root, property);
ParameterExpression<Object> 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<String> stringPath = getTypedPath(root, part);
Expression<String> propertyExpression = upperIfIgnoreCase(stringPath);
Expression<String> 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<Boolean> truePath = getTypedPath(root, part);
return builder.isTrue(truePath);
case FALSE:
Expression<Boolean> falsePath = getTypedPath(root, part);
return builder.isFalse(falsePath);
case SIMPLE_PROPERTY:
ParameterMetadata<Object> expression = provider.next(part);
Expression<Object> 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<String> stringPath = getTypedPath(root, part);
Expression<String> propertyExpression = upperIfIgnoreCase(stringPath);
Expression<String> 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<Boolean> truePath = getTypedPath(root, part);
return builder.isTrue(truePath);
case FALSE:
Expression<Boolean> falsePath = getTypedPath(root, part);
return builder.isFalse(falsePath);
case SIMPLE_PROPERTY:
ParameterMetadata<Object> expression = provider.next(part);
Expression<Object> 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<Collection<Object>> collectionPath = traversePath(root, property);
return type.equals(IS_NOT_EMPTY) ? builder.isNotEmpty(collectionPath) : builder.isEmpty(collectionPath);
Expression<Collection<Object>> 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);
}
}
@@ -342,22 +340,22 @@ public class JpaQueryCreator extends AbstractQueryCreator<CriteriaQuery<? extend
switch (part.shouldIgnoreCase()) {
case ALWAYS:
case ALWAYS:
Assert.state(canUpperCase(expression), "Unable to ignore case of " + expression.getJavaType().getName()
+ " types, the property '" + part.getProperty().getSegment() + "' must reference a String");
Assert.state(canUpperCase(expression), "Unable to ignore case of " + expression.getJavaType().getName()
+ " types, the property '" + part.getProperty().getSegment() + "' must reference a String");
return (Expression<T>) builder.upper((Expression<String>) expression);
case WHEN_POSSIBLE:
if (canUpperCase(expression)) {
return (Expression<T>) builder.upper((Expression<String>) expression);
}
case WHEN_POSSIBLE:
case NEVER:
default:
if (canUpperCase(expression)) {
return (Expression<T>) builder.upper((Expression<String>) expression);
}
case NEVER:
default:
return (Expression<T>) expression;
return (Expression<T>) expression;
}
}

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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<String> 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 <code>countProjection</code>.
*
* @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<String> getJoinAliases();
/**
* Gets the query we want to use for enhancements.
*
* @return non null {@link DeclaredQuery} that wraps the query
*/
DeclaredQuery getQuery();
}

View File

@@ -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 <code>true</code> if JSqlParser is in the classpath and the query is classified as a native query otherwise
* <code>false</code>
*/
private static boolean qualifiesForJSqlParserUsage(DeclaredQuery query) {
return JSQLPARSER_IN_CLASSPATH && query.isNativeQuery();
}
/**
* Checks whether JSqlParser is in classpath or not.
*
* @return <code>true</code> when in classpath otherwise <code>false</code>
*/
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;
}
}
}

View File

@@ -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.

View File

@@ -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<String> joinAliases, Set<String> selectionAlias, @Nullable String alias,
Order order) {
@@ -811,7 +812,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;

View File

@@ -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();
}
@Override
@@ -98,8 +104,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);
}
@Override
@@ -138,6 +145,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.
*
@@ -227,7 +239,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;
@@ -246,36 +259,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) {

View File

@@ -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;

View File

@@ -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<String> 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<String> 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);
}
}

View File

@@ -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();
}
}

View File

@@ -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<User, Integer> {
@Query(value = "query", nativeQuery = true)
@Query(value = "Select u from User u where u.lastname = ?1", nativeQuery = true)
List<User> findByLastname(String lastname);
@Query(name = "HateoasAwareSpringDataWebConfiguration.bar")

View File

@@ -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)) //

View File

@@ -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);
}
}

View File

@@ -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<Arguments> 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<String> aliases = QueryUtils.getOuterJoinAliases(queryString);
StringQuery nativeQuery = new StringQuery(queryString, true);
Set<String> 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<Arguments> 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<String> aliases) {
StringQuery nativeQuery = new StringQuery(queryString, true);
StringQuery nonNativeQuery = new StringQuery(queryString, false);
Set<String> nativeJoinAliases = getEnhancer(nativeQuery).getJoinAliases();
Set<String> 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<Arguments> 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);
}
}

View File

@@ -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.");
}
}

View File

@@ -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<InvoiceItem> query = builder.createQuery(InvoiceItem.class);
Root<InvoiceItem> 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<Employee> employees;
@Id
String id;
@OneToMany
Set<Employee> 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<Credential> credentials;
@Id
String id;
@OneToMany
Set<Credential> 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() {
}
}
}

View File

@@ -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");
}

View File

@@ -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<ParameterBinding> 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<ParameterBinding> 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<ParameterBinding> 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<ParameterBinding> 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<ParameterBinding> 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<ParameterBinding> 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<ParameterBinding> 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<ParameterBinding> 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<String> 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);
}