Refactor EQL, HQL & JPQL query rendering.

This commit turns existing query transformers into introspectors using eager parsing and detection of query parts.
Query transformation has also been split up into dedicated Count and Sort Query parts.
To reduce duplicate code across the existing parses we introduced a reusable functional configuration.
For better testing support a query assertion has been introduced to query parser tests and we adjusted invalid formatting.

See: #3309
Closes: #3326
This commit is contained in:
Mark Paluch
2024-05-23 11:14:44 +02:00
committed by Christoph Strobl
parent f607dd2757
commit 3e1c8a28a9
46 changed files with 5719 additions and 5556 deletions

View File

@@ -17,6 +17,7 @@ package org.springframework.data.jpa.repository;
import java.util.List;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.model.IPersonProjection;
import org.springframework.data.jpa.model.Person;
import org.springframework.data.repository.ListCrudRepository;
@@ -33,6 +34,9 @@ public interface PersonRepository extends ListCrudRepository<Person, Integer> {
@Query("SELECT p FROM org.springframework.data.jpa.model.Person p WHERE p.firstname = ?1")
List<Person> findAllWithAnnotatedQueryByFirstname(String firstname);
@Query("SELECT p FROM org.springframework.data.jpa.model.Person p WHERE p.firstname = ?1")
List<Person> findAllWithAnnotatedQueryByFirstname(String firstname, Sort sort);
@Query(value = "SELECT * FROM person WHERE firstname = ?1", nativeQuery = true)
List<Person> findAllWithNativeQueryByFirstname(String firstname);
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2024 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;
import org.springframework.data.domain.Sort;
/**
* @author Mark Paluch
*/
public class Profiler {
public static void main(String[] args) throws InterruptedException {
RepositoryFinderTests tests = new RepositoryFinderTests();
RepositoryFinderTests.BenchmarkParameters params = new RepositoryFinderTests.BenchmarkParameters();
params.doSetup();
System.out.println("Ready. Waiting 10sec");
Thread.sleep(10000);
System.out.println("Go!");
while (true) {
params.repositoryProxy.findAllWithAnnotatedQueryByFirstname("first", Sort.by("firstname"));
}
}
}

View File

@@ -40,6 +40,8 @@ import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;
import org.openjdk.jmh.annotations.Timeout;
import org.openjdk.jmh.annotations.Warmup;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.model.IPersonProjection;
import org.springframework.data.jpa.model.Person;
import org.springframework.data.jpa.model.Profile;
@@ -151,11 +153,17 @@ public class RepositoryFinderTests {
return parameters.repositoryProxy.findAllAndProjectToInterfaceByFirstname("first");
}
@Benchmark
public List<Person> stringBasedQuery(BenchmarkParameters parameters) {
return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname("first");
}
@Benchmark
public List<Person> stringBasedQueryDynamicSort(BenchmarkParameters parameters) {
return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname("first", Sort.by("firstname"));
}
@Benchmark
public List<Person> stringBasedNativeQuery(BenchmarkParameters parameters) {
return parameters.repositoryProxy.findAllWithNativeQueryByFirstname("first");

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2024 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 jmh.mbr.junit5.Microbenchmark;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Timeout;
import org.openjdk.jmh.annotations.Warmup;
import org.springframework.data.domain.Sort;
/**
* @author Mark Paluch
*/
@Microbenchmark
@Fork(1)
@Warmup(time = 2, iterations = 3)
@Measurement(time = 2)
@Timeout(time = 2)
public class HqlParserTests {
@State(Scope.Benchmark)
public static class BenchmarkParameters {
DeclaredQuery query;
Sort sort = Sort.by("foo");
QueryEnhancer enhancer;
@Setup(Level.Iteration)
public void doSetup() {
String s = """
SELECT e FROM Employee e JOIN e.projects p
WHERE TREAT(p AS LargeProject).budget > 1000
OR TREAT(p AS SmallProject).name LIKE 'Persist%'
OR p.description LIKE "cost overrun"
""";
query = DeclaredQuery.of(s, false);
enhancer = QueryEnhancerFactory.forQuery(query);
}
}
@Benchmark
public Object measure(BenchmarkParameters parameters) {
return parameters.enhancer.applySorting(parameters.sort);
}
}

View File

@@ -40,6 +40,15 @@ interface DeclaredQuery {
return ObjectUtils.isEmpty(query) ? EmptyDeclaredQuery.EMPTY_QUERY : new StringQuery(query, nativeQuery);
}
static boolean hasNamedParameter(String query) {
if (ObjectUtils.isEmpty(query)) {
return false;
}
return StringQuery.hasNamedParameter(query);
}
/**
* @return whether the underlying query has at least one named parameter.
*/

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2023-2024 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.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.List;
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
import org.springframework.lang.Nullable;
/**
* An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed EQL query into a
* {@code COUNT(…)} query.
*
* @author Greg Turnquist
* @author Mark Paluch
* @since 3.4
*/
@SuppressWarnings("ConstantValue")
class EqlCountQueryTransformer extends EqlQueryRenderer {
private final @Nullable String countProjection;
private final @Nullable String primaryFromAlias;
EqlCountQueryTransformer(@Nullable String countProjection, @Nullable String primaryFromAlias) {
this.countProjection = countProjection;
this.primaryFromAlias = primaryFromAlias;
}
@Override
public QueryRendererBuilder visitSelect_statement(EqlParser.Select_statementContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.appendExpression(visit(ctx.select_clause()));
builder.appendExpression(visit(ctx.from_clause()));
if (ctx.where_clause() != null) {
builder.appendExpression(visit(ctx.where_clause()));
}
if (ctx.groupby_clause() != null) {
builder.appendExpression(visit(ctx.groupby_clause()));
}
if (ctx.having_clause() != null) {
builder.appendExpression(visit(ctx.having_clause()));
}
return builder;
}
@Override
public QueryRendererBuilder visitSelect_clause(EqlParser.Select_clauseContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.append(JpaQueryParsingToken.expression(ctx.SELECT()));
builder.append(TOKEN_COUNT_FUNC);
if (countProjection != null) {
builder.append(JpaQueryParsingToken.token(countProjection));
}
QueryRendererBuilder nested = QueryRenderer.builder();
if (ctx.DISTINCT() != null) {
nested.append(JpaQueryParsingToken.expression(ctx.DISTINCT()));
}
if (countProjection == null) {
if (ctx.DISTINCT() != null) {
QueryRendererBuilder selectionListbuilder = QueryRendererBuilder.concat(ctx.select_item(), this::visit,
TOKEN_COMMA);
List<JpaQueryParsingToken> countSelection = QueryTransformers
.filterCountSelection(selectionListbuilder.build().stream().toList());
if (countSelection.stream().anyMatch(eqlToken -> eqlToken.getToken().contains("new"))) {
// constructor
nested.append(new JpaQueryParsingToken(primaryFromAlias));
} else {
// keep all the select items to distinct against
nested.append(countSelection);
}
} else {
nested.append(new JpaQueryParsingToken(primaryFromAlias));
}
}
builder.appendInline(nested);
builder.append(TOKEN_CLOSE_PAREN);
return builder;
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2024 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.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.lang.Nullable;
/**
* {@link ParsedQueryIntrospector} for EQL queries.
*
* @author Mark Paluch
*/
@SuppressWarnings("UnreachableCode")
class EqlQueryIntrospector extends EqlBaseVisitor<Void> implements ParsedQueryIntrospector {
private final EqlQueryRenderer renderer = new EqlQueryRenderer();
private @Nullable String primaryFromAlias = null;
private @Nullable List<JpaQueryParsingToken> projection;
private boolean projectionProcessed;
private boolean hasConstructorExpression = false;
@Override
public String getAlias() {
return primaryFromAlias;
}
@Override
public List<JpaQueryParsingToken> getProjection() {
return projection == null ? Collections.emptyList() : projection;
}
@Override
public boolean hasConstructorExpression() {
return hasConstructorExpression;
}
@Override
public Void visitSelect_clause(EqlParser.Select_clauseContext ctx) {
List<EqlParser.Select_itemContext> selections = ctx.select_item();
List<JpaQueryParsingToken> selectItemTokens = new ArrayList<>(selections.size() * 2);
for (EqlParser.Select_itemContext selection : selections) {
if (!selectItemTokens.isEmpty()) {
selectItemTokens.add(TOKEN_COMMA);
}
selectItemTokens.add(JpaQueryParsingToken.token(renderer.visitSelect_item(selection).build().render()));
}
if (!projectionProcessed) {
projection = selectItemTokens;
projectionProcessed = true;
}
return super.visitSelect_clause(ctx);
}
@Override
public Void visitRange_variable_declaration(EqlParser.Range_variable_declarationContext ctx) {
if (primaryFromAlias == null) {
primaryFromAlias = ctx.identification_variable() != null ? ctx.identification_variable().getText()
: ctx.entity_name().getText();
}
return super.visitRange_variable_declaration(ctx);
}
@Override
public Void visitConstructor_expression(EqlParser.Constructor_expressionContext ctx) {
hasConstructorExpression = true;
return super.visitConstructor_expression(ctx);
}
}

View File

@@ -15,118 +15,30 @@
*/
package org.springframework.data.jpa.repository.query;
import java.util.List;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
/**
* Implements the {@code EQL} parsing operations of a {@link JpaQueryParserSupport} using the ANTLR-generated
* {@link EqlParser} and {@link EqlQueryTransformer}.
* Implements the {@code EQL} parsing operations of a {@link JpaQueryParser} using the ANTLR-generated
* {@link EqlParser}.
*
* @author Greg Turnquist
* @author Mark Paluch
* @since 3.2
*/
class EqlQueryParser extends JpaQueryParserSupport {
class EqlQueryParser extends JpaQueryParser {
EqlQueryParser(String query) {
super(query);
private EqlQueryParser(String query) {
super(parse(query, EqlLexer::new, EqlParser::new, EqlParser::start), new EqlQueryIntrospector(),
EqlSortedQueryTransformer::new, EqlCountQueryTransformer::new);
}
/**
* Convenience method to parse a EQL query. Will throw a {@link BadJpqlGrammarException} if the query is invalid.
* Parse a EQL query.
*
* @param query
* @return a parsed query, ready for postprocessing
* @return the query parser.
* @throws BadJpqlGrammarException
*/
public static ParserRuleContext parseQuery(String query) {
EqlLexer lexer = new EqlLexer(CharStreams.fromString(query));
EqlParser parser = new EqlParser(new CommonTokenStream(lexer));
configureParser(query, lexer, parser);
return parser.start();
public static EqlQueryParser parseQuery(String query) throws BadJpqlGrammarException {
return new EqlQueryParser(query);
}
/**
* Parse the query using {@link #parseQuery(String)}.
*
* @return a parsed query
*/
@Override
protected ParserRuleContext parse(String query) {
return parseQuery(query);
}
/**
* Use the {@link EqlQueryTransformer} to transform the original query into a query with the {@link Sort} applied.
*
* @param parsedQuery
* @param sort can be {@literal null}
* @return list of {@link JpaQueryParsingToken}s
*/
@Override
protected List<JpaQueryParsingToken> applySort(ParserRuleContext parsedQuery, Sort sort) {
return new EqlQueryTransformer(sort).visit(parsedQuery);
}
/**
* Use the {@link EqlQueryTransformer} to transform the original query into a count query.
*
* @param parsedQuery
* @param countProjection
* @return list of {@link JpaQueryParsingToken}s
*/
@Override
protected List<JpaQueryParsingToken> doCreateCountQuery(ParserRuleContext parsedQuery,
@Nullable String countProjection) {
return new EqlQueryTransformer(true, countProjection).visit(parsedQuery);
}
/**
* Run the parsed query through {@link EqlQueryTransformer} to find the primary FROM clause's alias.
*
* @param parsedQuery
* @return can be {@literal null}
*/
@Override
protected String doFindAlias(ParserRuleContext parsedQuery) {
EqlQueryTransformer transformVisitor = new EqlQueryTransformer();
transformVisitor.visit(parsedQuery);
return transformVisitor.getAlias();
}
/**
* Use {@link EqlQueryTransformer} to find the projection of the query.
*
* @param parsedQuery
* @return
*/
@Override
protected List<JpaQueryParsingToken> doFindProjection(ParserRuleContext parsedQuery) {
EqlQueryTransformer transformVisitor = new EqlQueryTransformer();
transformVisitor.visit(parsedQuery);
return transformVisitor.getProjection();
}
/**
* Use {@link EqlQueryTransformer} to detect if the query uses a {@code new com.example.Dto()} DTO constructor in the
* primary select clause.
*
* @param parsedQuery
* @return Guaranteed to be {@literal true} or {@literal false}.
*/
@Override
protected boolean doCheckForConstructor(ParserRuleContext parsedQuery) {
EqlQueryTransformer transformVisitor = new EqlQueryTransformer();
transformVisitor.visit(parsedQuery);
return transformVisitor.hasConstructorExpression();
}
}

View File

@@ -1,246 +0,0 @@
/*
* Copyright 2023-2024 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.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed EQL query.
*
* @author Greg Turnquist
* @since 3.2
*/
class EqlQueryTransformer extends EqlQueryRenderer {
// TODO: Separate input from result parameters, encapsulation...
private final Sort sort;
private final boolean countQuery;
private final @Nullable String countProjection;
private @Nullable String primaryFromAlias = null;
private List<JpaQueryParsingToken> projection = Collections.emptyList();
private boolean projectionProcessed;
private boolean hasConstructorExpression = false;
private JpaQueryTransformerSupport transformerSupport;
EqlQueryTransformer() {
this(Sort.unsorted(), false, null);
}
EqlQueryTransformer(Sort sort) {
this(sort, false, null);
}
EqlQueryTransformer(boolean countQuery, @Nullable String countProjection) {
this(Sort.unsorted(), countQuery, countProjection);
}
private EqlQueryTransformer(Sort sort, boolean countQuery, @Nullable String countProjection) {
Assert.notNull(sort, "Sort must not be null");
this.sort = sort;
this.countQuery = countQuery;
this.countProjection = countProjection;
this.transformerSupport = new JpaQueryTransformerSupport();
}
@Nullable
public String getAlias() {
return this.primaryFromAlias;
}
public List<JpaQueryParsingToken> getProjection() {
return this.projection;
}
public boolean hasConstructorExpression() {
return this.hasConstructorExpression;
}
@Override
public List<JpaQueryParsingToken> visitSelect_statement(EqlParser.Select_statementContext ctx) {
List<JpaQueryParsingToken> tokens = newArrayList();
tokens.addAll(visit(ctx.select_clause()));
tokens.addAll(visit(ctx.from_clause()));
if (ctx.where_clause() != null) {
tokens.addAll(visit(ctx.where_clause()));
}
if (ctx.groupby_clause() != null) {
tokens.addAll(visit(ctx.groupby_clause()));
}
if (ctx.having_clause() != null) {
tokens.addAll(visit(ctx.having_clause()));
}
if (!countQuery) {
if (ctx.orderby_clause() != null) {
tokens.addAll(visit(ctx.orderby_clause()));
}
if (sort.isSorted()) {
if (ctx.orderby_clause() != null) {
NOSPACE(tokens);
tokens.add(TOKEN_COMMA);
} else {
SPACE(tokens);
tokens.add(TOKEN_ORDER_BY);
}
tokens.addAll(transformerSupport.generateOrderByArguments(primaryFromAlias, sort));
}
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitSelect_clause(EqlParser.Select_clauseContext ctx) {
List<JpaQueryParsingToken> tokens = newArrayList();
tokens.add(new JpaQueryParsingToken(ctx.SELECT()));
if (countQuery) {
tokens.add(TOKEN_COUNT_FUNC);
}
if (ctx.DISTINCT() != null) {
tokens.add(new JpaQueryParsingToken(ctx.DISTINCT()));
}
List<JpaQueryParsingToken> selectItemTokens = newArrayList();
ctx.select_item().forEach(selectItemContext -> {
selectItemTokens.addAll(visit(selectItemContext));
NOSPACE(selectItemTokens);
selectItemTokens.add(TOKEN_COMMA);
});
CLIP(selectItemTokens);
SPACE(selectItemTokens);
if (countQuery) {
if (countProjection != null) {
tokens.add(new JpaQueryParsingToken(countProjection));
} else {
if (ctx.DISTINCT() != null) {
List<JpaQueryParsingToken> countSelection = QueryTransformers.filterCountSelection(selectItemTokens);
if (countSelection.stream().anyMatch(jpqlToken -> jpqlToken.getToken().contains("new"))) {
// constructor
tokens.add(new JpaQueryParsingToken(() -> primaryFromAlias));
} else {
// keep all the select items to distinct against
tokens.addAll(countSelection);
}
} else {
tokens.add(new JpaQueryParsingToken(() -> primaryFromAlias));
}
}
NOSPACE(tokens);
tokens.add(TOKEN_CLOSE_PAREN);
} else {
tokens.addAll(selectItemTokens);
}
if (!projectionProcessed) {
projection = selectItemTokens;
projectionProcessed = true;
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitSelect_item(EqlParser.Select_itemContext ctx) {
List<JpaQueryParsingToken> tokens = super.visitSelect_item(ctx);
if (ctx.result_variable() != null) {
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitRange_variable_declaration(EqlParser.Range_variable_declarationContext ctx) {
List<JpaQueryParsingToken> tokens = newArrayList();
tokens.addAll(visit(ctx.entity_name()));
if (ctx.AS() != null) {
tokens.add(new JpaQueryParsingToken(ctx.AS()));
}
tokens.addAll(visit(ctx.identification_variable()));
if (primaryFromAlias == null) {
primaryFromAlias = tokens.get(tokens.size() - 1).getToken();
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitJoin(EqlParser.JoinContext ctx) {
List<JpaQueryParsingToken> tokens = super.visitJoin(ctx);
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitConstructor_expression(EqlParser.Constructor_expressionContext ctx) {
hasConstructorExpression = true;
return super.visitConstructor_expression(ctx);
}
private static <T> ArrayList<T> newArrayList() {
return new ArrayList<>();
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2023-2024 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.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.List;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed EQL query by applying
* {@link Sort}.
*
* @author Greg Turnquist
* @author Mark Paluch
* @since 3.2
*/
@SuppressWarnings("ConstantValue")
class EqlSortedQueryTransformer extends EqlQueryRenderer {
private final JpaQueryTransformerSupport transformerSupport = new JpaQueryTransformerSupport();
private final Sort sort;
private final @Nullable String primaryFromAlias;
EqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias) {
Assert.notNull(sort, "Sort must not be null");
this.sort = sort;
this.primaryFromAlias = primaryFromAlias;
}
@Override
public QueryRendererBuilder visitSelect_statement(EqlParser.Select_statementContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.appendExpression(visit(ctx.select_clause()));
builder.appendExpression(visit(ctx.from_clause()));
if (ctx.where_clause() != null) {
builder.appendExpression(visit(ctx.where_clause()));
}
if (ctx.groupby_clause() != null) {
builder.appendExpression(visit(ctx.groupby_clause()));
}
if (ctx.having_clause() != null) {
builder.appendExpression(visit(ctx.having_clause()));
}
doVisitOrderBy(builder, ctx);
for (int i = 0; i < ctx.setOperator().size(); i++) {
builder.appendExpression(visit(ctx.setOperator(i)));
builder.appendExpression(visit(ctx.select_statement(i)));
}
return builder;
}
private void doVisitOrderBy(QueryRendererBuilder builder, EqlParser.Select_statementContext ctx) {
if (ctx.orderby_clause() != null) {
QueryRendererBuilder existingOrder = visit(ctx.orderby_clause());
if (sort.isSorted()) {
builder.appendInline(existingOrder);
} else {
builder.append(existingOrder);
}
}
if (sort.isSorted()) {
List<JpaQueryParsingToken> sortBy = transformerSupport.orderBy(primaryFromAlias, sort);
if (ctx.orderby_clause() != null) {
QueryRendererBuilder extension = QueryRenderer.builder().append(TOKEN_COMMA).append(sortBy);
builder.appendInline(extension);
} else {
builder.append(TOKEN_ORDER_BY);
builder.append(sortBy);
}
}
}
@Override
public QueryRendererBuilder visitSelect_item(EqlParser.Select_itemContext ctx) {
QueryRendererBuilder builder = super.visitSelect_item(ctx);
if (ctx.result_variable() != null) {
List<JpaQueryParsingToken> tokens = builder.build().stream().toList();
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
}
return builder;
}
@Override
public QueryRendererBuilder visitJoin(EqlParser.JoinContext ctx) {
QueryRendererBuilder builder = super.visitJoin(ctx);
List<JpaQueryParsingToken> tokens = builder.build().stream().toList();
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
return builder;
}
}

View File

@@ -0,0 +1,249 @@
/*
* Copyright 2022-2024 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.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.List;
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
import org.springframework.lang.Nullable;
/**
* An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed HQL query into a
* {@code COUNT(…)} query.
*
* @author Greg Turnquist
* @author Christoph Strobl
* @author Mark Paluch
* @since 3.1
*/
@SuppressWarnings("ConstantValue")
class HqlCountQueryTransformer extends HqlQueryRenderer {
private final @Nullable String countProjection;
private final @Nullable String primaryFromAlias;
HqlCountQueryTransformer(@Nullable String countProjection, @Nullable String primaryFromAlias) {
this.countProjection = countProjection;
this.primaryFromAlias = primaryFromAlias;
}
@Override
public QueryRendererBuilder visitOrderedQuery(HqlParser.OrderedQueryContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
if (ctx.query() != null) {
builder.append(visit(ctx.query()));
} else if (ctx.queryExpression() != null) {
QueryRendererBuilder nested = QueryRenderer.builder();
nested.append(TOKEN_OPEN_PAREN);
nested.appendInline(visit(ctx.queryExpression()));
nested.append(TOKEN_CLOSE_PAREN);
builder.appendExpression(nested);
}
if (ctx.queryOrder() != null) {
builder.append(visit(ctx.queryOrder()));
}
return builder;
}
@Override
public QueryRendererBuilder visitFromQuery(HqlParser.FromQueryContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
if (!isSubquery(ctx) && ctx.selectClause() == null) {
QueryRendererBuilder countBuilder = QueryRenderer.builder();
countBuilder.append(TOKEN_SELECT_COUNT);
if (countProjection != null) {
countBuilder.append(new JpaQueryParsingToken(countProjection));
} else {
if (primaryFromAlias == null) {
countBuilder.append(TOKEN_DOUBLE_UNDERSCORE);
} else {
countBuilder.append(JpaQueryParsingToken.token(primaryFromAlias));
}
}
countBuilder.append(TOKEN_CLOSE_PAREN);
builder.appendExpression(countBuilder);
}
if (ctx.fromClause() != null) {
builder.appendExpression(visit(ctx.fromClause()));
}
if (ctx.whereClause() != null) {
builder.appendExpression(visit(ctx.whereClause()));
}
if (ctx.groupByClause() != null) {
builder.appendExpression(visit(ctx.groupByClause()));
}
if (ctx.havingClause() != null) {
builder.appendExpression(visit(ctx.havingClause()));
}
if (ctx.selectClause() != null) {
builder.appendExpression(visit(ctx.selectClause()));
}
return builder;
}
@Override
public QueryRendererBuilder visitFromRoot(HqlParser.FromRootContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
if (ctx.entityName() != null) {
builder.appendExpression(visit(ctx.entityName()));
if (ctx.variable() != null) {
builder.appendExpression(visit(ctx.variable()));
} else {
builder.append(TOKEN_AS);
builder.append(TOKEN_DOUBLE_UNDERSCORE);
}
} else if (ctx.subquery() != null) {
if (ctx.LATERAL() != null) {
builder.append(JpaQueryParsingToken.expression(ctx.LATERAL()));
}
QueryRendererBuilder nested = QueryRenderer.builder();
nested.append(TOKEN_OPEN_PAREN);
nested.appendInline(visit(ctx.subquery()));
nested.append(TOKEN_CLOSE_PAREN);
builder.appendExpression(nested);
if (ctx.variable() != null) {
builder.appendExpression(visit(ctx.variable()));
}
}
return builder;
}
@Override
public QueryRendererBuilder visitJoin(HqlParser.JoinContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.appendExpression(visit(ctx.joinType()));
builder.append(JpaQueryParsingToken.expression(ctx.JOIN()));
builder.appendExpression(visit(ctx.joinTarget()));
if (ctx.joinRestriction() != null) {
builder.appendExpression(visit(ctx.joinRestriction()));
}
return builder;
}
@Override
public QueryRendererBuilder visitSelectClause(HqlParser.SelectClauseContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.append(JpaQueryParsingToken.expression(ctx.SELECT()));
QueryRendererBuilder selectionListbuilder = visit(ctx.selectionList());
if (!isSubquery(ctx)) {
builder.append(TOKEN_COUNT_FUNC);
if (countProjection != null) {
builder.append(JpaQueryParsingToken.token(countProjection));
}
QueryRendererBuilder nested = QueryRenderer.builder();
if (ctx.DISTINCT() != null) {
nested.append(JpaQueryParsingToken.expression(ctx.DISTINCT()));
}
if (countProjection == null) {
if (ctx.DISTINCT() != null) {
List<JpaQueryParsingToken> countSelection = QueryTransformers
.filterCountSelection(selectionListbuilder.build().stream().toList());
if (countSelection.stream().anyMatch(hqlToken -> hqlToken.getToken().contains("new"))) {
// constructor
nested.append(new JpaQueryParsingToken(primaryFromAlias));
} else {
// keep all the select items to distinct against
nested.append(countSelection);
}
} else {
nested.append(new JpaQueryParsingToken(primaryFromAlias));
}
}
builder.appendInline(nested);
builder.append(TOKEN_CLOSE_PAREN);
} else {
if (ctx.DISTINCT() != null) {
builder.append(JpaQueryParsingToken.expression(ctx.DISTINCT()));
}
builder.append(selectionListbuilder);
}
return builder;
}
@Override
public QueryRendererBuilder visitQueryOrder(HqlParser.QueryOrderContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
if (ctx.limitClause() != null) {
builder.appendExpression(visit(ctx.limitClause()));
}
if (ctx.offsetClause() != null) {
builder.appendExpression(visit(ctx.offsetClause()));
}
if (ctx.fetchClause() != null) {
builder.appendExpression(visit(ctx.fetchClause()));
}
return builder;
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2024 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.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.lang.Nullable;
/**
* {@link ParsedQueryIntrospector} for HQL queries.
*
* @author Mark Paluch
*/
@SuppressWarnings({ "UnreachableCode", "ConstantValue" })
class HqlQueryIntrospector extends HqlBaseVisitor<Void> implements ParsedQueryIntrospector {
private final HqlQueryRenderer renderer = new HqlQueryRenderer();
private @Nullable String primaryFromAlias = null;
private @Nullable List<JpaQueryParsingToken> projection;
private boolean projectionProcessed;
private boolean hasConstructorExpression = false;
@Override
public String getAlias() {
return primaryFromAlias;
}
@Override
public List<JpaQueryParsingToken> getProjection() {
return projection == null ? Collections.emptyList() : projection;
}
@Override
public boolean hasConstructorExpression() {
return hasConstructorExpression;
}
@Override
public Void visitSelectClause(HqlParser.SelectClauseContext ctx) {
List<HqlParser.SelectionContext> selections = ctx.selectionList().selection();
List<JpaQueryParsingToken> selectItemTokens = new ArrayList<>(selections.size() * 2);
for (HqlParser.SelectionContext selection : selections) {
if (!selectItemTokens.isEmpty()) {
selectItemTokens.add(TOKEN_COMMA);
}
selectItemTokens.add(JpaQueryParsingToken.token(renderer.visitSelection(selection).build().render()));
}
if (!projectionProcessed) {
projection = selectItemTokens;
projectionProcessed = true;
}
return super.visitSelectClause(ctx);
}
@Override
public Void visitFromRoot(HqlParser.FromRootContext ctx) {
if (primaryFromAlias == null && ctx.variable() != null && !HqlQueryRenderer.isSubquery(ctx)) {
primaryFromAlias = (ctx.variable().reservedWord() != null ? ctx.variable().reservedWord()
: ctx.variable().identifier().reservedWord()).getText();
}
return null;
}
@Override
public Void visitInstantiation(HqlParser.InstantiationContext ctx) {
hasConstructorExpression = true;
return super.visitInstantiation(ctx);
}
}

View File

@@ -15,119 +15,30 @@
*/
package org.springframework.data.jpa.repository.query;
import java.util.List;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
/**
* Implements the {@code HQL} parsing operations of a {@link JpaQueryParserSupport} using the ANTLR-generated
* {@link HqlParser} and {@link HqlQueryTransformer}.
* Implements the {@code HQL} parsing operations of a {@link JpaQueryParser} using the ANTLR-generated {@link HqlParser}
* and {@link HqlSortedQueryTransformer}.
*
* @author Greg Turnquist
* @author Mark Paluch
* @since 3.1
*/
class HqlQueryParser extends JpaQueryParserSupport {
class HqlQueryParser extends JpaQueryParser {
HqlQueryParser(String query) {
super(query);
private HqlQueryParser(String query) {
super(parse(query, HqlLexer::new, HqlParser::new, HqlParser::start), new HqlQueryIntrospector(),
HqlSortedQueryTransformer::new, HqlCountQueryTransformer::new);
}
/**
* Convenience method to parse an HQL query. Will throw a {@link BadJpqlGrammarException} if the query is invalid.
* Parse a HQL query.
*
* @param query
* @return a parsed query, ready for postprocessing
* @return the query parser.
* @throws BadJpqlGrammarException
*/
public static ParserRuleContext parseQuery(String query) {
HqlLexer lexer = new HqlLexer(CharStreams.fromString(query));
HqlParser parser = new HqlParser(new CommonTokenStream(lexer));
configureParser(query, lexer, parser);
return parser.start();
public static HqlQueryParser parseQuery(String query) throws BadJpqlGrammarException {
return new HqlQueryParser(query);
}
/**
* Parse the query using {@link #parseQuery(String)}.
*
* @return a parsed query
*/
@Override
protected ParserRuleContext parse(String query) {
return parseQuery(query);
}
/**
* Use the {@link HqlQueryTransformer} to transform the original query into a query with the {@link Sort} applied.
*
* @param parsedQuery
* @param sort can be {@literal null}
* @return list of {@link JpaQueryParsingToken}s
*/
@Override
protected List<JpaQueryParsingToken> applySort(ParserRuleContext parsedQuery, Sort sort) {
return new HqlQueryTransformer(sort).visit(parsedQuery);
}
/**
* Use the {@link HqlQueryTransformer} to transform the original query into a count query.
*
* @param parsedQuery
* @param countProjection
* @return list of {@link JpaQueryParsingToken}s
*/
@Override
protected List<JpaQueryParsingToken> doCreateCountQuery(ParserRuleContext parsedQuery,
@Nullable String countProjection) {
return new HqlQueryTransformer(true, countProjection).visit(parsedQuery);
}
/**
* Run the parsed query through {@link HqlQueryTransformer} to find the primary FROM clause's alias.
*
* @param parsedQuery
* @return can be {@literal null}
*/
@Override
protected String doFindAlias(ParserRuleContext parsedQuery) {
HqlQueryTransformer transformVisitor = new HqlQueryTransformer();
transformVisitor.visit(parsedQuery);
return transformVisitor.getAlias();
}
/**
* Use {@link HqlQueryTransformer} to find the projection of the query.
*
* @param parsedQuery
* @return
*/
@Override
protected List<JpaQueryParsingToken> doFindProjection(ParserRuleContext parsedQuery) {
HqlQueryTransformer transformVisitor = new HqlQueryTransformer();
transformVisitor.visit(parsedQuery);
return transformVisitor.getProjection();
}
/**
* Use {@link HqlQueryTransformer} to detect if the query uses a {@code new com.example.Dto()} DTO constructor in the
* primary select clause.
*
* @param parsedQuery
* @return Guaranteed to be {@literal true} or {@literal false}.
*/
@Override
protected boolean doCheckForConstructor(ParserRuleContext parsedQuery) {
HqlQueryTransformer transformVisitor = new HqlQueryTransformer();
transformVisitor.visit(parsedQuery);
return transformVisitor.hasConstructorExpression();
}
}

View File

@@ -1,401 +0,0 @@
/*
* Copyright 2022-2024 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.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.antlr.v4.runtime.ParserRuleContext;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed HQL query.
*
* @author Greg Turnquist
* @author Christoph Strobl
* @since 3.1
*/
class HqlQueryTransformer extends HqlQueryRenderer {
// TODO: Separate input from result parameters, encapsulation...
private final Sort sort;
private final boolean countQuery;
private final @Nullable String countProjection;
private @Nullable String primaryFromAlias = null;
private List<JpaQueryParsingToken> projection = Collections.emptyList();
private boolean projectionProcessed;
private boolean hasConstructorExpression = false;
private JpaQueryTransformerSupport transformerSupport;
HqlQueryTransformer() {
this(Sort.unsorted(), false, null);
}
HqlQueryTransformer(Sort sort) {
this(sort, false, null);
}
HqlQueryTransformer(boolean countQuery, @Nullable String countProjection) {
this(Sort.unsorted(), countQuery, countProjection);
}
private HqlQueryTransformer(Sort sort, boolean countQuery, @Nullable String countProjection) {
Assert.notNull(sort, "Sort must not be null");
this.sort = sort;
this.countQuery = countQuery;
this.countProjection = countProjection;
this.transformerSupport = new JpaQueryTransformerSupport();
}
@Nullable
public String getAlias() {
return this.primaryFromAlias;
}
public List<JpaQueryParsingToken> getProjection() {
return this.projection;
}
public boolean hasConstructorExpression() {
return this.hasConstructorExpression;
}
/**
* Is this select clause a {@literal subquery}?
*
* @return boolean
*/
private static boolean isSubquery(ParserRuleContext ctx) {
if (ctx instanceof HqlParser.SubqueryContext || ctx instanceof HqlParser.CteContext) {
return true;
} else if (ctx instanceof HqlParser.SelectStatementContext) {
return false;
} else if (ctx instanceof HqlParser.InsertStatementContext) {
return false;
} else {
return isSubquery(ctx.getParent());
}
}
@Override
public List<JpaQueryParsingToken> visitOrderedQuery(HqlParser.OrderedQueryContext ctx) {
List<JpaQueryParsingToken> tokens = newArrayList();
if (ctx.query() != null) {
tokens.addAll(visit(ctx.query()));
} else if (ctx.queryExpression() != null) {
tokens.add(TOKEN_OPEN_PAREN);
tokens.addAll(visit(ctx.queryExpression()));
tokens.add(TOKEN_CLOSE_PAREN);
}
if (!countQuery && !isSubquery(ctx)) {
if (ctx.queryOrder() != null) {
tokens.addAll(visit(ctx.queryOrder()));
}
if (sort.isSorted()) {
if (ctx.queryOrder() != null) {
NOSPACE(tokens);
tokens.add(TOKEN_COMMA);
} else {
SPACE(tokens);
tokens.add(TOKEN_ORDER_BY);
}
tokens.addAll(transformerSupport.generateOrderByArguments(primaryFromAlias, sort));
}
} else {
if (ctx.queryOrder() != null) {
tokens.addAll(visit(ctx.queryOrder()));
}
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitFromQuery(HqlParser.FromQueryContext ctx) {
List<JpaQueryParsingToken> tokens = newArrayList();
if (countQuery && !isSubquery(ctx) && ctx.selectClause() == null) {
tokens.add(TOKEN_SELECT_COUNT);
if (countProjection != null) {
tokens.add(new JpaQueryParsingToken(countProjection));
} else {
tokens.add(new JpaQueryParsingToken(() -> primaryFromAlias, false));
}
tokens.add(TOKEN_CLOSE_PAREN);
}
if (ctx.fromClause() != null) {
tokens.addAll(visit(ctx.fromClause()));
}
if (ctx.whereClause() != null) {
tokens.addAll(visit(ctx.whereClause()));
}
if (ctx.groupByClause() != null) {
tokens.addAll(visit(ctx.groupByClause()));
}
if (ctx.havingClause() != null) {
tokens.addAll(visit(ctx.havingClause()));
}
if (ctx.selectClause() != null) {
tokens.addAll(visit(ctx.selectClause()));
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitQueryOrder(HqlParser.QueryOrderContext ctx) {
List<JpaQueryParsingToken> tokens = newArrayList();
if (!countQuery) {
tokens.addAll(visit(ctx.orderByClause()));
}
if (ctx.limitClause() != null) {
SPACE(tokens);
tokens.addAll(visit(ctx.limitClause()));
}
if (ctx.offsetClause() != null) {
tokens.addAll(visit(ctx.offsetClause()));
}
if (ctx.fetchClause() != null) {
tokens.addAll(visit(ctx.fetchClause()));
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitFromRoot(HqlParser.FromRootContext ctx) {
List<JpaQueryParsingToken> tokens = newArrayList();
if (ctx.entityName() != null) {
tokens.addAll(visit(ctx.entityName()));
if (ctx.variable() != null) {
tokens.addAll(visit(ctx.variable()));
if (primaryFromAlias == null && !isSubquery(ctx)) {
primaryFromAlias = tokens.get(tokens.size() - 1).getToken();
}
} else {
if (countQuery) {
tokens.add(TOKEN_AS);
tokens.add(TOKEN_DOUBLE_UNDERSCORE);
if (primaryFromAlias == null && !isSubquery(ctx)) {
primaryFromAlias = TOKEN_DOUBLE_UNDERSCORE.getToken();
}
}
}
} else if (ctx.subquery() != null) {
if (ctx.LATERAL() != null) {
tokens.add(new JpaQueryParsingToken(ctx.LATERAL()));
}
tokens.add(TOKEN_OPEN_PAREN);
tokens.addAll(visit(ctx.subquery()));
tokens.add(TOKEN_CLOSE_PAREN);
if (ctx.variable() != null) {
tokens.addAll(visit(ctx.variable()));
if (primaryFromAlias == null && !isSubquery(ctx)) {
primaryFromAlias = tokens.get(tokens.size() - 1).getToken();
}
}
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitJoin(HqlParser.JoinContext ctx) {
List<JpaQueryParsingToken> tokens = new ArrayList<>();
tokens.addAll(visit(ctx.joinType()));
tokens.add(new JpaQueryParsingToken(ctx.JOIN()));
if (!countQuery) {
if (ctx.FETCH() != null) {
tokens.add(new JpaQueryParsingToken(ctx.FETCH()));
}
}
tokens.addAll(visit(ctx.joinTarget()));
if (ctx.joinRestriction() != null) {
tokens.addAll(visit(ctx.joinRestriction()));
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitJoinPath(HqlParser.JoinPathContext ctx) {
List<JpaQueryParsingToken> tokens = super.visitJoinPath(ctx);
if (ctx.variable() != null) {
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitJoinSubquery(HqlParser.JoinSubqueryContext ctx) {
List<JpaQueryParsingToken> tokens = super.visitJoinSubquery(ctx);
if (ctx.variable() != null) {
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitAlias(HqlParser.AliasContext ctx) {
List<JpaQueryParsingToken> tokens = super.visitAlias(ctx);
if (primaryFromAlias == null && !isSubquery(ctx)) {
primaryFromAlias = tokens.get(tokens.size() - 1).getToken();
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitVariable(HqlParser.VariableContext ctx) {
List<JpaQueryParsingToken> tokens = super.visitVariable(ctx);
if (ctx.identifier() != null) {
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitSelectClause(HqlParser.SelectClauseContext ctx) {
List<JpaQueryParsingToken> tokens = newArrayList();
tokens.add(new JpaQueryParsingToken(ctx.SELECT()));
if (countQuery && !isSubquery(ctx)) {
tokens.add(TOKEN_COUNT_FUNC);
if (countProjection != null) {
tokens.add(new JpaQueryParsingToken(countProjection));
}
}
if (ctx.DISTINCT() != null) {
tokens.add(new JpaQueryParsingToken(ctx.DISTINCT()));
}
List<JpaQueryParsingToken> selectionListTokens = visit(ctx.selectionList());
if (countQuery && !isSubquery(ctx)) {
if (countProjection == null) {
if (ctx.DISTINCT() != null) {
List<JpaQueryParsingToken> countSelection = QueryTransformers.filterCountSelection(selectionListTokens);
if (countSelection.stream().anyMatch(hqlToken -> hqlToken.getToken().contains("new"))) {
// constructor
tokens.add(new JpaQueryParsingToken(() -> primaryFromAlias));
} else {
// keep all the select items to distinct against
tokens.addAll(countSelection);
}
} else {
tokens.add(new JpaQueryParsingToken(() -> primaryFromAlias));
}
}
NOSPACE(tokens);
tokens.add(TOKEN_CLOSE_PAREN);
} else {
tokens.addAll(selectionListTokens);
}
if (!projectionProcessed && !isSubquery(ctx)) {
projection = selectionListTokens;
projectionProcessed = true;
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitInstantiation(HqlParser.InstantiationContext ctx) {
hasConstructorExpression = true;
return super.visitInstantiation(ctx);
}
static <T> ArrayList<T> newArrayList() {
return new ArrayList<>();
}
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2022-2024 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.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.List;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed HQL query.
*
* @author Greg Turnquist
* @author Christoph Strobl
* @since 3.1
*/
@SuppressWarnings("ConstantValue")
class HqlSortedQueryTransformer extends HqlQueryRenderer {
private final JpaQueryTransformerSupport transformerSupport = new JpaQueryTransformerSupport();
private final Sort sort;
private final @Nullable String primaryFromAlias;
HqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias) {
Assert.notNull(sort, "Sort must not be null");
this.sort = sort;
this.primaryFromAlias = primaryFromAlias;
}
@Override
public QueryRendererBuilder visitOrderedQuery(HqlParser.OrderedQueryContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
if (ctx.query() != null) {
builder.append(visit(ctx.query()));
} else if (ctx.queryExpression() != null) {
builder.append(TOKEN_OPEN_PAREN);
builder.appendInline(visit(ctx.queryExpression()));
builder.append(TOKEN_CLOSE_PAREN);
}
if (!isSubquery(ctx)) {
if (ctx.queryOrder() != null) {
QueryRendererBuilder existingOrder = visit(ctx.queryOrder());
if (sort.isSorted()) {
builder.appendInline(existingOrder);
} else {
builder.append(existingOrder);
}
}
if (sort.isSorted()) {
List<JpaQueryParsingToken> sortBy = transformerSupport.orderBy(primaryFromAlias, sort);
if (ctx.queryOrder() != null) {
QueryRendererBuilder extension = QueryRenderer.builder().append(TOKEN_COMMA).append(sortBy);
builder.appendInline(extension);
} else {
builder.append(TOKEN_ORDER_BY);
builder.append(sortBy);
}
}
} else {
if (ctx.queryOrder() != null) {
builder.append(visit(ctx.queryOrder()));
}
}
return builder;
}
@Override
public QueryRendererBuilder visitJoinPath(HqlParser.JoinPathContext ctx) {
QueryRendererBuilder builder = super.visitJoinPath(ctx);
if (ctx.variable() != null) {
List<JpaQueryParsingToken> tokens = builder.build().stream().toList();
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
}
return builder;
}
@Override
public QueryRendererBuilder visitJoinSubquery(HqlParser.JoinSubqueryContext ctx) {
QueryRendererBuilder builder = super.visitJoinSubquery(ctx);
if (ctx.variable() != null) {
List<JpaQueryParsingToken> tokens = builder.build().stream().toList();
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
}
return builder;
}
@Override
public QueryRendererBuilder visitVariable(HqlParser.VariableContext ctx) {
QueryRendererBuilder builder = super.visitVariable(ctx);
if (ctx.identifier() != null) {
List<JpaQueryParsingToken> tokens = builder.build().stream().toList();
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
}
return builder;
}
}

View File

@@ -22,7 +22,7 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Implementation of {@link QueryEnhancer} to enhance JPA queries using a {@link JpaQueryParserSupport}.
* Implementation of {@link QueryEnhancer} to enhance JPA queries using a {@link JpaQueryParser}.
*
* @author Greg Turnquist
* @author Mark Paluch
@@ -34,22 +34,22 @@ import org.springframework.util.Assert;
class JpaQueryEnhancer implements QueryEnhancer {
private final DeclaredQuery query;
private final JpaQueryParserSupport queryParser;
private final JpaQueryParser queryParser;
/**
* Initialize with an {@link JpaQueryParserSupport}.
* Initialize with an {@link JpaQueryParser}.
*
* @param query
* @param queryParser
*/
private JpaQueryEnhancer(DeclaredQuery query, JpaQueryParserSupport queryParser) {
private JpaQueryEnhancer(DeclaredQuery query, JpaQueryParser queryParser) {
this.query = query;
this.queryParser = queryParser;
}
/**
* Factory method to create a {@link JpaQueryParserSupport} for {@link DeclaredQuery} using JPQL grammar.
* Factory method to create a {@link JpaQueryParser} for {@link DeclaredQuery} using JPQL grammar.
*
* @param query must not be {@literal null}.
* @return a new {@link JpaQueryEnhancer} using JPQL.
@@ -58,11 +58,11 @@ class JpaQueryEnhancer implements QueryEnhancer {
Assert.notNull(query, "DeclaredQuery must not be null!");
return new JpaQueryEnhancer(query, new JpqlQueryParser(query.getQueryString()));
return new JpaQueryEnhancer(query, JpqlQueryParser.parseQuery(query.getQueryString()));
}
/**
* Factory method to create a {@link JpaQueryParserSupport} for {@link DeclaredQuery} using HQL grammar.
* Factory method to create a {@link JpaQueryParser} for {@link DeclaredQuery} using HQL grammar.
*
* @param query must not be {@literal null}.
* @return a new {@link JpaQueryEnhancer} using HQL.
@@ -71,11 +71,11 @@ class JpaQueryEnhancer implements QueryEnhancer {
Assert.notNull(query, "DeclaredQuery must not be null!");
return new JpaQueryEnhancer(query, new HqlQueryParser(query.getQueryString()));
return new JpaQueryEnhancer(query, HqlQueryParser.parseQuery(query.getQueryString()));
}
/**
* Factory method to create a {@link JpaQueryParserSupport} for {@link DeclaredQuery} using EQL grammar.
* Factory method to create a {@link JpaQueryParser} for {@link DeclaredQuery} using EQL grammar.
*
* @param query must not be {@literal null}.
* @return a new {@link JpaQueryEnhancer} using EQL.
@@ -85,10 +85,10 @@ class JpaQueryEnhancer implements QueryEnhancer {
Assert.notNull(query, "DeclaredQuery must not be null!");
return new JpaQueryEnhancer(query, new EqlQueryParser(query.getQueryString()));
return new JpaQueryEnhancer(query, EqlQueryParser.parseQuery(query.getQueryString()));
}
protected JpaQueryParserSupport getQueryParsingStrategy() {
protected JpaQueryParser getQueryParsingStrategy() {
return queryParser;
}
@@ -116,8 +116,8 @@ class JpaQueryEnhancer implements QueryEnhancer {
}
/**
* Resolves the alias for the entity in the FROM clause from the JPA query. Since the {@link JpaQueryParserSupport}
* can already find the alias when generating sorted and count queries, this is mainly to serve test cases.
* Resolves the alias for the entity in the FROM clause from the JPA query. Since the {@link JpaQueryParser} can
* already find the alias when generating sorted and count queries, this is mainly to serve test cases.
*/
@Override
public String detectAlias() {
@@ -155,16 +155,16 @@ class JpaQueryEnhancer implements QueryEnhancer {
}
/**
* Looks up the projection of the JPA query. Since the {@link JpaQueryParserSupport} can already find the projection
* when generating sorted and count queries, this is mainly to serve test cases.
* Looks up the projection of the JPA query. Since the {@link JpaQueryParser} can already find the projection when
* generating sorted and count queries, this is mainly to serve test cases.
*/
@Override
public String getProjection() {
return queryParser.projection();
return queryParser.getProjection();
}
/**
* Since the {@link JpaQueryParserSupport} can already fully transform sorted and count queries by itself, this is a
* Since the {@link JpaQueryParser} can already fully transform sorted and count queries by itself, this is a
* placeholder method.
*
* @return empty set
@@ -175,7 +175,7 @@ class JpaQueryEnhancer implements QueryEnhancer {
}
/**
* Look up the {@link DeclaredQuery} from the {@link JpaQueryParserSupport}.
* Look up the {@link DeclaredQuery} from the {@link JpaQueryParser}.
*/
@Override
public DeclaredQuery getQuery() {

View File

@@ -24,7 +24,6 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -49,7 +48,6 @@ import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.StringUtils;
/**
@@ -100,7 +98,7 @@ public class JpaQueryMethod extends QueryMethod {
private final Lazy<Boolean> isCollectionQuery;
private final Lazy<Boolean> isProcedureQuery;
private final Lazy<JpaEntityMetadata<?>> entityMetadata;
private final Map<Class<? extends Annotation>, Optional<Annotation>> annotationCache;
private final Lazy<Optional<Meta>> metaAnnotation;
/**
* Creates a {@link JpaQueryMethod}.
@@ -142,7 +140,8 @@ public class JpaQueryMethod extends QueryMethod {
this.isCollectionQuery = Lazy.of(() -> super.isCollectionQuery() && !NATIVE_ARRAY_TYPES.contains(this.returnType));
this.isProcedureQuery = Lazy.of(() -> AnnotationUtils.findAnnotation(method, Procedure.class) != null);
this.entityMetadata = Lazy.of(() -> new DefaultJpaEntityMetadata<>(getDomainClass()));
this.annotationCache = new ConcurrentReferenceHashMap<>();
this.metaAnnotation = Lazy
.of(() -> Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Meta.class)));
Assert.isTrue(!(isModifyingQuery() && getParameters().hasSpecialParameter()),
() -> String.format("Modifying method must not contain %s", Parameters.TYPES));
@@ -165,7 +164,7 @@ public class JpaQueryMethod extends QueryMethod {
String annotatedQuery = getAnnotatedQuery();
if (!DeclaredQuery.of(annotatedQuery, this.isNativeQuery.get()).hasNamedParameter()) {
if (!DeclaredQuery.hasNamedParameter(annotatedQuery)) {
return;
}
@@ -201,13 +200,6 @@ public class JpaQueryMethod extends QueryMethod {
return modifying.getNullable() != null;
}
@SuppressWarnings("unchecked")
private <A extends Annotation> Optional<A> doFindAnnotation(Class<A> annotationType) {
return (Optional<A>) this.annotationCache.computeIfAbsent(annotationType,
it -> Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, it)));
}
/**
* Returns all {@link QueryHint}s annotated at this class. Note, that {@link QueryHints}
*
@@ -290,7 +282,7 @@ public class JpaQueryMethod extends QueryMethod {
*/
@Nullable
Meta getMetaAnnotation() {
return doFindAnnotation(Meta.class).orElse(null);
return metaAnnotation.get().orElse(null);
}
/**

View File

@@ -0,0 +1,142 @@
/*
* Copyright 2022-2024 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.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.Lexer;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.atn.PredictionMode;
import org.antlr.v4.runtime.tree.ParseTreeVisitor;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
/**
* Operations needed to parse a JPA query.
*
* @author Greg Turnquist
* @author Mark Paluch
* @since 3.1
*/
abstract class JpaQueryParser {
private final ParserRuleContext context;
private final ParsedQueryIntrospector introspector;
private final String projection;
private final BiFunction<Sort, String, ParseTreeVisitor<? extends Object>> sortFunction;
private final BiFunction<String, String, ParseTreeVisitor<? extends Object>> countQueryFunction;
JpaQueryParser(ParserRuleContext context, ParsedQueryIntrospector introspector,
@Nullable BiFunction<Sort, String, ParseTreeVisitor<? extends Object>> sortFunction,
@Nullable BiFunction<String, String, ParseTreeVisitor<? extends Object>> countQueryFunction) {
this.context = context;
this.introspector = introspector;
this.sortFunction = sortFunction;
this.countQueryFunction = countQueryFunction;
this.introspector.visit(context);
List<JpaQueryParsingToken> tokens = introspector.getProjection();
this.projection = tokens.isEmpty() ? "" : render(tokens);
}
static <P extends Parser> ParserRuleContext parse(String query, Function<CharStream, Lexer> lexerFactoryFunction,
Function<TokenStream, P> parserFactoryFunction, Function<P, ParserRuleContext> parseFunction) {
Lexer lexer = lexerFactoryFunction.apply(CharStreams.fromString(query));
P parser = parserFactoryFunction.apply(new CommonTokenStream(lexer));
configureParser(query, lexer, parser);
return parseFunction.apply(parser);
}
/**
* Generate a query using the original query with an {@literal order by} clause added (or amended) based upon the
* provider {@link Sort} parameter.
*
* @param sort can be {@literal null}
*/
String renderSortedQuery(Sort sort) {
return render(sortFunction.apply(sort, findAlias()).visit(context));
}
/**
* Generate a count-based query derived from the original query.
*
* @param countProjection
*/
String createCountQuery(@Nullable String countProjection) {
return render(countQueryFunction.apply(countProjection, findAlias()).visit(context));
}
/**
* Find the projection of the query.
*/
String getProjection() {
return this.projection;
}
/**
* Find the alias of the query's primary FROM clause
*
* @return can be {@literal null}
*/
@Nullable
String findAlias() {
return this.introspector.getAlias();
}
/**
* Discern if the query has a {@code new com.example.Dto()} DTO constructor in the select clause.
*
* @return Guaranteed to be {@literal true} or {@literal false}.
*/
boolean hasConstructorExpression() {
return this.introspector.hasConstructorExpression();
}
/**
* Apply common configuration (SLL prediction for performance, our own error listeners).
*
* @param query
* @param lexer
* @param parser
*/
static void configureParser(String query, Lexer lexer, Parser parser) {
BadJpqlGrammarErrorListener errorListener = new BadJpqlGrammarErrorListener(query);
lexer.removeErrorListeners();
lexer.addErrorListener(errorListener);
parser.getInterpreter().setPredictionMode(PredictionMode.SLL);
parser.removeErrorListeners();
parser.addErrorListener(errorListener);
}
}

View File

@@ -1,206 +0,0 @@
/*
* Copyright 2022-2024 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.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.List;
import org.antlr.v4.runtime.Lexer;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.atn.PredictionMode;
import org.springframework.data.domain.Sort;
import org.springframework.data.util.Lazy;
import org.springframework.lang.Nullable;
/**
* Operations needed to parse a JPA query.
*
* @author Greg Turnquist
* @author Mark Paluch
* @since 3.1
*/
abstract class JpaQueryParserSupport {
private final ParseState state;
JpaQueryParserSupport(String query) {
this.state = new ParseState(query);
}
/**
* Generate a query using the original query with an @literal order by} clause added (or amended) based upon the
* provider {@link Sort} parameter.
*
* @param sort can be {@literal null}
*/
String renderSortedQuery(Sort sort) {
try {
return render(applySort(state.getContext(), sort));
} catch (BadJpqlGrammarException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Generate a count-based query using the original query.
*
* @param countProjection
*/
String createCountQuery(@Nullable String countProjection) {
try {
return render(doCreateCountQuery(state.getContext(), countProjection));
} catch (BadJpqlGrammarException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Find the projection of the query.
*/
String projection() {
try {
List<JpaQueryParsingToken> tokens = doFindProjection(state.getContext());
return tokens.isEmpty() ? "" : render(tokens);
} catch (BadJpqlGrammarException e) {
return "";
}
}
/**
* Find the alias of the query's primary FROM clause
*
* @return can be {@literal null}
*/
@Nullable
String findAlias() {
try {
return doFindAlias(state.getContext());
} catch (BadJpqlGrammarException e) {
return null;
}
}
/**
* Discern if the query has a {@code new com.example.Dto()} DTO constructor in the select clause.
*
* @return Guaranteed to be {@literal true} or {@literal false}.
*/
boolean hasConstructorExpression() {
try {
return doCheckForConstructor(state.getContext());
} catch (BadJpqlGrammarException e) {
return false;
}
}
/**
* Parse the JPA query using its corresponding ANTLR parser.
*/
protected abstract ParserRuleContext parse(String query);
/**
* Apply common configuration (SLL prediction for performance, our own error listeners).
*
* @param query
* @param lexer
* @param parser
*/
static void configureParser(String query, Lexer lexer, Parser parser) {
BadJpqlGrammarErrorListener errorListener = new BadJpqlGrammarErrorListener(query);
lexer.removeErrorListeners();
lexer.addErrorListener(errorListener);
parser.getInterpreter().setPredictionMode(PredictionMode.SLL);
parser.removeErrorListeners();
parser.addErrorListener(errorListener);
}
/**
* Create a {@link JpaQueryParsingToken}-based query with an {@literal order by} applied/amended based upon the
* {@link Sort} parameter.
*
* @param parsedQuery
* @param sort can be {@literal null}
*/
protected abstract List<JpaQueryParsingToken> applySort(ParserRuleContext parsedQuery, Sort sort);
/**
* Create a {@link JpaQueryParsingToken}-based count query.
*
* @param parsedQuery
* @param countProjection
*/
protected abstract List<JpaQueryParsingToken> doCreateCountQuery(ParserRuleContext parsedQuery,
@Nullable String countProjection);
@Nullable
protected abstract String doFindAlias(ParserRuleContext parsedQuery);
/**
* Find the projection of the query's primary SELECT clause.
*
* @param parsedQuery
*/
protected abstract List<JpaQueryParsingToken> doFindProjection(ParserRuleContext parsedQuery);
protected abstract boolean doCheckForConstructor(ParserRuleContext parsedQuery);
/**
* Parser state capturing the lazily-parsed parser context.
*/
class ParseState {
private final Lazy<ParserRuleContext> parsedQuery;
private volatile @Nullable BadJpqlGrammarException error;
private final String query;
public ParseState(String query) {
this.query = query;
this.parsedQuery = Lazy.of(() -> parse(query));
}
public ParserRuleContext getContext() {
BadJpqlGrammarException error = this.error;
if (error != null) {
throw error;
}
try {
return parsedQuery.get();
} catch (BadJpqlGrammarException e) {
this.error = error = e;
throw error;
}
}
public String getQuery() {
return query;
}
}
}

View File

@@ -15,7 +15,7 @@
*/
package org.springframework.data.jpa.repository.query;
import java.util.List;
import java.util.Collection;
import java.util.function.Supplier;
import org.antlr.v4.runtime.Token;
@@ -33,102 +33,74 @@ class JpaQueryParsingToken {
/**
* Commonly use tokens.
*/
public static final JpaQueryParsingToken TOKEN_COMMA = new JpaQueryParsingToken(",");
public static final JpaQueryParsingToken TOKEN_DOT = new JpaQueryParsingToken(".", false);
public static final JpaQueryParsingToken TOKEN_EQUALS = new JpaQueryParsingToken("=");
public static final JpaQueryParsingToken TOKEN_OPEN_PAREN = new JpaQueryParsingToken("(", false);
public static final JpaQueryParsingToken TOKEN_CLOSE_PAREN = new JpaQueryParsingToken(")");
public static final JpaQueryParsingToken TOKEN_ORDER_BY = new JpaQueryParsingToken("order by");
public static final JpaQueryParsingToken TOKEN_LOWER_FUNC = new JpaQueryParsingToken("lower(", false);
public static final JpaQueryParsingToken TOKEN_SELECT_COUNT = new JpaQueryParsingToken("select count(", false);
public static final JpaQueryParsingToken TOKEN_PERCENT = new JpaQueryParsingToken("%");
public static final JpaQueryParsingToken TOKEN_COUNT_FUNC = new JpaQueryParsingToken("count(", false);
public static final JpaQueryParsingToken TOKEN_DOUBLE_PIPE = new JpaQueryParsingToken("||");
public static final JpaQueryParsingToken TOKEN_OPEN_SQUARE_BRACKET = new JpaQueryParsingToken("[", false);
public static final JpaQueryParsingToken TOKEN_NONE = JpaQueryParsingToken.token("");
public static final JpaQueryParsingToken TOKEN_COMMA = JpaQueryParsingToken.token(", ");
public static final JpaQueryParsingToken TOKEN_SPACE = JpaQueryParsingToken.token(" ");
public static final JpaQueryParsingToken TOKEN_DOT = JpaQueryParsingToken.token(".");
public static final JpaQueryParsingToken TOKEN_EQUALS = JpaQueryParsingToken.token(" = ");
public static final JpaQueryParsingToken TOKEN_OPEN_PAREN = JpaQueryParsingToken.token("(");
public static final JpaQueryParsingToken TOKEN_CLOSE_PAREN = JpaQueryParsingToken.token(")");
public static final JpaQueryParsingToken TOKEN_ORDER_BY = JpaQueryParsingToken.expression("order by");
public static final JpaQueryParsingToken TOKEN_LOWER_FUNC = new JpaQueryParsingToken("lower(");
public static final JpaQueryParsingToken TOKEN_SELECT_COUNT = JpaQueryParsingToken.token("select count(");
public static final JpaQueryParsingToken TOKEN_COUNT_FUNC = JpaQueryParsingToken.token("count(");
public static final JpaQueryParsingToken TOKEN_DOUBLE_PIPE = JpaQueryParsingToken.token(" || ");
public static final JpaQueryParsingToken TOKEN_OPEN_SQUARE_BRACKET = JpaQueryParsingToken.token("[");
public static final JpaQueryParsingToken TOKEN_CLOSE_SQUARE_BRACKET = new JpaQueryParsingToken("]");
public static final JpaQueryParsingToken TOKEN_COLON = new JpaQueryParsingToken(":", false);
public static final JpaQueryParsingToken TOKEN_QUESTION_MARK = new JpaQueryParsingToken("?", false);
public static final JpaQueryParsingToken TOKEN_OPEN_BRACE = new JpaQueryParsingToken("{", false);
public static final JpaQueryParsingToken TOKEN_COLON = JpaQueryParsingToken.token(":");
public static final JpaQueryParsingToken TOKEN_QUESTION_MARK = JpaQueryParsingToken.token("?");
public static final JpaQueryParsingToken TOKEN_OPEN_BRACE = JpaQueryParsingToken.token("{");
public static final JpaQueryParsingToken TOKEN_CLOSE_BRACE = new JpaQueryParsingToken("}");
public static final JpaQueryParsingToken TOKEN_CLOSE_SQUARE_BRACKET_BRACE = new JpaQueryParsingToken("]}");
public static final JpaQueryParsingToken TOKEN_CLOSE_PAREN_BRACE = new JpaQueryParsingToken(")}");
public static final JpaQueryParsingToken TOKEN_DOUBLE_UNDERSCORE = new JpaQueryParsingToken("__");
public static final JpaQueryParsingToken TOKEN_AS = new JpaQueryParsingToken("AS");
public static final JpaQueryParsingToken TOKEN_DESC = new JpaQueryParsingToken("desc", false);
public static final JpaQueryParsingToken TOKEN_ASC = new JpaQueryParsingToken("asc", false);
public static final JpaQueryParsingToken TOKEN_WITH = new JpaQueryParsingToken("WITH");
public static final JpaQueryParsingToken TOKEN_NOT = new JpaQueryParsingToken("NOT");
public static final JpaQueryParsingToken TOKEN_MATERIALIZED = new JpaQueryParsingToken("materialized");
public static final JpaQueryParsingToken TOKEN_NULLS = new JpaQueryParsingToken("NULLS");
public static final JpaQueryParsingToken TOKEN_FIRST = new JpaQueryParsingToken("FIRST");
public static final JpaQueryParsingToken TOKEN_LAST = new JpaQueryParsingToken("LAST");
public static final JpaQueryParsingToken TOKEN_DOUBLE_UNDERSCORE = JpaQueryParsingToken.token("__");
public static final JpaQueryParsingToken TOKEN_AS = JpaQueryParsingToken.expression("AS");
public static final JpaQueryParsingToken TOKEN_DESC = JpaQueryParsingToken.expression("desc");
public static final JpaQueryParsingToken TOKEN_ASC = JpaQueryParsingToken.expression("asc");
public static final JpaQueryParsingToken TOKEN_WITH = JpaQueryParsingToken.expression("WITH");
public static final JpaQueryParsingToken TOKEN_NOT = JpaQueryParsingToken.expression("NOT");
public static final JpaQueryParsingToken TOKEN_MATERIALIZED = JpaQueryParsingToken.expression("materialized");
public static final JpaQueryParsingToken TOKEN_NULLS = JpaQueryParsingToken.expression("NULLS");
public static final JpaQueryParsingToken TOKEN_FIRST = JpaQueryParsingToken.expression("FIRST");
public static final JpaQueryParsingToken TOKEN_LAST = JpaQueryParsingToken.expression("LAST");
/**
* The text value of the token.
*/
private final Supplier<String> token;
/**
* Space|NoSpace after token is rendered?
*/
private final boolean space;
JpaQueryParsingToken(Supplier<String> token, boolean space) {
this.token = token;
this.space = space;
}
JpaQueryParsingToken(String token, boolean space) {
this(() -> token, space);
}
JpaQueryParsingToken(Supplier<String> token) {
this(token, true);
}
private final String token;
JpaQueryParsingToken(String token) {
this(() -> token, true);
this.token = token;
}
JpaQueryParsingToken(TerminalNode node, boolean space) {
this(node.getText(), space);
public static JpaQueryParsingToken token(TerminalNode node) {
return token(node.getText());
}
JpaQueryParsingToken(TerminalNode node) {
this(node.getText());
public static JpaQueryParsingToken token(Token token) {
return token(token.getText());
}
JpaQueryParsingToken(Token token, boolean space) {
this(token.getText(), space);
static JpaQueryParsingToken token(String token) {
return new JpaQueryParsingToken(token);
}
JpaQueryParsingToken(Token token) {
this(token.getText(), true);
static JpaQueryParsingToken expression(String expression) {
return new JpaQueryExpression(expression);
}
public static JpaQueryParsingToken expression(Token token) {
return expression(token.getText());
}
public static JpaQueryParsingToken expression(TerminalNode node) {
return expression(node.getText());
}
public static JpaQueryParsingToken ventilated(Token op) {
return new JpaQueryParsingToken(" " + op.getText() + " ");
}
/**
* Extract the token's value from it's {@link Supplier}.
*/
String getToken() {
return this.token.get();
}
/**
* Should we render a space after the token?
*/
boolean getSpace() {
return this.space;
return token;
}
/**
@@ -147,43 +119,18 @@ class JpaQueryParsingToken {
}
/**
* Switch the last {@link JpaQueryParsingToken}'s spacing to {@literal true}.
* Render a list of {@link JpaQueryParsingToken}s into a string.
*
* @param tokens
* @return rendered string containing either a query or some subset of that query
*/
static void SPACE(List<JpaQueryParsingToken> tokens) {
static String render(Object tokens) {
if (!tokens.isEmpty()) {
int index = tokens.size() - 1;
JpaQueryParsingToken lastTokenWithSpacing = new JpaQueryParsingToken(tokens.get(index).token);
tokens.remove(index);
tokens.add(lastTokenWithSpacing);
if (tokens instanceof Collection tpr) {
return render(tpr);
}
}
/**
* Switch the last {@link JpaQueryParsingToken}'s spacing to {@literal false}.
*/
static void NOSPACE(List<JpaQueryParsingToken> tokens) {
if (!tokens.isEmpty()) {
int index = tokens.size() - 1;
JpaQueryParsingToken lastTokenWithNoSpacing = new JpaQueryParsingToken(tokens.get(index).token, false);
tokens.remove(index);
tokens.add(lastTokenWithNoSpacing);
}
}
/**
* Drop the last entry from the list of {@link JpaQueryParsingToken}s.
*/
static void CLIP(List<JpaQueryParsingToken> tokens) {
if (!tokens.isEmpty()) {
tokens.remove(tokens.size() - 1);
}
return ((QueryRenderer.QueryRendererBuilder) tokens).build().render();
}
/**
@@ -192,19 +139,31 @@ class JpaQueryParsingToken {
* @param tokens
* @return rendered string containing either a query or some subset of that query
*/
static String render(List<JpaQueryParsingToken> tokens) {
static String render(Collection<JpaQueryParsingToken> tokens) {
StringBuilder results = new StringBuilder();
tokens.forEach(token -> {
boolean previousExpression = false;
results.append(token.getToken());
for (JpaQueryParsingToken jpaQueryParsingToken : tokens) {
if (token.getSpace()) {
results.append(" ");
if (previousExpression) {
if (!results.isEmpty() && results.charAt(results.length() - 1) != ' ') {
results.append(' ');
}
}
});
return results.toString().trim();
previousExpression = jpaQueryParsingToken instanceof JpaQueryExpression;
results.append(jpaQueryParsingToken.getToken());
}
return results.toString();
}
static class JpaQueryExpression extends JpaQueryParsingToken {
JpaQueryExpression(String token) {
super(token);
}
}
}

View File

@@ -15,8 +15,9 @@ import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
/**
* Transformational operations needed to support either {@link HqlQueryTransformer} or {@link JpqlQueryTransformer}.
*
* Transformational operations needed to support either {@link HqlSortedQueryTransformer} or
* {@link JpqlSortedQueryTransformer}.
*
* @author Greg Turnquist
* @author Donghun Shin
* @since 3.1
@@ -29,11 +30,7 @@ class JpaQueryTransformerSupport {
+ "aliases used in the select clause; If you really want to use something other than that for sorting, please use "
+ "JpaSort.unsafe(…)";
private Set<String> projectionAliases;
JpaQueryTransformerSupport() {
this.projectionAliases = new HashSet<>();
}
private final Set<String> projectionAliases = new HashSet<>();
/**
* Register an {@literal alias} so it can later be evaluated when applying {@link Sort}s.
@@ -47,12 +44,12 @@ class JpaQueryTransformerSupport {
/**
* Using the primary {@literal FROM} clause's alias and a {@link Sort}, construct all the {@literal ORDER BY}
* arguments.
*
*
* @param primaryFromAlias
* @param sort
* @return
*/
List<JpaQueryParsingToken> generateOrderByArguments(String primaryFromAlias, Sort sort) {
List<JpaQueryParsingToken> orderBy(String primaryFromAlias, Sort sort) {
List<JpaQueryParsingToken> tokens = new ArrayList<>();
@@ -60,20 +57,27 @@ class JpaQueryTransformerSupport {
checkSortExpression(order);
if (order.isIgnoreCase()) {
tokens.add(TOKEN_LOWER_FUNC);
}
tokens.add(new JpaQueryParsingToken(() -> generateOrderByArgument(primaryFromAlias, order)));
StringBuilder builder = new StringBuilder();
if (order.isIgnoreCase()) {
NOSPACE(tokens);
tokens.add(TOKEN_CLOSE_PAREN);
builder.append(TOKEN_LOWER_FUNC.getToken());
}
tokens.add(order.isDescending() ? TOKEN_DESC : TOKEN_ASC);
tokens.add(TOKEN_COMMA);
builder.append(generateOrderByArgument(primaryFromAlias, order));
if (order.isIgnoreCase()) {
builder.append(TOKEN_CLOSE_PAREN);
}
builder.append(" ");
builder.append(order.isDescending() ? TOKEN_DESC : TOKEN_ASC);
if (!tokens.isEmpty()) {
tokens.add(TOKEN_COMMA);
}
tokens.add(JpaQueryParsingToken.token(builder.toString()));
});
CLIP(tokens);
return tokens;
}
@@ -98,7 +102,7 @@ class JpaQueryTransformerSupport {
/**
* Using the {@code primaryFromAlias} and the {@link org.springframework.data.domain.Sort.Order}, construct a suitable
* argument to be added to an {@literal ORDER BY} expression.
*
*
* @param primaryFromAlias
* @param order
* @return
@@ -120,7 +124,7 @@ class JpaQueryTransformerSupport {
* @param primaryFromAlias
* @return boolean whether or not to apply the primary FROM clause's alias as a prefix
*/
private boolean shouldPrefixWithAlias(Sort.Order order, String primaryFromAlias) {
private boolean shouldPrefixWithAlias(Sort.Order order, @Nullable String primaryFromAlias) {
// If there is no primary alias
if (ObjectUtils.isEmpty(primaryFromAlias)) {

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2022-2024 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.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.List;
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
import org.springframework.lang.Nullable;
/**
* An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed JPQL query into a
* {@code COUNT(…)} query.
*
* @author Greg Turnquist
* @author Mark Paluch
* @since 3.1
*/
@SuppressWarnings("ConstantValue")
class JpqlCountQueryTransformer extends JpqlQueryRenderer {
private final @Nullable String countProjection;
private final @Nullable String primaryFromAlias;
JpqlCountQueryTransformer(@Nullable String countProjection, @Nullable String primaryFromAlias) {
this.countProjection = countProjection;
this.primaryFromAlias = primaryFromAlias;
}
@Override
public QueryRenderer.QueryRendererBuilder visitSelect_statement(JpqlParser.Select_statementContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.appendExpression(visit(ctx.select_clause()));
builder.appendExpression(visit(ctx.from_clause()));
if (ctx.where_clause() != null) {
builder.appendExpression(visit(ctx.where_clause()));
}
if (ctx.groupby_clause() != null) {
builder.appendExpression(visit(ctx.groupby_clause()));
}
if (ctx.having_clause() != null) {
builder.appendExpression(visit(ctx.having_clause()));
}
return builder;
}
@Override
public QueryRendererBuilder visitSelect_clause(JpqlParser.Select_clauseContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.append(JpaQueryParsingToken.expression(ctx.SELECT()));
builder.append(TOKEN_COUNT_FUNC);
if (countProjection != null) {
builder.append(JpaQueryParsingToken.token(countProjection));
}
QueryRendererBuilder nested = QueryRenderer.builder();
if (ctx.DISTINCT() != null) {
nested.append(JpaQueryParsingToken.expression(ctx.DISTINCT()));
}
if (countProjection == null) {
if (ctx.DISTINCT() != null) {
QueryRendererBuilder selectionListbuilder = QueryRendererBuilder.concat(ctx.select_item(), this::visit,
TOKEN_COMMA);
List<JpaQueryParsingToken> countSelection = QueryTransformers
.filterCountSelection(selectionListbuilder.build().stream().toList());
if (countSelection.stream().anyMatch(jpqlToken -> jpqlToken.getToken().contains("new"))) {
// constructor
nested.append(new JpaQueryParsingToken(primaryFromAlias));
} else {
// keep all the select items to distinct against
nested.append(countSelection);
}
} else {
nested.append(new JpaQueryParsingToken(primaryFromAlias));
}
}
builder.appendInline(nested);
builder.append(TOKEN_CLOSE_PAREN);
return builder;
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2024 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.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.lang.Nullable;
/**
* {@link ParsedQueryIntrospector} for JPQL queries.
*
* @author Mark Paluch
*/
@SuppressWarnings({ "UnreachableCode", "ConstantValue" })
class JpqlQueryIntrospector extends JpqlBaseVisitor<Void> implements ParsedQueryIntrospector {
private final JpqlQueryRenderer renderer = new JpqlQueryRenderer();
private @Nullable String primaryFromAlias = null;
private @Nullable List<JpaQueryParsingToken> projection;
private boolean projectionProcessed;
private boolean hasConstructorExpression = false;
@Nullable
public String getAlias() {
return primaryFromAlias;
}
public List<JpaQueryParsingToken> getProjection() {
return projection == null ? Collections.emptyList() : projection;
}
public boolean hasConstructorExpression() {
return hasConstructorExpression;
}
@Override
public Void visitRange_variable_declaration(JpqlParser.Range_variable_declarationContext ctx) {
if (primaryFromAlias == null) {
primaryFromAlias = ctx.identification_variable() == null ? ctx.entity_name().getText()
: ctx.identification_variable().getText();
}
return null;
}
@Override
public Void visitSelect_clause(JpqlParser.Select_clauseContext ctx) {
List<JpqlParser.Select_itemContext> selections = ctx.select_item();
List<JpaQueryParsingToken> selectItemTokens = new ArrayList<>(selections.size() * 2);
for (JpqlParser.Select_itemContext selection : selections) {
if (!selectItemTokens.isEmpty()) {
selectItemTokens.add(TOKEN_COMMA);
}
selectItemTokens.add(JpaQueryParsingToken.token(renderer.visitSelect_item(selection).build().render()));
}
if (!projectionProcessed) {
projection = selectItemTokens;
projectionProcessed = true;
}
return super.visitSelect_clause(ctx);
}
@Override
public Void visitConstructor_expression(JpqlParser.Constructor_expressionContext ctx) {
hasConstructorExpression = true;
return super.visitConstructor_expression(ctx);
}
}

View File

@@ -15,120 +15,29 @@
*/
package org.springframework.data.jpa.repository.query;
import java.util.List;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
/**
* Implements the {@code JPQL} parsing operations of a {@link JpaQueryParserSupport} using the ANTLR-generated
* {@link JpqlParser} and {@link JpqlQueryTransformer}.
* Implements the {@code JPQL} parsing operations of a {@link JpaQueryParser} using the ANTLR-generated
* {@link JpqlParser} and {@link JpqlSortedQueryTransformer}.
*
* @author Greg Turnquist
* @author Mark Paluch
* @since 3.1
*/
class JpqlQueryParser extends JpaQueryParserSupport {
class JpqlQueryParser extends JpaQueryParser {
JpqlQueryParser(String query) {
super(query);
private JpqlQueryParser(String query) {
super(parse(query, JpqlLexer::new, JpqlParser::new, JpqlParser::start), new JpqlQueryIntrospector(),
JpqlSortedQueryTransformer::new, JpqlCountQueryTransformer::new);
}
/**
* Convenience method to parse a JPQL query. Will throw a {@link BadJpqlGrammarException} if the query is invalid.
* Parse a JPQL query.
*
* @param query
* @return a parsed query, ready for postprocessing
* @return the query parser.
* @throws BadJpqlGrammarException
*/
public static ParserRuleContext parseQuery(String query) {
JpqlLexer lexer = new JpqlLexer(CharStreams.fromString(query));
JpqlParser parser = new JpqlParser(new CommonTokenStream(lexer));
configureParser(query, lexer, parser);
return parser.start();
}
/**
* Parse the query using {@link #parseQuery(String)}.
*
* @return a parsed query
*/
@Override
protected ParserRuleContext parse(String query) {
return parseQuery(query);
}
/**
* Use the {@link JpqlQueryTransformer} to transform the original query into a query with the {@link Sort} applied.
*
* @param parsedQuery
* @param sort can be {@literal null}
* @return list of {@link JpaQueryParsingToken}s
*/
@Override
protected List<JpaQueryParsingToken> applySort(ParserRuleContext parsedQuery, Sort sort) {
return new JpqlQueryTransformer(sort).visit(parsedQuery);
}
/**
* Use the {@link JpqlQueryTransformer} to transform the original query into a count query.
*
* @param parsedQuery
* @param countProjection
* @return list of {@link JpaQueryParsingToken}s
*/
@Override
protected List<JpaQueryParsingToken> doCreateCountQuery(ParserRuleContext parsedQuery,
@Nullable String countProjection) {
return new JpqlQueryTransformer(true, countProjection).visit(parsedQuery);
}
/**
* Run the parsed query through {@link JpqlQueryTransformer} to find the primary FROM clause's alias.
*
* @param parsedQuery
* @return can be {@literal null}
*/
@Override
protected String doFindAlias(ParserRuleContext parsedQuery) {
JpqlQueryTransformer transformVisitor = new JpqlQueryTransformer();
transformVisitor.visit(parsedQuery);
return transformVisitor.getAlias();
}
/**
* Use {@link JpqlQueryTransformer} to find the projection of the query.
*
* @param parsedQuery
* @return
*/
@Override
protected List<JpaQueryParsingToken> doFindProjection(ParserRuleContext parsedQuery) {
JpqlQueryTransformer transformVisitor = new JpqlQueryTransformer();
transformVisitor.visit(parsedQuery);
return transformVisitor.getProjection();
}
/**
* Use {@link JpqlQueryTransformer} to detect if the query uses a {@code new com.example.Dto()} DTO constructor in the
* primary select clause.
*
* @param parsedQuery
* @return Guaranteed to be {@literal true} or {@literal false}.
*/
@Override
protected boolean doCheckForConstructor(ParserRuleContext parsedQuery) {
JpqlQueryTransformer transformVisitor = new JpqlQueryTransformer();
transformVisitor.visit(parsedQuery);
return transformVisitor.hasConstructorExpression();
public static JpqlQueryParser parseQuery(String query) throws BadJpqlGrammarException {
return new JpqlQueryParser(query);
}
}

View File

@@ -1,245 +0,0 @@
/*
* Copyright 2022-2024 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.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed JPQL query.
*
* @author Greg Turnquist
* @since 3.1
*/
class JpqlQueryTransformer extends JpqlQueryRenderer {
// TODO: Separate input from result parameters, encapsulation...
private final Sort sort;
private final boolean countQuery;
private final @Nullable String countProjection;
private @Nullable String primaryFromAlias = null;
private List<JpaQueryParsingToken> projection = Collections.emptyList();
private boolean projectionProcessed;
private boolean hasConstructorExpression = false;
private JpaQueryTransformerSupport transformerSupport;
JpqlQueryTransformer() {
this(Sort.unsorted(), false, null);
}
JpqlQueryTransformer(Sort sort) {
this(sort, false, null);
}
JpqlQueryTransformer(boolean countQuery, @Nullable String countProjection) {
this(Sort.unsorted(), countQuery, countProjection);
}
private JpqlQueryTransformer(Sort sort, boolean countQuery, @Nullable String countProjection) {
Assert.notNull(sort, "Sort must not be null");
this.sort = sort;
this.countQuery = countQuery;
this.countProjection = countProjection;
this.transformerSupport = new JpaQueryTransformerSupport();
}
@Nullable
public String getAlias() {
return this.primaryFromAlias;
}
public List<JpaQueryParsingToken> getProjection() {
return this.projection;
}
public boolean hasConstructorExpression() {
return this.hasConstructorExpression;
}
@Override
public List<JpaQueryParsingToken> visitSelect_statement(JpqlParser.Select_statementContext ctx) {
List<JpaQueryParsingToken> tokens = newArrayList();
tokens.addAll(visit(ctx.select_clause()));
tokens.addAll(visit(ctx.from_clause()));
if (ctx.where_clause() != null) {
tokens.addAll(visit(ctx.where_clause()));
}
if (ctx.groupby_clause() != null) {
tokens.addAll(visit(ctx.groupby_clause()));
}
if (ctx.having_clause() != null) {
tokens.addAll(visit(ctx.having_clause()));
}
if (!countQuery) {
if (ctx.orderby_clause() != null) {
tokens.addAll(visit(ctx.orderby_clause()));
}
if (sort.isSorted()) {
if (ctx.orderby_clause() != null) {
NOSPACE(tokens);
tokens.add(TOKEN_COMMA);
} else {
SPACE(tokens);
tokens.add(TOKEN_ORDER_BY);
}
tokens.addAll(transformerSupport.generateOrderByArguments(primaryFromAlias, sort));
}
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitSelect_clause(JpqlParser.Select_clauseContext ctx) {
List<JpaQueryParsingToken> tokens = newArrayList();
tokens.add(new JpaQueryParsingToken(ctx.SELECT()));
if (countQuery) {
tokens.add(TOKEN_COUNT_FUNC);
}
if (ctx.DISTINCT() != null) {
tokens.add(new JpaQueryParsingToken(ctx.DISTINCT()));
}
List<JpaQueryParsingToken> selectItemTokens = newArrayList();
ctx.select_item().forEach(selectItemContext -> {
selectItemTokens.addAll(visit(selectItemContext));
NOSPACE(selectItemTokens);
selectItemTokens.add(TOKEN_COMMA);
});
CLIP(selectItemTokens);
SPACE(selectItemTokens);
if (countQuery) {
if (countProjection != null) {
tokens.add(new JpaQueryParsingToken(countProjection));
} else {
if (ctx.DISTINCT() != null) {
List<JpaQueryParsingToken> countSelection = QueryTransformers.filterCountSelection(selectItemTokens);
if (countSelection.stream().anyMatch(jpqlToken -> jpqlToken.getToken().contains("new"))) {
// constructor
tokens.add(new JpaQueryParsingToken(() -> primaryFromAlias));
} else {
// keep all the select items to distinct against
tokens.addAll(countSelection);
}
} else {
tokens.add(new JpaQueryParsingToken(() -> primaryFromAlias));
}
}
NOSPACE(tokens);
tokens.add(TOKEN_CLOSE_PAREN);
} else {
tokens.addAll(selectItemTokens);
}
if (!projectionProcessed) {
projection = selectItemTokens;
projectionProcessed = true;
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitSelect_item(JpqlParser.Select_itemContext ctx) {
List<JpaQueryParsingToken> tokens = super.visitSelect_item(ctx);
if (ctx.result_variable() != null) {
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitRange_variable_declaration(JpqlParser.Range_variable_declarationContext ctx) {
List<JpaQueryParsingToken> tokens = newArrayList();
tokens.addAll(visit(ctx.entity_name()));
if (ctx.AS() != null) {
tokens.add(new JpaQueryParsingToken(ctx.AS()));
}
tokens.addAll(visit(ctx.identification_variable()));
if (primaryFromAlias == null) {
primaryFromAlias = tokens.get(tokens.size() - 1).getToken();
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitJoin(JpqlParser.JoinContext ctx) {
List<JpaQueryParsingToken> tokens = super.visitJoin(ctx);
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitConstructor_expression(JpqlParser.Constructor_expressionContext ctx) {
hasConstructorExpression = true;
return super.visitConstructor_expression(ctx);
}
private static <T> ArrayList<T> newArrayList() {
return new ArrayList<>();
}
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright 2022-2024 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.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.List;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed JPQL query by applying
* {@link Sort}.
*
* @author Greg Turnquist
* @author Mark Paluch
* @since 3.1
*/
@SuppressWarnings("ConstantValue")
class JpqlSortedQueryTransformer extends JpqlQueryRenderer {
private final JpaQueryTransformerSupport transformerSupport = new JpaQueryTransformerSupport();
private final Sort sort;
private final @Nullable String primaryFromAlias;
JpqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias) {
Assert.notNull(sort, "Sort must not be null");
this.sort = sort;
this.primaryFromAlias = primaryFromAlias;
}
@Override
public QueryRendererBuilder visitSelect_statement(JpqlParser.Select_statementContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
builder.appendExpression(visit(ctx.select_clause()));
builder.appendExpression(visit(ctx.from_clause()));
if (ctx.where_clause() != null) {
builder.appendExpression(visit(ctx.where_clause()));
}
if (ctx.groupby_clause() != null) {
builder.appendExpression(visit(ctx.groupby_clause()));
}
if (ctx.having_clause() != null) {
builder.appendExpression(visit(ctx.having_clause()));
}
doVisitOrderBy(builder, ctx);
return builder;
}
private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Select_statementContext ctx) {
if (ctx.orderby_clause() != null) {
QueryRendererBuilder existingOrder = visit(ctx.orderby_clause());
if (sort.isSorted()) {
builder.appendInline(existingOrder);
} else {
builder.append(existingOrder);
}
}
if (sort.isSorted()) {
List<JpaQueryParsingToken> sortBy = transformerSupport.orderBy(primaryFromAlias, sort);
if (ctx.orderby_clause() != null) {
QueryRendererBuilder extension = QueryRenderer.builder().append(TOKEN_COMMA).append(sortBy);
builder.appendInline(extension);
} else {
builder.append(TOKEN_ORDER_BY);
builder.append(sortBy);
}
}
}
@Override
public QueryRendererBuilder visitSelect_item(JpqlParser.Select_itemContext ctx) {
QueryRendererBuilder builder = super.visitSelect_item(ctx);
if (ctx.result_variable() != null) {
List<JpaQueryParsingToken> tokens = builder.build().stream().toList();
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
}
return builder;
}
@Override
public QueryRendererBuilder visitJoin(JpqlParser.JoinContext ctx) {
QueryRendererBuilder builder = super.visitJoin(ctx);
List<JpaQueryParsingToken> tokens = builder.build().stream().toList();
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
return builder;
}
}

View File

@@ -77,7 +77,8 @@ final class NamedQuery extends AbstractJpaQuery {
Query query = em.createNamedQuery(queryName);
String queryString = extractor.extractQueryString(query);
this.declaredQuery = DeclaredQuery.of(queryString, false);
// TODO: Detect whether a named query is a named one.
this.declaredQuery = DeclaredQuery.of(queryString, query != null && query.toString().contains("NativeQuery"));
boolean weNeedToCreateCountQuery = !namedCountQueryIsPresent && method.getParameters().hasLimitingParameters();
boolean cantExtractQuery = !extractor.canExtractQuery();

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2024 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.List;
import org.antlr.v4.runtime.tree.ParseTree;
import org.springframework.lang.Nullable;
/**
* Interface defining an introspector for String-queries providing details about the primary table alias, the primary
* selection projection and whether the query makes use of constructor expressions.
*
* @author Mark Paluch
*/
interface ParsedQueryIntrospector {
Void visit(ParseTree tree);
/**
* Primary table alias. Contains the first table name/table alias in case multiple tables are specified in the query.
*
* @return
*/
@Nullable
String getAlias();
List<JpaQueryParsingToken> getProjection();
boolean hasConstructorExpression();
}

View File

@@ -0,0 +1,464 @@
/*
* Copyright 2024 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.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
/**
* Abstraction to encapsulate query expressions and render a query.
* <p>
* Query rendering consists of multiple building blocks:
* <ul>
* <li>{@link JpaQueryParsingToken tokens} and
* {@link org.springframework.data.jpa.repository.query.JpaQueryParsingToken.JpaQueryExpression expression tokens}</li>
* <li>{@link QueryRenderer compositions} such as a composition of multiple tokens.</li>
* <li>{@link QueryRenderer expressions} that are individual parts such as {@code SELECT} or {@code ORDER BY …}</li>
* <li>{@link QueryRenderer inline expressions} such as composition of tokens and expressions such as function calls
* with parenthesis {@code SOME_FUNCTION(ARGS)}</li>
* </ul>
*
* @author Mark Paluch
*/
abstract class QueryRenderer {
/**
* Creates a QueryRenderer from a collection of {@link JpaQueryParsingToken}.
*
* @param tokens
* @return
*/
static QueryRenderer from(Collection<JpaQueryParsingToken> tokens) {
List<JpaQueryParsingToken> tokensToUse = new ArrayList<>(32);
tokensToUse.addAll(tokens);
return new TokenRenderer(tokensToUse);
}
/**
* Creates a new empty {@link QueryRenderer}.
*
* @return
*/
public static QueryRenderer empty() {
return EmptyQueryRenderer.INSTANCE;
}
/**
* Creates a new {@link QueryRendererBuilder}.
*
* @return
*/
static QueryRendererBuilder builder() {
return new QueryRendererBuilder();
}
/**
* @return the rendered query.
*/
abstract String render();
/**
* Append a {@link QueryRenderer} to create a composed renderer.
*
* @param renderer
* @return
*/
QueryRenderer append(QueryRenderer renderer) {
List<QueryRenderer> objects = new ArrayList<>(32);
objects.add(this);
objects.add(renderer);
return new CompositeRenderer(objects);
}
/**
* @return {@code true} if the query renderer represents an expression.
*/
public boolean isExpression() {
return false;
}
/**
* @return stream of tokens.
*/
public Stream<JpaQueryParsingToken> stream() {
return Stream.empty();
}
@Override
public String toString() {
return render();
}
/**
* Composed renderer consisting of one or more QueryRenderers.
*/
static class CompositeRenderer extends QueryRenderer {
private final List<QueryRenderer> nested;
CompositeRenderer(List<QueryRenderer> nested) {
this.nested = new ArrayList<>(nested);
}
@Override
String render() {
StringBuilder builder = new StringBuilder(2048);
boolean lastExpression = false;
for (QueryRenderer queryRenderer : nested) {
if (!builder.isEmpty() && builder.charAt(builder.length() - 1) != ' '
&& (lastExpression || queryRenderer.isExpression())) {
builder.append(' ');
}
builder.append(queryRenderer.render());
lastExpression = queryRenderer.isExpression();
}
return builder.toString();
}
@Override
QueryRenderer append(QueryRenderer renderer) {
nested.add(renderer);
return this;
}
@Override
public boolean isExpression() {
return !nested.isEmpty() && nested.get(nested.size() - 1).isExpression();
}
@Override
public Stream<JpaQueryParsingToken> stream() {
Stream<JpaQueryParsingToken> stream = Stream.empty();
for (QueryRenderer renderer : nested) {
stream = Stream.concat(stream, renderer.stream());
}
return stream;
}
}
/**
* Renderer using {@link JpaQueryParsingToken}.
*/
static class TokenRenderer extends QueryRenderer {
private final List<JpaQueryParsingToken> tokens;
TokenRenderer(List<JpaQueryParsingToken> tokens) {
this.tokens = tokens;
}
@Override
String render() {
return JpaQueryParsingToken.render(tokens);
}
@Override
QueryRenderer append(QueryRenderer renderer) {
if (renderer instanceof TokenRenderer tr) {
this.tokens.addAll(tr.tokens);
return this;
}
return super.append(renderer);
}
@Override
public boolean isExpression() {
return !tokens.isEmpty() && tokens.get(tokens.size() - 1) instanceof JpaQueryParsingToken.JpaQueryExpression;
}
@Override
public Stream<JpaQueryParsingToken> stream() {
return tokens.stream();
}
}
/**
* Builder for {@link QueryRenderer}.
*/
static class QueryRendererBuilder {
protected QueryRenderer current = QueryRenderer.empty();
/**
* Compose a {@link QueryRendererBuilder} from a collection of inline elements that can be mapped to
* {@link QueryRendererBuilder}.
*
* @param elements
* @param visitor
* @param separator
* @return
* @param <T>
*/
public static <T> QueryRendererBuilder concat(Collection<T> elements, Function<T, QueryRendererBuilder> visitor,
JpaQueryParsingToken separator) {
return concat(elements, visitor, QueryRendererBuilder::toInline, separator);
}
/**
* Compose a {@link QueryRendererBuilder} from a collection of expression elements that can be mapped to
* {@link QueryRendererBuilder}.
*
* @param elements
* @param visitor
* @param separator
* @return
* @param <T>
*/
public static <T> QueryRendererBuilder concatExpressions(Collection<T> elements,
Function<T, QueryRendererBuilder> visitor, JpaQueryParsingToken separator) {
return concat(elements, visitor, QueryRendererBuilder::toExpression, separator);
}
/**
* Compose a {@link QueryRendererBuilder} from a collection of elements that can be mapped to
* {@link QueryRendererBuilder}.
*
* @param elements
* @param visitor
* @param postProcess post-processing function to convert {@link QueryRendererBuilder} into {@link QueryRenderer}.
* @param separator
* @return
* @param <T>
*/
public static <T> QueryRendererBuilder concat(Collection<T> elements, Function<T, QueryRendererBuilder> visitor,
Function<QueryRendererBuilder, QueryRenderer> postProcess, JpaQueryParsingToken separator) {
QueryRendererBuilder builder = new QueryRendererBuilder();
for (T element : elements) {
if (!builder.isEmpty()) {
builder.append(separator);
}
builder.append(postProcess.apply(visitor.apply(element)));
}
return builder;
}
/**
* Create and initialize a QueryRendererBuilder from a {@link JpaQueryParsingToken}.
*
* @param token
* @return
*/
public static QueryRendererBuilder from(JpaQueryParsingToken token) {
return new QueryRendererBuilder().append(token);
}
/**
* Append a {@link JpaQueryParsingToken}.
*
* @param token
* @return {@code this} builder.
*/
QueryRendererBuilder append(JpaQueryParsingToken token) {
return append(List.of(token));
}
/**
* Append a collection of {@link JpaQueryParsingToken}.
*
* @param tokens
* @return {@code this} builder.
*/
QueryRendererBuilder append(Collection<JpaQueryParsingToken> tokens) {
return append(QueryRenderer.from(tokens));
}
/**
* Append a QueryRendererBuilder.
*
* @param builder
* @return {@code this} builder.
*/
QueryRendererBuilder append(QueryRendererBuilder builder) {
return append(builder.current);
}
/**
* Append a QueryRendererBuilder as expression.
*
* @param builder
* @return {@code this} builder.
*/
QueryRendererBuilder appendExpression(QueryRendererBuilder builder) {
return appendExpression(builder.current);
}
/**
* Append a QueryRendererBuilder as inline.
*
* @param builder
* @return {@code this} builder.
*/
QueryRendererBuilder appendInline(QueryRendererBuilder builder) {
return appendInline(builder.current);
}
/**
* Append a QueryRenderer.
*
* @param renderer
* @return {@code this} builder.
*/
QueryRendererBuilder append(QueryRenderer renderer) {
if (renderer instanceof EmptyQueryRenderer) {
return this;
}
current = current.append(renderer);
return this;
}
/**
* Append a QueryRenderer inline.
*
* @param renderer
* @return {@code this} builder.
*/
QueryRendererBuilder appendInline(QueryRenderer renderer) {
if (renderer instanceof EmptyQueryRenderer) {
return this;
}
current = current.append(!renderer.isExpression() ? renderer : new InlineRenderer(renderer));
return this;
}
/**
* Append a QueryRenderer as expression.
*
* @param renderer
* @return {@code this} builder.
*/
QueryRendererBuilder appendExpression(QueryRenderer renderer) {
if (renderer instanceof EmptyQueryRenderer) {
return this;
}
current = current.append(renderer.isExpression() ? renderer : new ExpressionRenderer(renderer));
return this;
}
/**
* Return whet the builder is empty.
*
* @return
*/
public boolean isEmpty() {
return current instanceof EmptyQueryRenderer;
}
public QueryRenderer build() {
return current;
}
@Override
public String toString() {
return current.render();
}
private QueryRenderer toExpression() {
if (current instanceof ExpressionRenderer) {
return current;
}
return new ExpressionRenderer(current);
}
public QueryRenderer toInline() {
return new InlineRenderer(current);
}
}
private static class InlineRenderer extends QueryRenderer {
private final QueryRenderer delegate;
private InlineRenderer(QueryRenderer delegate) {
this.delegate = delegate;
}
@Override
String render() {
return delegate.render();
}
@Override
public Stream<JpaQueryParsingToken> stream() {
return delegate.stream();
}
}
private static class ExpressionRenderer extends QueryRenderer {
private final QueryRenderer delegate;
private ExpressionRenderer(QueryRenderer delegate) {
this.delegate = delegate;
}
@Override
String render() {
return delegate.render();
}
@Override
public boolean isExpression() {
return true;
}
@Override
public Stream<JpaQueryParsingToken> stream() {
return delegate.stream();
}
}
private static class EmptyQueryRenderer extends QueryRenderer {
public static final QueryRenderer INSTANCE = new EmptyQueryRenderer();
@Override
String render() {
return "";
}
@Override
QueryRenderer append(QueryRenderer renderer) {
return renderer;
}
}
}

View File

@@ -51,6 +51,11 @@ class QueryTransformers {
skipNext = true;
continue;
}
if (!token.isA(TOKEN_COMMA) && token instanceof JpaQueryExpression) {
token = JpaQueryParsingToken.token(token.getToken());
}
target.add(token);
}

View File

@@ -60,8 +60,6 @@ class StringQuery implements DeclaredQuery {
private final String query;
private final List<ParameterBinding> bindings;
private final @Nullable String alias;
private final boolean hasConstructorExpression;
private final boolean containsPageableInSpel;
private final boolean usesJdbcStyleParameters;
private final boolean isNative;
@@ -87,8 +85,22 @@ class StringQuery implements DeclaredQuery {
this.usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters;
this.queryEnhancer = QueryEnhancerFactory.forQuery(this);
this.alias = this.queryEnhancer.detectAlias();
this.hasConstructorExpression = this.queryEnhancer.hasConstructorExpression();
}
// TODO: Conflict with eager JpaQueryMethod.assertParameterNamesInAnnotatedQuery validation that attempts parsing
// without pre-processing the query leaving #{#entityName} substitution to a later time.
public static boolean hasNamedParameter(String query) {
if (ObjectUtils.isEmpty(query)) {
return false;
}
List<ParameterBinding> parameterBindings = new ArrayList<>();
Metadata queryMeta = new Metadata();
ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query,
parameterBindings, queryMeta);
return parameterBindings.stream().anyMatch(b -> b.getIdentifier().hasName());
}
/**
@@ -134,17 +146,17 @@ class StringQuery implements DeclaredQuery {
@Override
@Nullable
public String getAlias() {
return alias;
return queryEnhancer.detectAlias();
}
@Override
public boolean hasConstructorExpression() {
return hasConstructorExpression;
return queryEnhancer.hasConstructorExpression();
}
@Override
public boolean isDefaultProjection() {
return getProjection().equalsIgnoreCase(alias);
return getProjection().equalsIgnoreCase(getAlias());
}
@Override
@@ -221,7 +233,7 @@ class StringQuery implements DeclaredQuery {
* Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings. Returns
* the cleaned up query.
*/
private String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query,
String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query,
List<ParameterBinding> bindings, Metadata queryMeta) {
int greatestParameterIndex = tryFindGreatestParameterIndexIn(query);
@@ -447,7 +459,7 @@ class StringQuery implements DeclaredQuery {
}
}
private static class Metadata {
static class Metadata {
private boolean usesJdbcStyleParameters = false;
}

View File

@@ -197,10 +197,10 @@ class EqlComplianceTests {
assertQuery("SELECT e.salary - 1000 FROM Employee e");
assertQuery("SELECT e.salary + 1000 FROM Employee e");
assertQuery("SELECT e.salary*2 FROM Employee e");
assertQuery("SELECT e.salary*2.0 FROM Employee e");
assertQuery("SELECT e.salary/2 FROM Employee e");
assertQuery("SELECT e.salary/2.0 FROM Employee e");
assertQuery("SELECT e.salary * 2 FROM Employee e");
assertQuery("SELECT e.salary * 2.0 FROM Employee e");
assertQuery("SELECT e.salary / 2 FROM Employee e");
assertQuery("SELECT e.salary / 2.0 FROM Employee e");
assertQuery("SELECT ABS(e.salary - e.manager.salary) FROM Employee e");
assertQuery(
"select e from Employee e where case e.firstName when 'Bob' then 'Robert' when 'Jill' then 'Gillian' else '' end = 'Robert'");
@@ -233,10 +233,10 @@ class EqlComplianceTests {
assertQuery("SELECT e FROM Employee e WHERE e.salary - 1000 > 0");
assertQuery("SELECT e FROM Employee e WHERE e.salary + 1000 > 0");
assertQuery("SELECT e FROM Employee e WHERE e.salary*2 > 0");
assertQuery("SELECT e FROM Employee e WHERE e.salary*2.0 > 0.0");
assertQuery("SELECT e FROM Employee e WHERE e.salary/2 > 0");
assertQuery("SELECT e FROM Employee e WHERE e.salary/2.0 > 0.0");
assertQuery("SELECT e FROM Employee e WHERE e.salary * 2 > 0");
assertQuery("SELECT e FROM Employee e WHERE e.salary * 2.0 > 0.0");
assertQuery("SELECT e FROM Employee e WHERE e.salary / 2 > 0");
assertQuery("SELECT e FROM Employee e WHERE e.salary / 2.0 > 0.0");
assertQuery("SELECT e FROM Employee e WHERE ABS(e.salary - e.manager.salary) > 0");
assertQuery("SELECT e FROM Employee e WHERE COALESCE(e.salary, 0) > 0");
assertQuery("SELECT e FROM Employee e WHERE CONCAT(e.firstName, ' ', e.lastName) = 'Bilbo'");
@@ -261,10 +261,10 @@ class EqlComplianceTests {
assertQuery("SELECT e FROM Employee e ORDER BY e.salary - 1000");
assertQuery("SELECT e FROM Employee e ORDER BY e.salary + 1000");
assertQuery("SELECT e FROM Employee e ORDER BY e.salary*2");
assertQuery("SELECT e FROM Employee e ORDER BY e.salary*2.0");
assertQuery("SELECT e FROM Employee e ORDER BY e.salary/2");
assertQuery("SELECT e FROM Employee e ORDER BY e.salary/2.0");
assertQuery("SELECT e FROM Employee e ORDER BY e.salary * 2");
assertQuery("SELECT e FROM Employee e ORDER BY e.salary * 2.0");
assertQuery("SELECT e FROM Employee e ORDER BY e.salary / 2");
assertQuery("SELECT e FROM Employee e ORDER BY e.salary / 2.0");
assertQuery("SELECT e FROM Employee e ORDER BY ABS(e.salary - e.manager.salary)");
assertQuery("SELECT e FROM Employee e ORDER BY COALESCE(e.salary, 0)");
assertQuery("SELECT e FROM Employee e ORDER BY CONCAT(e.firstName, ' ', e.lastName)");
@@ -290,10 +290,10 @@ class EqlComplianceTests {
assertQuery("SELECT e FROM Employee e GROUP BY e.salary - 1000");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary + 1000");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary*2");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary*2.0");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary/2");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary/2.0");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary * 2");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary * 2.0");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary / 2");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary / 2.0");
assertQuery("SELECT e FROM Employee e GROUP BY ABS(e.salary - e.manager.salary)");
assertQuery("SELECT e FROM Employee e GROUP BY COALESCE(e.salary, 0)");
assertQuery("SELECT e FROM Employee e GROUP BY CONCAT(e.firstName, ' ', e.lastName)");
@@ -319,10 +319,10 @@ class EqlComplianceTests {
assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING e.salary - 1000 > 0");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING e.salary + 1000 > 0");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING e.salary*2 > 0");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING e.salary*2.0 > 0.0");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING e.salary/2 > 0");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING e.salary/2.0 > 0.0");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING e.salary * 2 > 0");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING e.salary * 2.0 > 0.0");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING e.salary / 2 > 0");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING e.salary / 2.0 > 0.0");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING ABS(e.salary - e.manager.salary) > 0");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING COALESCE(e.salary, 0) > 0");
assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING CONCAT(e.firstName, ' ', e.lastName) = 'Bilbo'");

View File

@@ -295,7 +295,7 @@ class EqlQueryRendererTests {
assertQuery("""
SELECT b.name, b.ISBN
FROM Order o JOIN TREAT(o.product AS Book) b
""");
""");
}
@Test
@@ -304,7 +304,7 @@ class EqlQueryRendererTests {
assertQuery("""
SELECT e FROM Employee e JOIN TREAT(e.projects AS LargeProject) lp
WHERE lp.budget > 1000
""");
""");
}
/**
@@ -319,7 +319,7 @@ class EqlQueryRendererTests {
WHERE TREAT(p AS LargeProject).budget > 1000
OR TREAT(p AS SmallProject).name LIKE 'Persist%'
OR p.description LIKE "cost overrun"
""");
""");
}
@Test
@@ -330,7 +330,7 @@ class EqlQueryRendererTests {
WHERE TREAT(p AS LargeProject).budget > 1000
OR TREAT(p AS SmallProject).name LIKE 'Persist%'
OR p.description LIKE 'cost overrun'
""");
""");
}
@Test
@@ -340,7 +340,7 @@ class EqlQueryRendererTests {
SELECT e FROM Employee e
WHERE TREAT(e AS Exempt).vacationDays > 10
OR TREAT(e AS Contractor).hours > 100
""");
""");
}
@Test
@@ -404,7 +404,7 @@ class EqlQueryRendererTests {
WHERE emp.salary > ALL (SELECT m.salary
FROM Manager m
WHERE m.department = emp.department)
""");
""");
}
@Test
@@ -416,7 +416,7 @@ class EqlQueryRendererTests {
WHERE EXISTS (SELECT spouseEmp
FROM Employee spouseEmp
WHERE spouseEmp = emp.spouse)
""");
""");
}
@Test
@@ -435,7 +435,7 @@ class EqlQueryRendererTests {
assertQuery("""
SELECT goodCustomer
FROM Customer goodCustomer
WHERE goodCustomer.balanceOwed < (SELECT AVG(c.balanceOwed)/2.0 FROM Customer c)
WHERE goodCustomer.balanceOwed < (SELECT AVG(c.balanceOwed) / 2.0 FROM Customer c)
""");
}
@@ -480,11 +480,11 @@ class EqlQueryRendererTests {
assertQuery("""
UPDATE Employee e
SET e.salary =
CASE WHEN e.rating = 1 THEN e.salary*1.1
WHEN e.rating = 2 THEN e.salary*1.05
ELSE e.salary*1.01
CASE WHEN e.rating = 1 THEN e.salary * 1.1
WHEN e.rating = 2 THEN e.salary * 1.05
ELSE e.salary * 1.01
END
""");
""");
}
@Test
@@ -493,11 +493,11 @@ class EqlQueryRendererTests {
assertQuery("""
UPDATE Employee e
SET e.salary =
CASE e.rating WHEN 1 THEN e.salary*1.1
WHEN 2 THEN e.salary*1.05
ELSE e.salary*1.01
CASE e.rating WHEN 1 THEN e.salary * 1.1
WHEN 2 THEN e.salary * 1.05
ELSE e.salary * 1.01
END
""");
""");
}
@Test
@@ -537,7 +537,7 @@ class EqlQueryRendererTests {
SELECT e
FROM Employee e
WHERE TYPE(e) IN (Exempt, Contractor)
""");
""");
}
@Test
@@ -734,7 +734,7 @@ class EqlQueryRendererTests {
void orderByThatMatchesAllSelectAliasesShouldWork() {
assertQuery("""
SELECT o.quantity, o.cost*1.08 AS taxedCost, a.zipcode
SELECT o.quantity, o.cost * 1.08 AS taxedCost, a.zipcode
FROM Customer c JOIN c.orders o JOIN c.address a
WHERE a.state = 'CA' AND a.county = 'Santa Clara'
ORDER BY o.quantity, taxedCost, a.zipcode
@@ -967,11 +967,26 @@ class EqlQueryRendererTests {
assertQuery("select e from Employee e where e.firstName != :name");
}
@Test
void regexShouldWork() {
assertQuery("select e from Employee e where e.lastName REGEXP '^Dr\\.*'");
}
@Test // GH-3092
void dateAndFromShouldBeValidNames() {
assertQuery("SELECT e FROM Entity e WHERE e.embeddedId.date BETWEEN :from AND :to");
}
@Test
void betweenStrings() {
assertQuery("SELECT e FROM Entity e WHERE e.embeddedId.date NOT BETWEEN 'a' AND 'b'");
}
@Test
void betweenDates() {
assertQuery("SELECT e FROM Entity e WHERE e.embeddedId.date BETWEEN CURRENT_DATE AND CURRENT_TIME");
}
@Test // GH-3092
void timeShouldBeAValidParameterName() {
assertQuery("""
@@ -1004,7 +1019,7 @@ class EqlQueryRendererTests {
@ParameterizedTest // GH-3342
@ValueSource(strings = { "select 1 from User u", "select -1 from User u", "select +1 from User u",
"select +1*-100 from User u", "select count(u)*-0.7f from User u",
"select +1 * -100 from User u", "select count(u) * -0.7f from User u",
"select count(oi) + (-100) as perc from StockOrderItem oi",
"select p from Payment p where length(p.cardNumber) between +16 and -20" })
void signedLiteralShouldWork(String query) {
@@ -1012,7 +1027,7 @@ class EqlQueryRendererTests {
}
@ParameterizedTest // GH-3342
@ValueSource(strings = { "select -count(u) from User u", "select +1*(-count(u)) from User u" })
@ValueSource(strings = { "select -count(u) from User u", "select +1 * (-count(u)) from User u" })
void signedExpressionsShouldWork(String query) {
assertQuery(query);
}

View File

@@ -188,7 +188,7 @@ class EqlQueryTransformerTests {
assertThat(alias("select u from User u where not exists (select u2 from User u2)")).isEqualTo("u");
assertThat(alias(
"select u from User u where not exists (select u2 from User u2 where not exists (select u3 from User u3))"))
.isEqualTo("u");
.isEqualTo("u");
}
@Test // GH-2557
@@ -349,7 +349,7 @@ class EqlQueryTransformerTests {
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();
.isTrue();
}
@Test // DATAJPA-938
@@ -441,7 +441,7 @@ class EqlQueryTransformerTests {
String query = "SELECT AVG(m.price) AS m.avg FROM Magazine m";
Sort sort = Sort.by("m.avg");
assertThatIllegalArgumentException().isThrownBy(() -> createQueryFor(query, sort));
assertThatExceptionOfType(BadJpqlGrammarException.class).isThrownBy(() -> createQueryFor(query, sort));
}
@Test // DATAJPA-965, DATAJPA-970, GH-2863
@@ -456,8 +456,10 @@ class EqlQueryTransformerTests {
@Test // DATAJPA-1506
void detectsAliasWithGroupAndOrderBy() {
assertThat(alias("select * from User group by name")).isNull();
assertThat(alias("select * from User order by name")).isNull();
assertThatExceptionOfType(BadJpqlGrammarException.class)
.isThrownBy(() -> alias("select * from User group by name"));
assertThatExceptionOfType(BadJpqlGrammarException.class)
.isThrownBy(() -> alias("select * from User order by name"));
assertThat(alias("select u from User u group by name")).isEqualTo("u");
assertThat(alias("select u from User u order by name")).isEqualTo("u");
}
@@ -559,8 +561,10 @@ class EqlQueryTransformerTests {
@Test
void detectsAliasWithGroupAndOrderByWithLineBreaks() {
assertThat(alias("select * from User group\nby name")).isNull();
assertThat(alias("select * from User order\nby name")).isNull();
assertThatExceptionOfType(BadJpqlGrammarException.class)
.isThrownBy(() -> alias("select * from User group\nby name"));
assertThatExceptionOfType(BadJpqlGrammarException.class)
.isThrownBy(() -> alias("select * from User order\nby name"));
assertThat(alias("select u from User u group\nby name")).isEqualTo("u");
assertThat(alias("select u from User u order\nby name")).isEqualTo("u");
assertThat(alias("select u from User\nu\norder \n by name")).isEqualTo("u");
@@ -583,7 +587,8 @@ class EqlQueryTransformerTests {
// 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(projection("select * from (select x from y)")).isNotEqualTo("*");
assertThatExceptionOfType(BadJpqlGrammarException.class)
.isThrownBy(() -> projection("select * from (select x from y)"));
}
@Test // DATAJPA-1696
@@ -620,11 +625,11 @@ class EqlQueryTransformerTests {
assertCountQuery("select distinct 1 as x from Employee e", "select count(distinct 1) from Employee e");
assertCountQuery("SELECT DISTINCT abc AS x FROM T t", "SELECT count(DISTINCT abc) FROM T t");
assertCountQuery("select distinct a as x, b as y from Employee e", "select count(distinct a , b) from Employee e");
assertCountQuery("select distinct a as x, b as y from Employee e", "select count(distinct a, b) from Employee e");
assertCountQuery("select distinct sum(amount) as x from Employee e GROUP BY n",
"select count(distinct sum(amount)) from Employee e GROUP BY n");
assertCountQuery("select distinct a, b, sum(amount) as c, d from Employee e GROUP BY n",
"select count(distinct a, b, sum(amount) , d) from Employee e GROUP BY n");
"select count(distinct a, b, sum(amount), d) from Employee e GROUP BY n");
assertCountQuery("select distinct a, count(b) as c from Employee e GROUP BY n",
"select count(distinct a, count(b)) from Employee e GROUP BY n");
}
@@ -660,14 +665,14 @@ class EqlQueryTransformerTests {
assertThat(
createCountQueryFor("SELECT t FROM mytable t WHERE nr = :number AND kon = :kon AND datum >= '2019-01-01'"))
.isEqualTo("SELECT count(t) FROM mytable t WHERE nr = :number AND kon = :kon AND datum >= '2019-01-01'");
.isEqualTo("SELECT count(t) FROM mytable t WHERE nr = :number AND kon = :kon AND datum >= '2019-01-01'");
assertThat(createCountQueryFor("select s FROM users_statuses s WHERE (user_created_at BETWEEN $1 AND $2)"))
.isEqualTo("select count(s) FROM users_statuses s WHERE (user_created_at BETWEEN $1 AND $2)");
assertThat(
createCountQueryFor("SELECT us FROM users_statuses us WHERE (user_created_at BETWEEN :fromDate AND :toDate)"))
.isEqualTo("SELECT count(us) FROM users_statuses us WHERE (user_created_at BETWEEN :fromDate AND :toDate)");
.isEqualTo("SELECT count(us) FROM users_statuses us WHERE (user_created_at BETWEEN :fromDate AND :toDate)");
}
@Test // GH-2496, GH-2522, GH-2537, GH-2045

View File

@@ -16,7 +16,10 @@
package org.springframework.data.jpa.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -32,13 +35,38 @@ class EqlSpecificationTests {
private static final String SPEC_FAULT = "Disabled due to spec fault> ";
private static String parseWithoutChanges(String query) {
EqlLexer lexer = new EqlLexer(CharStreams.fromString(query));
EqlParser parser = new EqlParser(new CommonTokenStream(lexer));
parser.addErrorListener(new BadJpqlGrammarErrorListener(query));
EqlParser.StartContext parsedQuery = parser.start();
return render(new EqlQueryRenderer().visit(parsedQuery));
}
private void assertQuery(String query) {
String slimmedDownQuery = reduceWhitespace(query);
assertThat(parseWithoutChanges(slimmedDownQuery)).isEqualTo(slimmedDownQuery);
}
private String reduceWhitespace(String original) {
return original //
.replaceAll("[ \\t\\n]{1,}", " ") //
.trim();
}
/**
* @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#example
*/
@Test
void joinExample1() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT o
FROM Order AS o JOIN o.lineItems AS l
WHERE l.shipped = FALSE
@@ -52,7 +80,7 @@ class EqlSpecificationTests {
@Test
void joinExample2() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT o
FROM Order o JOIN o.lineItems l JOIN l.product p
WHERE p.productType = 'office_supplies'
@@ -65,12 +93,12 @@ class EqlSpecificationTests {
@Test
void rangeVariableDeclarations() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT o1
FROM Order o1, Order o2
WHERE o1.quantity > o2.quantity AND
o2.customer.lastname = 'Smith' AND
o2.customer.firstname= 'John'
o2.customer.firstname = 'John'
""");
}
@@ -80,7 +108,7 @@ class EqlSpecificationTests {
@Test
void pathExpressionsExample1() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT i.name, VALUE(p)
FROM Item i JOIN i.photos p
WHERE KEY(p) LIKE '%egret'
@@ -93,7 +121,7 @@ class EqlSpecificationTests {
@Test
void pathExpressionsExample2() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT i.name, p
FROM Item i JOIN i.photos p
WHERE KEY(p) LIKE '%egret'
@@ -106,7 +134,7 @@ class EqlSpecificationTests {
@Test
void pathExpressionsExample3() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT p.vendor
FROM Employee e JOIN e.contactInfo.phones p
""");
@@ -118,7 +146,7 @@ class EqlSpecificationTests {
@Test
void pathExpressionsExample4() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT p.vendor
FROM Employee e JOIN e.contactInfo c JOIN c.phones p
WHERE e.contactInfo.address.zipcode = '95054'
@@ -128,7 +156,7 @@ class EqlSpecificationTests {
@Test
void pathExpressionSyntaxExample1() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT l.product
FROM Order AS o JOIN o.lineItems l
""");
@@ -137,7 +165,7 @@ class EqlSpecificationTests {
@Test
void joinsExample1() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT c FROM Customer c, Employee e WHERE c.hatsize = e.shoesize
""");
}
@@ -145,7 +173,7 @@ class EqlSpecificationTests {
@Test
void joinsExample2() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT c FROM Customer c JOIN c.orders o WHERE c.status = 1
""");
}
@@ -153,7 +181,7 @@ class EqlSpecificationTests {
@Test
void joinsInnerExample() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT c FROM Customer c INNER JOIN c.orders o WHERE c.status = 1
""");
}
@@ -161,7 +189,7 @@ class EqlSpecificationTests {
@Test
void joinsInExample() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT OBJECT(c) FROM Customer c, IN(c.orders) o WHERE c.status = 1
""");
}
@@ -169,7 +197,7 @@ class EqlSpecificationTests {
@Test
void doubleJoinExample() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT p.vendor
FROM Employee e JOIN e.contactInfo c JOIN c.phones p
WHERE c.address.zipcode = '95054'
@@ -179,7 +207,7 @@ class EqlSpecificationTests {
@Test
void leftJoinExample() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT s.name, COUNT(p)
FROM Suppliers s LEFT JOIN s.products p
GROUP BY s.name
@@ -189,7 +217,7 @@ class EqlSpecificationTests {
@Test
void leftJoinOnExample() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT s.name, COUNT(p)
FROM Suppliers s LEFT JOIN s.products p
ON p.status = 'inStock'
@@ -200,7 +228,7 @@ class EqlSpecificationTests {
@Test
void leftJoinWhereExample() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT s.name, COUNT(p)
FROM Suppliers s LEFT JOIN s.products p
WHERE p.status = 'inStock'
@@ -211,7 +239,7 @@ class EqlSpecificationTests {
@Test
void leftJoinFetchExample() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT d
FROM Department d LEFT JOIN FETCH d.employees
WHERE d.deptno = 1
@@ -221,7 +249,7 @@ class EqlSpecificationTests {
@Test
void collectionMemberExample() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT o
FROM Order o JOIN o.lineItems l
WHERE l.product.productType = 'office_supplies'
@@ -231,7 +259,7 @@ class EqlSpecificationTests {
@Test
void collectionMemberInExample() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT o
FROM Order o, IN(o.lineItems) l
WHERE l.product.productType = 'office_supplies'
@@ -241,7 +269,7 @@ class EqlSpecificationTests {
@Test
void fromClauseExample() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT o
FROM Order AS o JOIN o.lineItems l JOIN l.product p
""");
@@ -250,7 +278,7 @@ class EqlSpecificationTests {
@Test
void fromClauseDowncastingExample1() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT b.name, b.ISBN
FROM Order o JOIN TREAT(o.product AS Book) b
""");
@@ -259,7 +287,7 @@ class EqlSpecificationTests {
@Test
void fromClauseDowncastingExample2() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT e FROM Employee e JOIN TREAT(e.projects AS LargeProject) lp
WHERE lp.budget > 1000
""");
@@ -272,7 +300,7 @@ class EqlSpecificationTests {
@Disabled(SPEC_FAULT + "Use double-quotes when it should be using single-quotes for a string literal")
void fromClauseDowncastingExample3_SPEC_BUG() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT e FROM Employee e JOIN e.projects p
WHERE TREAT(p AS LargeProject).budget > 1000
OR TREAT(p AS SmallProject).name LIKE 'Persist%'
@@ -283,7 +311,7 @@ class EqlSpecificationTests {
@Test
void fromClauseDowncastingExample3fixed() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT e FROM Employee e JOIN e.projects p
WHERE TREAT(p AS LargeProject).budget > 1000
OR TREAT(p AS SmallProject).name LIKE 'Persist%'
@@ -294,7 +322,7 @@ class EqlSpecificationTests {
@Test
void fromClauseDowncastingExample4() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT e FROM Employee e
WHERE TREAT(e AS Exempt).vacationDays > 10
OR TREAT(e AS Contractor).hours > 100
@@ -304,7 +332,7 @@ class EqlSpecificationTests {
@Test
void pathExpressionsNamedParametersExample() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT c
FROM Customer c
WHERE c.status = :stat
@@ -314,7 +342,7 @@ class EqlSpecificationTests {
@Test
void betweenExpressionsExample() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT t
FROM CreditCard c JOIN c.transactionHistory t
WHERE c.holder.name = 'John Doe' AND INDEX(t) BETWEEN 0 AND 9
@@ -324,7 +352,7 @@ class EqlSpecificationTests {
@Test
void isEmptyExample() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT o
FROM Order o
WHERE o.lineItems IS EMPTY
@@ -334,7 +362,7 @@ class EqlSpecificationTests {
@Test
void memberOfExample() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT p
FROM Person p
WHERE 'Joe' MEMBER OF p.nicknames
@@ -344,11 +372,10 @@ class EqlSpecificationTests {
@Test
void existsSubSelectExample1() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT emp
FROM Employee emp
WHERE EXISTS (
SELECT spouseEmp
WHERE EXISTS (SELECT spouseEmp
FROM Employee spouseEmp
WHERE spouseEmp = emp.spouse)
""");
@@ -357,11 +384,10 @@ class EqlSpecificationTests {
@Test
void allExample() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT emp
FROM Employee emp
WHERE emp.salary > ALL (
SELECT m.salary
WHERE emp.salary > ALL (SELECT m.salary
FROM Manager m
WHERE m.department = emp.department)
""");
@@ -370,11 +396,10 @@ class EqlSpecificationTests {
@Test
void existsSubSelectExample2() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT emp
FROM Employee emp
WHERE EXISTS (
SELECT spouseEmp
WHERE EXISTS (SELECT spouseEmp
FROM Employee spouseEmp
WHERE spouseEmp = emp.spouse)
""");
@@ -383,7 +408,7 @@ class EqlSpecificationTests {
@Test
void subselectNumericComparisonExample1() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT c
FROM Customer c
WHERE (SELECT AVG(o.price) FROM c.orders o) > 100
@@ -393,18 +418,17 @@ class EqlSpecificationTests {
@Test
void subselectNumericComparisonExample2() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT goodCustomer
FROM Customer goodCustomer
WHERE goodCustomer.balanceOwed < (
SELECT AVG(c.balanceOwed)/2.0 FROM Customer c)
WHERE goodCustomer.balanceOwed < (SELECT AVG(c.balanceOwed) / 2.0 FROM Customer c)
""");
}
@Test
void indexExample() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT w.name
FROM Course c JOIN c.studentWaitlist w
WHERE c.name = 'Calculus'
@@ -419,7 +443,7 @@ class EqlSpecificationTests {
@Disabled(SPEC_FAULT + "FUNCTION calls needs a comparator")
void functionInvocationExample_SPEC_BUG() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT c
FROM Customer c
WHERE FUNCTION('hasGoodCredit', c.balance, c.creditLimit)
@@ -429,7 +453,7 @@ class EqlSpecificationTests {
@Test
void functionInvocationExampleWithCorrection() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT c
FROM Customer c
WHERE FUNCTION('hasGoodCredit', c.balance, c.creditLimit) = TRUE
@@ -439,7 +463,7 @@ class EqlSpecificationTests {
@Test
void updateCaseExample1() {
EqlQueryParser.parseQuery("""
assertQuery("""
UPDATE Employee e
SET e.salary =
CASE WHEN e.rating = 1 THEN e.salary * 1.1
@@ -452,7 +476,7 @@ class EqlSpecificationTests {
@Test
void updateCaseExample2() {
EqlQueryParser.parseQuery("""
assertQuery("""
UPDATE Employee e
SET e.salary =
CASE e.rating WHEN 1 THEN e.salary * 1.1
@@ -465,7 +489,7 @@ class EqlSpecificationTests {
@Test
void selectCaseExample1() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT e.name,
CASE TYPE(e) WHEN Exempt THEN 'Exempt'
WHEN Contractor THEN 'Contractor'
@@ -480,7 +504,7 @@ class EqlSpecificationTests {
@Test
void selectCaseExample2() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT e.name,
f.name,
CONCAT(CASE WHEN f.annualMiles > 50000 THEN 'Platinum '
@@ -495,7 +519,7 @@ class EqlSpecificationTests {
@Test
void theRest() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT e
FROM Employee e
WHERE TYPE(e) IN (Exempt, Contractor)
@@ -505,7 +529,7 @@ class EqlSpecificationTests {
@Test
void theRest2() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT e
FROM Employee e
WHERE TYPE(e) IN (:empType1, :empType2)
@@ -515,7 +539,7 @@ class EqlSpecificationTests {
@Test
void theRest3() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT e
FROM Employee e
WHERE TYPE(e) IN :empTypes
@@ -525,7 +549,7 @@ class EqlSpecificationTests {
@Test
void theRest4() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT TYPE(e)
FROM Employee e
WHERE TYPE(e) <> Exempt
@@ -535,7 +559,7 @@ class EqlSpecificationTests {
@Test
void theRest5() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT c.status, AVG(c.filledOrderCount), COUNT(c)
FROM Customer c
GROUP BY c.status
@@ -546,7 +570,7 @@ class EqlSpecificationTests {
@Test
void theRest6() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT c.country, COUNT(c)
FROM Customer c
GROUP BY c.country
@@ -557,7 +581,7 @@ class EqlSpecificationTests {
@Test
void theRest7() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT c, COUNT(o)
FROM Customer c JOIN c.orders o
GROUP BY c
@@ -568,7 +592,7 @@ class EqlSpecificationTests {
@Test
void theRest8() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT c.id, c.status
FROM Customer c JOIN c.orders o
WHERE o.count > 100
@@ -578,7 +602,7 @@ class EqlSpecificationTests {
@Test
void theRest9() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT v.location.street, KEY(i).title, VALUE(i)
FROM VideoStore v JOIN v.videoInventory i
WHERE v.location.zipcode = '94301' AND VALUE(i) > 0
@@ -588,7 +612,7 @@ class EqlSpecificationTests {
@Test
void theRest10() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT o.lineItems FROM Order AS o
""");
}
@@ -596,7 +620,7 @@ class EqlSpecificationTests {
@Test
void theRest11() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT c, COUNT(l) AS itemCount
FROM Customer c JOIN c.Orders o JOIN o.lineItems l
WHERE c.address.state = 'CA'
@@ -608,7 +632,7 @@ class EqlSpecificationTests {
@Test
void theRest12() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT NEW com.acme.example.CustomerDetails(c.id, c.status, o.count)
FROM Customer c JOIN c.orders o
WHERE o.count > 100
@@ -618,7 +642,7 @@ class EqlSpecificationTests {
@Test
void theRest13() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT e.address AS addr
FROM Employee e
""");
@@ -627,7 +651,7 @@ class EqlSpecificationTests {
@Test
void theRest14() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT AVG(o.quantity) FROM Order o
""");
}
@@ -635,7 +659,7 @@ class EqlSpecificationTests {
@Test
void theRest15() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT SUM(l.price)
FROM Order o JOIN o.lineItems l JOIN o.customer c
WHERE c.lastname = 'Smith' AND c.firstname = 'John'
@@ -645,7 +669,7 @@ class EqlSpecificationTests {
@Test
void theRest16() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT COUNT(o) FROM Order o
""");
}
@@ -653,7 +677,7 @@ class EqlSpecificationTests {
@Test
void theRest17() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT COUNT(l.price)
FROM Order o JOIN o.lineItems l JOIN o.customer c
WHERE c.lastname = 'Smith' AND c.firstname = 'John'
@@ -663,7 +687,7 @@ class EqlSpecificationTests {
@Test
void theRest18() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT COUNT(l)
FROM Order o JOIN o.lineItems l JOIN o.customer c
WHERE c.lastname = 'Smith' AND c.firstname = 'John' AND l.price IS NOT NULL
@@ -673,7 +697,7 @@ class EqlSpecificationTests {
@Test
void theRest19() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT o
FROM Customer c JOIN c.orders o JOIN c.address a
WHERE a.state = 'CA'
@@ -684,7 +708,7 @@ class EqlSpecificationTests {
@Test
void theRest20() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT o.quantity, a.zipcode
FROM Customer c JOIN c.orders o JOIN c.address a
WHERE a.state = 'CA'
@@ -695,8 +719,8 @@ class EqlSpecificationTests {
@Test
void theRest21() {
EqlQueryParser.parseQuery("""
SELECT o.quantity, o.cost*1.08 AS taxedCost, a.zipcode
assertQuery("""
SELECT o.quantity, o.cost * 1.08 AS taxedCost, a.zipcode
FROM Customer c JOIN c.orders o JOIN c.address a
WHERE a.state = 'CA' AND a.county = 'Santa Clara'
ORDER BY o.quantity, taxedCost, a.zipcode
@@ -706,7 +730,7 @@ class EqlSpecificationTests {
@Test
void theRest22() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT AVG(o.quantity) as q, a.zipcode
FROM Customer c JOIN c.orders o JOIN c.address a
WHERE a.state = 'CA'
@@ -718,7 +742,7 @@ class EqlSpecificationTests {
@Test
void theRest23() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT p.product_name
FROM Order o JOIN o.lineItems l JOIN l.product p JOIN o.customer c
WHERE c.lastname = 'Smith' AND c.firstname = 'John'
@@ -733,7 +757,7 @@ class EqlSpecificationTests {
void theRest24() {
assertThatExceptionOfType(BadJpqlGrammarException.class).isThrownBy(() -> {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT p.product_name
FROM Order o, IN(o.lineItems) l JOIN o.customer c
WHERE c.lastname = 'Smith' AND c.firstname = 'John'
@@ -745,7 +769,7 @@ class EqlSpecificationTests {
@Test
void theRest25() {
EqlQueryParser.parseQuery("""
assertQuery("""
DELETE
FROM Customer c
WHERE c.status = 'inactive'
@@ -755,7 +779,7 @@ class EqlSpecificationTests {
@Test
void theRest26() {
EqlQueryParser.parseQuery("""
assertQuery("""
DELETE
FROM Customer c
WHERE c.status = 'inactive'
@@ -766,7 +790,7 @@ class EqlSpecificationTests {
@Test
void theRest27() {
EqlQueryParser.parseQuery("""
assertQuery("""
UPDATE Customer c
SET c.status = 'outstanding'
WHERE c.balance < 10000
@@ -776,7 +800,7 @@ class EqlSpecificationTests {
@Test
void theRest28() {
EqlQueryParser.parseQuery("""
assertQuery("""
UPDATE Employee e
SET e.address.building = 22
WHERE e.address.building = 14
@@ -788,7 +812,7 @@ class EqlSpecificationTests {
@Test
void theRest29() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT o
FROM Order o
""");
@@ -797,7 +821,7 @@ class EqlSpecificationTests {
@Test
void theRest30() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT o
FROM Order o
WHERE o.shippingAddress.state = 'CA'
@@ -807,7 +831,7 @@ class EqlSpecificationTests {
@Test
void theRest31() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT o.shippingAddress.state
FROM Order o
""");
@@ -816,7 +840,7 @@ class EqlSpecificationTests {
@Test
void theRest32() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT o
FROM Order o JOIN o.lineItems l
""");
@@ -825,7 +849,7 @@ class EqlSpecificationTests {
@Test
void theRest33() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT o
FROM Order o
WHERE o.lineItems IS NOT EMPTY
@@ -835,7 +859,7 @@ class EqlSpecificationTests {
@Test
void theRest34() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT o
FROM Order o
WHERE o.lineItems IS EMPTY
@@ -845,7 +869,7 @@ class EqlSpecificationTests {
@Test
void theRest35() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT o
FROM Order o JOIN o.lineItems l
WHERE l.shipped = FALSE
@@ -855,7 +879,7 @@ class EqlSpecificationTests {
@Test
void theRest36() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT o
FROM Order o
WHERE
@@ -868,7 +892,7 @@ class EqlSpecificationTests {
@Test
void theRest37() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT o
FROM Order o
WHERE o.shippingAddress <> o.billingAddress
@@ -878,7 +902,7 @@ class EqlSpecificationTests {
@Test
void theRest38() {
EqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT o
FROM Order o JOIN o.lineItems l
WHERE l.product.name = ?1

View File

@@ -16,7 +16,6 @@
package org.springframework.data.jpa.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.stream.Stream;
@@ -56,7 +55,7 @@ class HqlQueryRendererTests {
HqlParser.StartContext parsedQuery = parser.start();
return render(new HqlQueryRenderer().visit(parsedQuery));
return new HqlQueryRenderer().visit(parsedQuery).build().render();
}
static Stream<Arguments> reservedWords() {
@@ -404,7 +403,7 @@ class HqlQueryRendererTests {
assertQuery("""
SELECT emp
FROM Employee emp
WHERE emp.salary > ALL(SELECT m.salary
WHERE emp.salary > ALL (SELECT m.salary
FROM Manager m
WHERE m.department = emp.department)
""");
@@ -438,7 +437,7 @@ class HqlQueryRendererTests {
assertQuery("""
SELECT goodCustomer
FROM Customer goodCustomer
WHERE goodCustomer.balanceOwed < (SELECT AVG(c.balanceOwed)/2.0 FROM Customer c)
WHERE goodCustomer.balanceOwed < (SELECT AVG(c.balanceOwed) / 2.0 FROM Customer c)
""");
}
@@ -483,9 +482,9 @@ class HqlQueryRendererTests {
assertQuery("""
UPDATE Employee e
SET e.salary =
CASE WHEN e.rating = 1 THEN e.salary*1.1
WHEN e.rating = 2 THEN e.salary*1.05
ELSE e.salary*1.01
CASE WHEN e.rating = 1 THEN e.salary * 1.1
WHEN e.rating = 2 THEN e.salary * 1.05
ELSE e.salary * 1.01
END
""");
}
@@ -496,9 +495,9 @@ class HqlQueryRendererTests {
assertQuery("""
UPDATE Employee e
SET e.salary =
CASE e.rating WHEN 1 THEN e.salary*1.1
WHEN 2 THEN e.salary*1.05
ELSE e.salary*1.01
CASE e.rating WHEN 1 THEN e.salary * 1.1
WHEN 2 THEN e.salary * 1.05
ELSE e.salary * 1.01
END
""");
}
@@ -753,7 +752,7 @@ class HqlQueryRendererTests {
void theRest21() {
assertQuery("""
SELECT o.quantity, o.cost*1.08 AS taxedCost, a.zipcode
SELECT o.quantity, o.cost * 1.08 AS taxedCost, a.zipcode
FROM Customer c JOIN c.orders o JOIN c.address a
WHERE a.state = 'CA' AND a.county = 'Santa Clara'
ORDER BY o.quantity, taxedCost, a.zipcode
@@ -969,6 +968,10 @@ class HqlQueryRendererTests {
parseWithoutChanges("select p " + //
"from Person p " + //
"where p.name like 'Joe'");
parseWithoutChanges("select p " + //
"from Person p " + //
"where p.name ilike 'Joe'");
parseWithoutChanges("select p " + //
"from Person p " + //
"where p.name like 'Joe''s'");
@@ -1359,7 +1362,8 @@ class HqlQueryRendererTests {
" sum(c.duration), " + //
" min(c.duration), " + //
" max(c.duration), " + //
" avg(c.duration)" + //
" avg(c.duration)," + //
" 1" + //
") " + //
"from Call c ");
parseWithoutChanges("select new map(" + //
@@ -1449,6 +1453,12 @@ class HqlQueryRendererTests {
"join c.phone p " + //
"order by p.number " + //
"fetch first 50 rows only");
parseWithoutChanges("select c " + //
"from Call c " + //
"join c.phone p " + //
"order by p.number " + //
"offset 10 rows " + //
"fetch first 50 rows with ties");
parseWithoutChanges("select p " + //
"from Phone p " + //
"join fetch p.calls " + //
@@ -1501,10 +1511,9 @@ class HqlQueryRendererTests {
void cteWithClauseShouldWork() {
assertQuery("""
WITH maxId AS(select max(sr.snapshot.id) snapshotId from SnapshotReference sr
WITH maxId AS (select max(sr.snapshot.id) snapshotId from SnapshotReference sr
where sr.id.selectionId = ?1 and sr.enabled
group by sr.userId
)
group by sr.userId)
select sr from maxId m join SnapshotReference sr on sr.snapshot.id = m.snapshotId
""");
}
@@ -1654,15 +1663,15 @@ class HqlQueryRendererTests {
@ParameterizedTest // GH-3342
@ValueSource(
strings = { "select 1 from User", "select -1 from User", "select +1 from User", "select +1*-100 from User",
"select count(u)*-0.7f from User u", "select count(oi) + (-100) as perc from StockOrderItem oi",
strings = { "select 1 from User", "select -1 from User", "select +1 from User", "select +1 * -100 from User",
"select count(u) * -0.7f from User u", "select count(oi) + (-100) as perc from StockOrderItem oi",
"select p from Payment p where length(p.cardNumber) between +16 and -20" })
void signedLiteralShouldWork(String query) {
assertQuery(query);
}
@ParameterizedTest // GH-3342
@ValueSource(strings = { "select -count(u) from User u", "select +1*(-count(u)) from User u" })
@ValueSource(strings = { "select -count(u) from User u", "select +1 * (-count(u)) from User u" })
void signedExpressionsShouldWork(String query) {
assertQuery(query);
}

View File

@@ -197,10 +197,10 @@ class HqlQueryTransformerTests {
assertThat(alias("select u from User u where not exists (select u2 from User u2)")).isEqualTo("u");
assertThat(alias(
"select u from User u where not exists (select u2 from User u2 where not exists (select u3 from User u3))"))
.isEqualTo("u");
.isEqualTo("u");
assertThat(alias(
"SELECT e FROM DbEvent e WHERE TREAT(modifiedFrom AS date) IS NULL OR e.modificationDate >= :modifiedFrom"))
.isEqualTo("e");
.isEqualTo("e");
}
@Test // GH-2557
@@ -360,7 +360,7 @@ class HqlQueryTransformerTests {
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();
.isTrue();
}
@Test // DATAJPA-938
@@ -452,7 +452,7 @@ class HqlQueryTransformerTests {
String query = "SELECT AVG(m.price) AS m.avg FROM Magazine m";
Sort sort = Sort.by("m.avg");
assertThatIllegalArgumentException().isThrownBy(() -> createQueryFor(query, sort));
assertThatExceptionOfType(BadJpqlGrammarException.class).isThrownBy(() -> createQueryFor(query, sort));
}
@Test // DATAJPA-965, DATAJPA-970, GH-2863
@@ -467,8 +467,10 @@ class HqlQueryTransformerTests {
@Test // DATAJPA-1506
void detectsAliasWithGroupAndOrderBy() {
assertThat(alias("select * from User group by name")).isNull();
assertThat(alias("select * from User order by name")).isNull();
assertThatExceptionOfType(BadJpqlGrammarException.class)
.isThrownBy(() -> alias("select * from User group by name"));
assertThatExceptionOfType(BadJpqlGrammarException.class)
.isThrownBy(() -> alias("select * from User order by name"));
assertThat(alias("select u from User u group by name")).isEqualTo("u");
assertThat(alias("select u from User u order by name")).isEqualTo("u");
}
@@ -600,8 +602,10 @@ class HqlQueryTransformerTests {
@Test
void detectsAliasWithGroupAndOrderByWithLineBreaks() {
assertThat(alias("select * from User group\nby name")).isNull();
assertThat(alias("select * from User order\nby name")).isNull();
assertThatExceptionOfType(BadJpqlGrammarException.class)
.isThrownBy(() -> alias("select * from User group\nby name"));
assertThatExceptionOfType(BadJpqlGrammarException.class)
.isThrownBy(() -> alias("select * from User order\nby name"));
assertThat(alias("select u from User u group\nby name")).isEqualTo("u");
assertThat(alias("select u from User u order\nby name")).isEqualTo("u");
assertThat(alias("select u from User\nu\norder \n by name")).isEqualTo("u");
@@ -624,7 +628,8 @@ class HqlQueryTransformerTests {
// 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(projection("select * from (select x from y)")).isNotEqualTo("*");
assertThatExceptionOfType(BadJpqlGrammarException.class)
.isThrownBy(() -> projection("select * from (select x from y)"));
}
@Test // DATAJPA-1696
@@ -695,15 +700,18 @@ class HqlQueryTransformerTests {
.isEqualTo("select dense_rank() over (order by lastname) from user u order by u.lastname, u.age desc");
// partition by + order by in over clause
assertThat(createQueryFor("select dense_rank() over (partition by active, age order by lastname) from user u",
sort)).isEqualTo(
"select dense_rank() over (partition by active, age order by lastname) from user u order by u.age desc");
assertThat(
createQueryFor(
"select dense_rank() over (partition by active, age order by lastname range between 1.0 preceding and 1.0 following) from user u",
sort))
.isEqualTo(
"select dense_rank() over (partition by active, age order by lastname range between 1.0 preceding and 1.0 following) from user u order by u.age desc");
// partition by + order by in over clause + order by at the end
assertThat(createQueryFor(
"select dense_rank() over (partition by active, age order by lastname) from user u order by active", sort))
.isEqualTo(
"select dense_rank() over (partition by active, age order by lastname) from user u order by active, u.age desc");
.isEqualTo(
"select dense_rank() over (partition by active, age order by lastname) from user u order by active, u.age desc");
// partition by + order by in over clause + frame clause
assertThat(createQueryFor(
@@ -728,13 +736,13 @@ class HqlQueryTransformerTests {
// order by in subselect (from expression)
assertThat(createQueryFor("select u from (select u2 from user u2 order by age desc limit 10) u", sort))
.isEqualTo("select u from (select u2 from user u2 order by age desc limit 10 ) u order by u.age desc");
.isEqualTo("select u from (select u2 from user u2 order by age desc limit 10) u order by u.age desc");
// order by in subselect (from expression) + at the end
assertThat(createQueryFor(
"select u from (select u2 from user u2 order by 1, 2, 3 desc limit 10) u order by u.active asc", sort))
.isEqualTo(
"select u from (select u2 from user u2 order by 1, 2, 3 desc limit 10 ) u order by u.active asc, u.age desc");
.isEqualTo(
"select u from (select u2 from user u2 order by 1, 2, 3 desc limit 10) u order by u.active asc, u.age desc");
}
@Test // GH-2511
@@ -745,7 +753,7 @@ class HqlQueryTransformerTests {
assertThat(
createCountQueryFor("SELECT e FROM mytable e WHERE nr = :number AND kon = :kon AND datum >= '2019-01-01'"))
.isEqualTo("SELECT count(e) FROM mytable e WHERE nr = :number AND kon = :kon AND datum >= '2019-01-01'");
.isEqualTo("SELECT count(e) FROM mytable e WHERE nr = :number AND kon = :kon AND datum >= '2019-01-01'");
assertThat(createCountQueryFor("SELECT e FROM context e ORDER BY time"))
.isEqualTo("SELECT count(e) FROM context e");
@@ -755,7 +763,7 @@ class HqlQueryTransformerTests {
assertThat(
createCountQueryFor("SELECT us FROM users_statuses us WHERE (user_created_at BETWEEN :fromDate AND :toDate)"))
.isEqualTo("SELECT count(us) FROM users_statuses us WHERE (user_created_at BETWEEN :fromDate AND :toDate)");
.isEqualTo("SELECT count(us) FROM users_statuses us WHERE (user_created_at BETWEEN :fromDate AND :toDate)");
}
@Test // GH-2496, GH-2522, GH-2537, GH-2045
@@ -953,7 +961,7 @@ class HqlQueryTransformerTests {
""", Sort.by(Sort.Direction.ASC, "cheapestBundlePrice") //
.and(Sort.by(Sort.Direction.ASC, "earliestBundleStart")) //
.and(Sort.by(Sort.Direction.ASC, "name"))))
.endsWith(" order by cheapestBundlePrice asc, earliestBundleStart asc, name asc");
.endsWith(" order by cheapestBundlePrice asc, earliestBundleStart asc, name asc");
}
@Test // GH-2863, GH-1655
@@ -1019,8 +1027,7 @@ class HqlQueryTransformerTests {
assertThat(
createQueryFor("select e from Employee e where e.name = :name", Sort.by(Sort.Order.desc("evaluationDate"))))
.isEqualToIgnoringWhitespace(
"select e from Employee e where e.name = :name order by e.evaluationDate desc");
.isEqualToIgnoringWhitespace("select e from Employee e where e.name = :name order by e.evaluationDate desc");
assertThat(createQueryFor("select e from Employee e join training t where e.name = :name",
Sort.by(Sort.Order.desc("trainingDueDate")))).isEqualToIgnoringWhitespace(
@@ -1042,12 +1049,15 @@ class HqlQueryTransformerTests {
@Test // GH-3269
void createsCountQueryUsingAliasCorrectly() {
assertCountQuery("select distinct 1 as x from Employee","select count(distinct 1) from Employee AS __");
assertCountQuery("SELECT DISTINCT abc AS x FROM T","SELECT count(DISTINCT abc) FROM T AS __");
assertCountQuery("select distinct a as x, b as y from Employee","select count(distinct a , b) from Employee AS __");
assertCountQuery("select distinct sum(amount) as x from Employee GROUP BY n","select count(distinct sum(amount)) from Employee AS __ GROUP BY n");
assertCountQuery("select distinct a, b, sum(amount) as c, d from Employee GROUP BY n","select count(distinct a, b, sum(amount) , d) from Employee AS __ GROUP BY n");
assertCountQuery("select distinct a, count(b) as c from Employee GROUP BY n","select count(distinct a, count(b)) from Employee AS __ GROUP BY n");
assertCountQuery("select distinct 1 as x from Employee", "select count(distinct 1) from Employee AS __");
assertCountQuery("SELECT DISTINCT abc AS x FROM T", "SELECT count(DISTINCT abc) FROM T AS __");
assertCountQuery("select distinct a as x, b as y from Employee", "select count(distinct a, b) from Employee AS __");
assertCountQuery("select distinct sum(amount) as x from Employee GROUP BY n",
"select count(distinct sum(amount)) from Employee AS __ GROUP BY n");
assertCountQuery("select distinct a, b, sum(amount) as c, d from Employee GROUP BY n",
"select count(distinct a, b, sum(amount), d) from Employee AS __ GROUP BY n");
assertCountQuery("select distinct a, count(b) as c from Employee GROUP BY n",
"select count(distinct a, count(b)) from Employee AS __ GROUP BY n");
}
private void assertCountQuery(String originalQuery, String countQuery) {

View File

@@ -93,12 +93,8 @@ class JpaQueryLookupStrategyUnitTests {
Method method = UserRepository.class.getMethod("findByFoo", String.class);
RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class);
Throwable reference = new RuntimeException();
when(em.createQuery(anyString())).thenThrow(reference);
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> strategy.resolveQuery(method, metadata, projectionFactory, namedQueries))
.withCause(reference);
assertThatExceptionOfType(BadJpqlGrammarException.class)
.isThrownBy(() -> strategy.resolveQuery(method, metadata, projectionFactory, namedQueries));
}
@Test // DATAJPA-554
@@ -244,7 +240,7 @@ class JpaQueryLookupStrategyUnitTests {
@Query(value = "select foo from Foo foo", countName = "foo.count")
Page<User> findByStringQueryWithNamedCountQuery(String foo, Pageable pageable);
@Query(value = "something absurd", name = "my-query-name")
@Query(value = "select foo from Foo foo", name = "my-query-name")
User annotatedQueryWithQueryAndQueryName();
@Query("SELECT * FROM table WHERE (json_col->'jsonKey')::jsonb \\?\\? :param ")

View File

@@ -436,7 +436,7 @@ class JpqlQueryRendererTests {
assertQuery("""
SELECT goodCustomer
FROM Customer goodCustomer
WHERE goodCustomer.balanceOwed < (SELECT AVG(c.balanceOwed)/2.0 FROM Customer c)
WHERE goodCustomer.balanceOwed < (SELECT AVG(c.balanceOwed) / 2.0 FROM Customer c)
""");
}
@@ -481,9 +481,9 @@ class JpqlQueryRendererTests {
assertQuery("""
UPDATE Employee e
SET e.salary =
CASE WHEN e.rating = 1 THEN e.salary*1.1
WHEN e.rating = 2 THEN e.salary*1.05
ELSE e.salary*1.01
CASE WHEN e.rating = 1 THEN e.salary * 1.1
WHEN e.rating = 2 THEN e.salary * 1.05
ELSE e.salary * 1.01
END
""");
}
@@ -494,9 +494,9 @@ class JpqlQueryRendererTests {
assertQuery("""
UPDATE Employee e
SET e.salary =
CASE e.rating WHEN 1 THEN e.salary*1.1
WHEN 2 THEN e.salary*1.05
ELSE e.salary*1.01
CASE e.rating WHEN 1 THEN e.salary * 1.1
WHEN 2 THEN e.salary * 1.05
ELSE e.salary * 1.01
END
""");
}
@@ -735,7 +735,7 @@ class JpqlQueryRendererTests {
void orderByThatMatchesAllSelectAliasesShouldWork() {
assertQuery("""
SELECT o.quantity, o.cost*1.08 AS taxedCost, a.zipcode
SELECT o.quantity, o.cost * 1.08 AS taxedCost, a.zipcode
FROM Customer c JOIN c.orders o JOIN c.address a
WHERE a.state = 'CA' AND a.county = 'Santa Clara'
ORDER BY o.quantity, taxedCost, a.zipcode
@@ -1012,15 +1012,15 @@ class JpqlQueryRendererTests {
@ParameterizedTest // GH-3342
@ValueSource(strings = { "select 1 as value from User u", "select -1 as value from User u",
"select +1 as value from User u", "select +1*-100 as value from User u",
"select count(u)*-0.7f as value from User u", "select count(oi) + (-100) as perc from StockOrderItem oi",
"select +1 as value from User u", "select +1 * -100 as value from User u",
"select count(u) * -0.7f as value from User u", "select count(oi) + (-100) as perc from StockOrderItem oi",
"select p from Payment p where length(p.cardNumber) between +16 and -20" })
void signedLiteralShouldWork(String query) {
assertQuery(query);
}
@ParameterizedTest // GH-3342
@ValueSource(strings = { "select -count(u) from User u", "select +1*(-count(u)) from User u" })
@ValueSource(strings = { "select -count(u) from User u", "select +1 * (-count(u)) from User u" })
void signedExpressionsShouldWork(String query) {
assertQuery(query);
}

View File

@@ -34,6 +34,7 @@ import org.springframework.lang.Nullable;
* {@link JpqlQueryParser}.
*
* @author Greg Turnquist
* @author Mark Paluch
*/
class JpqlQueryTransformerTests {
@@ -189,7 +190,7 @@ class JpqlQueryTransformerTests {
assertThat(alias("select u from User u where not exists (select u2 from User u2)")).isEqualTo("u");
assertThat(alias(
"select u from User u where not exists (select u2 from User u2 where not exists (select u3 from User u3))"))
.isEqualTo("u");
.isEqualTo("u");
}
@Test // GH-2557
@@ -360,7 +361,7 @@ class JpqlQueryTransformerTests {
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();
.isTrue();
}
@Test // DATAJPA-938
@@ -452,7 +453,7 @@ class JpqlQueryTransformerTests {
String query = "SELECT AVG(m.price) AS m.avg FROM Magazine m";
Sort sort = Sort.by("m.avg");
assertThatIllegalArgumentException().isThrownBy(() -> createQueryFor(query, sort));
assertThatExceptionOfType(BadJpqlGrammarException.class).isThrownBy(() -> createQueryFor(query, sort));
}
@Test // DATAJPA-965, DATAJPA-970, GH-2863
@@ -467,8 +468,8 @@ class JpqlQueryTransformerTests {
@Test // DATAJPA-1506
void detectsAliasWithGroupAndOrderBy() {
assertThat(alias("select * from User group by name")).isNull();
assertThat(alias("select * from User order by name")).isNull();
// assertThat(alias("select * from User group by name")).isNull();
// assertThat(alias("select * from User order by name")).isNull();
assertThat(alias("select u from User u group by name")).isEqualTo("u");
assertThat(alias("select u from User u order by name")).isEqualTo("u");
}
@@ -492,11 +493,6 @@ class JpqlQueryTransformerTests {
@Test
void createCountQuerySupportsLineBreaksInSelectClause() {
//
//
//
//
//
assertThat(createCountQueryFor("""
select user.age,
user.name
@@ -582,8 +578,11 @@ class JpqlQueryTransformerTests {
@Test
void detectsAliasWithGroupAndOrderByWithLineBreaks() {
assertThat(alias("select * from User group\nby name")).isNull();
assertThat(alias("select * from User order\nby name")).isNull();
assertThatExceptionOfType(BadJpqlGrammarException.class)
.isThrownBy(() -> alias("select * from User group\nby name"));
assertThatExceptionOfType(BadJpqlGrammarException.class)
.isThrownBy(() -> alias("select * from User order\nby name"));
assertThat(alias("select u from User u group\nby name")).isEqualTo("u");
assertThat(alias("select u from User u order\nby name")).isEqualTo("u");
assertThat(alias("select u from User\nu\norder \n by name")).isEqualTo("u");
@@ -606,7 +605,8 @@ class JpqlQueryTransformerTests {
// 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(projection("select * from (select x from y)")).isNotEqualTo("*");
assertThatExceptionOfType(BadJpqlGrammarException.class)
.isThrownBy(() -> projection("select * from (select x from y)"));
}
@Test // DATAJPA-1696
@@ -669,14 +669,14 @@ class JpqlQueryTransformerTests {
assertThat(
createCountQueryFor("SELECT t FROM mytable t WHERE nr = :number AND kon = :kon AND datum >= '2019-01-01'"))
.isEqualTo("SELECT count(t) FROM mytable t WHERE nr = :number AND kon = :kon AND datum >= '2019-01-01'");
.isEqualTo("SELECT count(t) FROM mytable t WHERE nr = :number AND kon = :kon AND datum >= '2019-01-01'");
assertThat(createCountQueryFor("select s FROM users_statuses s WHERE (user_created_at BETWEEN $1 AND $2)"))
.isEqualTo("select count(s) FROM users_statuses s WHERE (user_created_at BETWEEN $1 AND $2)");
assertThat(
createCountQueryFor("SELECT us FROM users_statuses us WHERE (user_created_at BETWEEN :fromDate AND :toDate)"))
.isEqualTo("SELECT count(us) FROM users_statuses us WHERE (user_created_at BETWEEN :fromDate AND :toDate)");
.isEqualTo("SELECT count(us) FROM users_statuses us WHERE (user_created_at BETWEEN :fromDate AND :toDate)");
}
@Test // GH-3269
@@ -684,11 +684,11 @@ class JpqlQueryTransformerTests {
assertCountQuery("select distinct 1 as x from Employee e", "select count(distinct 1) from Employee e");
assertCountQuery("SELECT DISTINCT abc AS x FROM T t", "SELECT count(DISTINCT abc) FROM T t");
assertCountQuery("select distinct a as x, b as y from Employee e", "select count(distinct a , b) from Employee e");
assertCountQuery("select distinct a as x, b as y from Employee e", "select count(distinct a, b) from Employee e");
assertCountQuery("select distinct sum(amount) as x from Employee e GROUP BY n",
"select count(distinct sum(amount)) from Employee e GROUP BY n");
assertCountQuery("select distinct a, b, sum(amount) as c, d from Employee e GROUP BY n",
"select count(distinct a, b, sum(amount) , d) from Employee e GROUP BY n");
"select count(distinct a, b, sum(amount), d) from Employee e GROUP BY n");
assertCountQuery("select distinct a, count(b) as c from Employee e GROUP BY n",
"select count(distinct a, count(b)) from Employee e GROUP BY n");
}

View File

@@ -16,7 +16,10 @@
package org.springframework.data.jpa.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -33,13 +36,41 @@ class JpqlSpecificationTests {
private static final String SPEC_FAULT = "Disabled due to spec fault> ";
/**
* Parse the query using {@link HqlParser} then run it through the query-preserving {@link HqlQueryRenderer}.
*/
private static String parseWithoutChanges(String query) {
JpqlLexer lexer = new JpqlLexer(CharStreams.fromString(query));
JpqlParser parser = new JpqlParser(new CommonTokenStream(lexer));
parser.addErrorListener(new BadJpqlGrammarErrorListener(query));
JpqlParser.StartContext parsedQuery = parser.start();
return render(new JpqlQueryRenderer().visit(parsedQuery));
}
private void assertQuery(String query) {
String slimmedDownQuery = reduceWhitespace(query);
assertThat(parseWithoutChanges(slimmedDownQuery)).isEqualTo(slimmedDownQuery);
}
private String reduceWhitespace(String original) {
return original //
.replaceAll("[ \\t\\n]{1,}", " ") //
.trim();
}
/**
* @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#example
*/
@Test
void joinExample1() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT o
FROM Order AS o JOIN o.lineItems AS l
WHERE l.shipped = FALSE
@@ -53,7 +84,7 @@ class JpqlSpecificationTests {
@Test
void joinExample2() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT o
FROM Order o JOIN o.lineItems l JOIN l.product p
WHERE p.productType = 'office_supplies'
@@ -66,12 +97,12 @@ class JpqlSpecificationTests {
@Test
void rangeVariableDeclarations() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT o1
FROM Order o1, Order o2
WHERE o1.quantity > o2.quantity AND
o2.customer.lastname = 'Smith' AND
o2.customer.firstname= 'John'
o2.customer.firstname = 'John'
""");
}
@@ -81,7 +112,7 @@ class JpqlSpecificationTests {
@Test
void pathExpressionsExample1() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT i.name, VALUE(p)
FROM Item i JOIN i.photos p
WHERE KEY(p) LIKE '%egret'
@@ -94,7 +125,7 @@ class JpqlSpecificationTests {
@Test
void pathExpressionsExample2() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT i.name, p
FROM Item i JOIN i.photos p
WHERE KEY(p) LIKE '%egret'
@@ -107,7 +138,7 @@ class JpqlSpecificationTests {
@Test
void pathExpressionsExample3() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT p.vendor
FROM Employee e JOIN e.contactInfo.phones p
""");
@@ -119,7 +150,7 @@ class JpqlSpecificationTests {
@Test
void pathExpressionsExample4() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT p.vendor
FROM Employee e JOIN e.contactInfo c JOIN c.phones p
WHERE e.contactInfo.address.zipcode = '95054'
@@ -129,7 +160,7 @@ class JpqlSpecificationTests {
@Test
void pathExpressionSyntaxExample1() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT l.product
FROM Order AS o JOIN o.lineItems l
""");
@@ -138,7 +169,7 @@ class JpqlSpecificationTests {
@Test
void joinsExample1() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT c FROM Customer c, Employee e WHERE c.hatsize = e.shoesize
""");
}
@@ -146,7 +177,7 @@ class JpqlSpecificationTests {
@Test
void joinsExample2() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT c FROM Customer c JOIN c.orders o WHERE c.status = 1
""");
}
@@ -154,7 +185,7 @@ class JpqlSpecificationTests {
@Test
void joinsInnerExample() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT c FROM Customer c INNER JOIN c.orders o WHERE c.status = 1
""");
}
@@ -162,7 +193,7 @@ class JpqlSpecificationTests {
@Test
void joinsInExample() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT OBJECT(c) FROM Customer c, IN(c.orders) o WHERE c.status = 1
""");
}
@@ -170,7 +201,7 @@ class JpqlSpecificationTests {
@Test
void doubleJoinExample() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT p.vendor
FROM Employee e JOIN e.contactInfo c JOIN c.phones p
WHERE c.address.zipcode = '95054'
@@ -180,7 +211,7 @@ class JpqlSpecificationTests {
@Test
void leftJoinExample() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT s.name, COUNT(p)
FROM Suppliers s LEFT JOIN s.products p
GROUP BY s.name
@@ -190,7 +221,7 @@ class JpqlSpecificationTests {
@Test
void leftJoinOnExample() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT s.name, COUNT(p)
FROM Suppliers s LEFT JOIN s.products p
ON p.status = 'inStock'
@@ -201,7 +232,7 @@ class JpqlSpecificationTests {
@Test
void leftJoinWhereExample() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT s.name, COUNT(p)
FROM Suppliers s LEFT JOIN s.products p
WHERE p.status = 'inStock'
@@ -212,7 +243,7 @@ class JpqlSpecificationTests {
@Test
void leftJoinFetchExample() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT d
FROM Department d LEFT JOIN FETCH d.employees
WHERE d.deptno = 1
@@ -222,7 +253,7 @@ class JpqlSpecificationTests {
@Test
void collectionMemberExample() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT o
FROM Order o JOIN o.lineItems l
WHERE l.product.productType = 'office_supplies'
@@ -232,7 +263,7 @@ class JpqlSpecificationTests {
@Test
void collectionMemberInExample() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT o
FROM Order o, IN(o.lineItems) l
WHERE l.product.productType = 'office_supplies'
@@ -242,7 +273,7 @@ class JpqlSpecificationTests {
@Test
void fromClauseExample() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT o
FROM Order AS o JOIN o.lineItems l JOIN l.product p
""");
@@ -251,19 +282,19 @@ class JpqlSpecificationTests {
@Test
void fromClauseDowncastingExample1() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT b.name, b.ISBN
FROM Order o JOIN TREAT(o.product AS Book) b
""");
""");
}
@Test
void fromClauseDowncastingExample2() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT e FROM Employee e JOIN TREAT(e.projects AS LargeProject) lp
WHERE lp.budget > 1000
""");
""");
}
/**
@@ -273,39 +304,39 @@ class JpqlSpecificationTests {
@Disabled(SPEC_FAULT + "Use double-quotes when it should be using single-quotes for a string literal")
void fromClauseDowncastingExample3_SPEC_BUG() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT e FROM Employee e JOIN e.projects p
WHERE TREAT(p AS LargeProject).budget > 1000
OR TREAT(p AS SmallProject).name LIKE 'Persist%'
OR p.description LIKE "cost overrun"
""");
""");
}
@Test
void fromClauseDowncastingExample3fixed() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT e FROM Employee e JOIN e.projects p
WHERE TREAT(p AS LargeProject).budget > 1000
OR TREAT(p AS SmallProject).name LIKE 'Persist%'
OR p.description LIKE 'cost overrun'
""");
""");
}
@Test
void fromClauseDowncastingExample4() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT e FROM Employee e
WHERE TREAT(e AS Exempt).vacationDays > 10
OR TREAT(e AS Contractor).hours > 100
""");
""");
}
@Test
void pathExpressionsNamedParametersExample() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT c
FROM Customer c
WHERE c.status = :stat
@@ -315,7 +346,7 @@ class JpqlSpecificationTests {
@Test
void betweenExpressionsExample() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT t
FROM CreditCard c JOIN c.transactionHistory t
WHERE c.holder.name = 'John Doe' AND INDEX(t) BETWEEN 0 AND 9
@@ -325,7 +356,7 @@ class JpqlSpecificationTests {
@Test
void isEmptyExample() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT o
FROM Order o
WHERE o.lineItems IS EMPTY
@@ -335,7 +366,7 @@ class JpqlSpecificationTests {
@Test
void memberOfExample() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT p
FROM Person p
WHERE 'Joe' MEMBER OF p.nicknames
@@ -345,11 +376,10 @@ class JpqlSpecificationTests {
@Test
void existsSubSelectExample1() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT emp
FROM Employee emp
WHERE EXISTS (
SELECT spouseEmp
WHERE EXISTS (SELECT spouseEmp
FROM Employee spouseEmp
WHERE spouseEmp = emp.spouse)
""");
@@ -358,33 +388,31 @@ class JpqlSpecificationTests {
@Test
void allExample() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT emp
FROM Employee emp
WHERE emp.salary > ALL (
SELECT m.salary
WHERE emp.salary > ALL (SELECT m.salary
FROM Manager m
WHERE m.department = emp.department)
""");
""");
}
@Test
void existsSubSelectExample2() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT emp
FROM Employee emp
WHERE EXISTS (
SELECT spouseEmp
WHERE EXISTS (SELECT spouseEmp
FROM Employee spouseEmp
WHERE spouseEmp = emp.spouse)
""");
""");
}
@Test
void subselectNumericComparisonExample1() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT c
FROM Customer c
WHERE (SELECT AVG(o.price) FROM c.orders o) > 100
@@ -394,18 +422,17 @@ class JpqlSpecificationTests {
@Test
void subselectNumericComparisonExample2() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT goodCustomer
FROM Customer goodCustomer
WHERE goodCustomer.balanceOwed < (
SELECT AVG(c.balanceOwed)/2.0 FROM Customer c)
WHERE goodCustomer.balanceOwed < (SELECT AVG(c.balanceOwed) / 2.0 FROM Customer c)
""");
}
@Test
void indexExample() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT w.name
FROM Course c JOIN c.studentWaitlist w
WHERE c.name = 'Calculus'
@@ -420,7 +447,7 @@ class JpqlSpecificationTests {
@Disabled(SPEC_FAULT + "FUNCTION calls needs a comparator")
void functionInvocationExample_SPEC_BUG() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT c
FROM Customer c
WHERE FUNCTION('hasGoodCredit', c.balance, c.creditLimit)
@@ -430,7 +457,7 @@ class JpqlSpecificationTests {
@Test
void functionInvocationExampleWithCorrection() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT c
FROM Customer c
WHERE FUNCTION('hasGoodCredit', c.balance, c.creditLimit) = TRUE
@@ -440,33 +467,33 @@ class JpqlSpecificationTests {
@Test
void updateCaseExample1() {
JpqlQueryParser.parseQuery("""
assertQuery("""
UPDATE Employee e
SET e.salary =
CASE WHEN e.rating = 1 THEN e.salary * 1.1
WHEN e.rating = 2 THEN e.salary * 1.05
ELSE e.salary * 1.01
END
""");
""");
}
@Test
void updateCaseExample2() {
JpqlQueryParser.parseQuery("""
assertQuery("""
UPDATE Employee e
SET e.salary =
CASE e.rating WHEN 1 THEN e.salary * 1.1
WHEN 2 THEN e.salary * 1.05
ELSE e.salary * 1.01
END
""");
""");
}
@Test
void selectCaseExample1() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT e.name,
CASE TYPE(e) WHEN Exempt THEN 'Exempt'
WHEN Contractor THEN 'Contractor'
@@ -481,7 +508,7 @@ class JpqlSpecificationTests {
@Test
void selectCaseExample2() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT e.name,
f.name,
CONCAT(CASE WHEN f.annualMiles > 50000 THEN 'Platinum '
@@ -496,17 +523,17 @@ class JpqlSpecificationTests {
@Test
void theRest() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT e
FROM Employee e
WHERE TYPE(e) IN (Exempt, Contractor)
""");
""");
}
@Test
void theRest2() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT e
FROM Employee e
WHERE TYPE(e) IN (:empType1, :empType2)
@@ -516,7 +543,7 @@ class JpqlSpecificationTests {
@Test
void theRest3() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT e
FROM Employee e
WHERE TYPE(e) IN :empTypes
@@ -526,7 +553,7 @@ class JpqlSpecificationTests {
@Test
void theRest4() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT TYPE(e)
FROM Employee e
WHERE TYPE(e) <> Exempt
@@ -536,7 +563,7 @@ class JpqlSpecificationTests {
@Test
void theRest5() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT c.status, AVG(c.filledOrderCount), COUNT(c)
FROM Customer c
GROUP BY c.status
@@ -547,7 +574,7 @@ class JpqlSpecificationTests {
@Test
void theRest6() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT c.country, COUNT(c)
FROM Customer c
GROUP BY c.country
@@ -558,7 +585,7 @@ class JpqlSpecificationTests {
@Test
void theRest7() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT c, COUNT(o)
FROM Customer c JOIN c.orders o
GROUP BY c
@@ -569,7 +596,7 @@ class JpqlSpecificationTests {
@Test
void theRest8() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT c.id, c.status
FROM Customer c JOIN c.orders o
WHERE o.count > 100
@@ -579,7 +606,7 @@ class JpqlSpecificationTests {
@Test
void theRest9() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT v.location.street, KEY(i).title, VALUE(i)
FROM VideoStore v JOIN v.videoInventory i
WHERE v.location.zipcode = '94301' AND VALUE(i) > 0
@@ -589,7 +616,7 @@ class JpqlSpecificationTests {
@Test
void theRest10() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT o.lineItems FROM Order AS o
""");
}
@@ -597,7 +624,7 @@ class JpqlSpecificationTests {
@Test
void theRest11() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT c, COUNT(l) AS itemCount
FROM Customer c JOIN c.Orders o JOIN o.lineItems l
WHERE c.address.state = 'CA'
@@ -609,7 +636,7 @@ class JpqlSpecificationTests {
@Test
void theRest12() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT NEW com.acme.example.CustomerDetails(c.id, c.status, o.count)
FROM Customer c JOIN c.orders o
WHERE o.count > 100
@@ -619,7 +646,7 @@ class JpqlSpecificationTests {
@Test
void theRest13() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT e.address AS addr
FROM Employee e
""");
@@ -628,7 +655,7 @@ class JpqlSpecificationTests {
@Test
void theRest14() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT AVG(o.quantity) FROM Order o
""");
}
@@ -636,7 +663,7 @@ class JpqlSpecificationTests {
@Test
void theRest15() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT SUM(l.price)
FROM Order o JOIN o.lineItems l JOIN o.customer c
WHERE c.lastname = 'Smith' AND c.firstname = 'John'
@@ -646,7 +673,7 @@ class JpqlSpecificationTests {
@Test
void theRest16() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT COUNT(o) FROM Order o
""");
}
@@ -654,7 +681,7 @@ class JpqlSpecificationTests {
@Test
void theRest17() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT COUNT(l.price)
FROM Order o JOIN o.lineItems l JOIN o.customer c
WHERE c.lastname = 'Smith' AND c.firstname = 'John'
@@ -664,7 +691,7 @@ class JpqlSpecificationTests {
@Test
void theRest18() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT COUNT(l)
FROM Order o JOIN o.lineItems l JOIN o.customer c
WHERE c.lastname = 'Smith' AND c.firstname = 'John' AND l.price IS NOT NULL
@@ -674,7 +701,7 @@ class JpqlSpecificationTests {
@Test
void theRest19() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT o
FROM Customer c JOIN c.orders o JOIN c.address a
WHERE a.state = 'CA'
@@ -685,7 +712,7 @@ class JpqlSpecificationTests {
@Test
void theRest20() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT o.quantity, a.zipcode
FROM Customer c JOIN c.orders o JOIN c.address a
WHERE a.state = 'CA'
@@ -696,8 +723,8 @@ class JpqlSpecificationTests {
@Test
void theRest21() {
JpqlQueryParser.parseQuery("""
SELECT o.quantity, o.cost*1.08 AS taxedCost, a.zipcode
assertQuery("""
SELECT o.quantity, o.cost * 1.08 AS taxedCost, a.zipcode
FROM Customer c JOIN c.orders o JOIN c.address a
WHERE a.state = 'CA' AND a.county = 'Santa Clara'
ORDER BY o.quantity, taxedCost, a.zipcode
@@ -707,7 +734,7 @@ class JpqlSpecificationTests {
@Test
void theRest22() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT AVG(o.quantity) as q, a.zipcode
FROM Customer c JOIN c.orders o JOIN c.address a
WHERE a.state = 'CA'
@@ -719,7 +746,7 @@ class JpqlSpecificationTests {
@Test
void theRest23() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT p.product_name
FROM Order o JOIN o.lineItems l JOIN l.product p JOIN o.customer c
WHERE c.lastname = 'Smith' AND c.firstname = 'John'
@@ -734,7 +761,7 @@ class JpqlSpecificationTests {
void theRest24() {
assertThatExceptionOfType(BadJpqlGrammarException.class).isThrownBy(() -> {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT p.product_name
FROM Order o, IN(o.lineItems) l JOIN o.customer c
WHERE c.lastname = 'Smith' AND c.firstname = 'John'
@@ -746,7 +773,7 @@ class JpqlSpecificationTests {
@Test
void theRest25() {
JpqlQueryParser.parseQuery("""
assertQuery("""
DELETE
FROM Customer c
WHERE c.status = 'inactive'
@@ -756,7 +783,7 @@ class JpqlSpecificationTests {
@Test
void theRest26() {
JpqlQueryParser.parseQuery("""
assertQuery("""
DELETE
FROM Customer c
WHERE c.status = 'inactive'
@@ -767,7 +794,7 @@ class JpqlSpecificationTests {
@Test
void theRest27() {
JpqlQueryParser.parseQuery("""
assertQuery("""
UPDATE Customer c
SET c.status = 'outstanding'
WHERE c.balance < 10000
@@ -777,7 +804,7 @@ class JpqlSpecificationTests {
@Test
void theRest28() {
JpqlQueryParser.parseQuery("""
assertQuery("""
UPDATE Employee e
SET e.address.building = 22
WHERE e.address.building = 14
@@ -789,7 +816,7 @@ class JpqlSpecificationTests {
@Test
void theRest29() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT o
FROM Order o
""");
@@ -798,7 +825,7 @@ class JpqlSpecificationTests {
@Test
void theRest30() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT o
FROM Order o
WHERE o.shippingAddress.state = 'CA'
@@ -808,7 +835,7 @@ class JpqlSpecificationTests {
@Test
void theRest31() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT o.shippingAddress.state
FROM Order o
""");
@@ -817,7 +844,7 @@ class JpqlSpecificationTests {
@Test
void theRest32() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT o
FROM Order o JOIN o.lineItems l
""");
@@ -826,7 +853,7 @@ class JpqlSpecificationTests {
@Test
void theRest33() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT o
FROM Order o
WHERE o.lineItems IS NOT EMPTY
@@ -836,7 +863,7 @@ class JpqlSpecificationTests {
@Test
void theRest34() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT o
FROM Order o
WHERE o.lineItems IS EMPTY
@@ -846,7 +873,7 @@ class JpqlSpecificationTests {
@Test
void theRest35() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT o
FROM Order o JOIN o.lineItems l
WHERE l.shipped = FALSE
@@ -856,7 +883,7 @@ class JpqlSpecificationTests {
@Test
void theRest36() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT o
FROM Order o
WHERE
@@ -869,7 +896,7 @@ class JpqlSpecificationTests {
@Test
void theRest37() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT o
FROM Order o
WHERE o.shippingAddress <> o.billingAddress
@@ -879,7 +906,7 @@ class JpqlSpecificationTests {
@Test
void theRest38() {
JpqlQueryParser.parseQuery("""
assertQuery("""
SELECT DISTINCT o
FROM Order o JOIN o.lineItems l
WHERE l.product.name = ?1

View File

@@ -17,6 +17,7 @@ package org.springframework.data.jpa.repository.query;
import static org.assertj.core.api.Assertions.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -450,7 +451,7 @@ class StringQueryUnitTests {
assertThat(
new StringQuery("select new com.example.Dto(a.foo, a.bar) from A a", false).hasConstructorExpression())
.isTrue();
.isTrue();
assertThat(new StringQuery("select new com.example.Dto (a.foo, a.bar) from A a", false).hasConstructorExpression())
.isTrue();
assertThat(new StringQuery("select a from A a", true).hasConstructorExpression()).isFalse();
@@ -490,8 +491,10 @@ class StringQueryUnitTests {
checkProjection("select something from Entity something", "something", "single expression", false);
checkProjection("select x, y, z from Entity something", "x, y, z", "tuple", false);
checkProjection("sect x, y, z from Entity something", "", "missing select", false);
checkProjection("select x, y, z fron Entity something", "", "missing from", false);
assertThatExceptionOfType(BadJpqlGrammarException.class)
.isThrownBy(() -> checkProjection("sect x, y, z from Entity something", "", "missing select", false));
assertThatExceptionOfType(BadJpqlGrammarException.class)
.isThrownBy(() -> checkProjection("select x, y, z fron Entity something", "", "missing from", false));
}
void checkProjection(String query, String expected, String description, boolean nativeQuery) {
@@ -615,9 +618,9 @@ class StringQueryUnitTests {
assertThat(new StringQuery(testQuery, false) //
.usesJdbcStyleParameters()) //
.describedAs(testQuery) //
.describedAs(testQuery) //
.isFalse();
.describedAs(testQuery) //
.describedAs(testQuery) //
.isFalse();
}
}
@@ -716,7 +719,11 @@ class StringQueryUnitTests {
private void checkHasNamedParameter(String query, boolean expected, String label, boolean nativeQuery) {
assertThat(new StringQuery(query, nativeQuery).hasNamedParameter()) //
List<ParameterBinding> bindings = new ArrayList<>();
StringQuery.ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query,
bindings, new StringQuery.Metadata());
assertThat(bindings.stream().anyMatch(it -> it.getIdentifier().hasName())) //
.describedAs(String.format("<%s> (%s)", query, label)) //
.isEqualTo(expected);
}