diff --git a/pom.xml b/pom.xml index b82816e812..fcd5af54c6 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-GH-3902-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 43c08369f6..3abac42017 100755 --- a/spring-data-envers/pom.xml +++ b/spring-data-envers/pom.xml @@ -5,12 +5,12 @@ org.springframework.data spring-data-envers - 4.0.0-SNAPSHOT + 4.0.0-GH-3902-SNAPSHOT org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-GH-3902-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index af5244a230..1b16cd8703 100644 --- a/spring-data-jpa-distribution/pom.xml +++ b/spring-data-jpa-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-GH-3902-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index cdb738558f..5707317dcc 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-jpa - 4.0.0-SNAPSHOT + 4.0.0-GH-3902-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-GH-3902-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 index 86b0111ca1..71d9c05ab6 100644 --- a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 +++ b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 @@ -18,10 +18,9 @@ grammar Eql; @header { /** * Implementation of EclipseLink Query Language (EQL) - * See: - * * https://eclipse.dev/eclipselink/documentation/3.0/jpa/extensions/jpql.htm - * * https://wiki.eclipse.org/EclipseLink/UserGuide/JPA/Basic_JPA_Development/Querying/JPQL * + * @see https://wiki.eclipse.org/EclipseLink/UserGuide/JPA/Basic_JPA_Development/Querying/JPQL + * @see https://eclipse.dev/eclipselink/documentation/3.0/jpa/extensions/jpql.htm * @author Greg Turnquist * @author Christoph Strobl * @since 3.2 @@ -43,7 +42,8 @@ ql_statement ; select_statement - : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (set_fuction)? + : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (set_fuction)? # SelectQuery + | from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (set_fuction)? # FromQuery ; setOperator @@ -80,7 +80,7 @@ identification_variable_declaration ; range_variable_declaration - : (entity_name|function_invocation) AS? identification_variable + : (entity_name|function_invocation) AS? identification_variable? ; join @@ -246,14 +246,15 @@ orderby_clause : ORDER BY orderby_item (',' orderby_item)* ; -// TODO Error in spec BNF, correctly shown elsewhere in spec. orderby_item - : state_field_path_expression (ASC | DESC)? nullsPrecedence? - | general_identification_variable (ASC | DESC)? nullsPrecedence? - | result_variable (ASC | DESC)? nullsPrecedence? - | string_expression (ASC | DESC)? nullsPrecedence? - | scalar_expression (ASC | DESC)? nullsPrecedence? - | + : orderby_expression (ASC | DESC)? nullsPrecedence? + ; + +orderby_expression + : state_field_path_expression + | general_identification_variable + | string_expression + | scalar_expression ; nullsPrecedence diff --git a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 index b3afdb9b1e..6632690a42 100644 --- a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 +++ b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 @@ -43,7 +43,8 @@ ql_statement ; select_statement - : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (set_fuction)? + : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (set_fuction)? # SelectQuery + | from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (set_fuction)? # FromQuery ; setOperator @@ -72,6 +73,7 @@ from_clause identificationVariableDeclarationOrCollectionMemberDeclaration : identification_variable_declaration | collection_member_declaration + | '(' subquery ')' identification_variable ; identification_variable_declaration @@ -79,11 +81,11 @@ identification_variable_declaration ; range_variable_declaration - : entity_name AS? identification_variable + : entity_name AS? identification_variable? ; join - : join_spec join_association_path_expression AS? identification_variable (join_condition)? + : join_spec join_association_path_expression AS? identification_variable? (join_condition)? ; fetch_join @@ -106,11 +108,11 @@ join_association_path_expression ; join_collection_valued_path_expression - : identification_variable '.' (single_valued_embeddable_object_field '.')* collection_valued_field + : (identification_variable '.')? (single_valued_embeddable_object_field '.')* collection_valued_field ; join_single_valued_path_expression - : identification_variable '.' (single_valued_embeddable_object_field '.')* single_valued_object_field + : (identification_variable '.')? (single_valued_embeddable_object_field '.')* single_valued_object_field ; collection_member_declaration @@ -244,9 +246,15 @@ orderby_clause : ORDER BY orderby_item (',' orderby_item)* ; -// TODO Error in spec BNF, correctly shown elsewhere in spec. orderby_item - : (state_field_path_expression | general_identification_variable | result_variable ) (ASC | DESC)? nullsPrecedence? + : orderby_expression (ASC | DESC)? nullsPrecedence? + ; + +orderby_expression + : state_field_path_expression + | general_identification_variable + | string_expression + | scalar_expression ; nullsPrecedence diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlCountQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlCountQueryTransformer.java index 2d8e27c167..9b4f06b381 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlCountQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlCountQueryTransformer.java @@ -17,10 +17,11 @@ import static org.springframework.data.jpa.repository.query.QueryTokens.*; -import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; - import org.jspecify.annotations.Nullable; + +import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; import org.springframework.data.jpa.repository.query.QueryTransformers.CountSelectionTokenStream; +import org.springframework.util.StringUtils; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed EQL query into a @@ -43,7 +44,7 @@ class EqlCountQueryTransformer extends EqlQueryRenderer { } @Override - public QueryTokenStream visitSelect_statement(EqlParser.Select_statementContext ctx) { + public QueryTokenStream visitSelectQuery(EqlParser.SelectQueryContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -63,6 +64,49 @@ public QueryTokenStream visitSelect_statement(EqlParser.Select_statementContext return builder; } + @Override + public QueryTokenStream visitFromQuery(EqlParser.FromQueryContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + QueryRendererBuilder countBuilder = QueryRenderer.builder(); + countBuilder.append(TOKEN_SELECT_COUNT); + + if (countProjection != null) { + countBuilder.append(QueryTokens.token(countProjection)); + } else { + if (primaryFromAlias == null) { + countBuilder.append(TOKEN_DOUBLE_UNDERSCORE); + } else { + countBuilder.append(QueryTokens.token(primaryFromAlias)); + } + } + + countBuilder.append(TOKEN_CLOSE_PAREN); + + builder.appendExpression(countBuilder); + + if (ctx.from_clause() != null) { + builder.appendExpression(visit(ctx.from_clause())); + if (primaryFromAlias == null) { + builder.append(TOKEN_AS); + builder.append(TOKEN_DOUBLE_UNDERSCORE); + } + } + + 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 QueryTokenStream visitSelect_clause(EqlParser.Select_clauseContext ctx) { @@ -78,14 +122,21 @@ public QueryTokenStream visitSelect_clause(EqlParser.Select_clauseContext ctx) { if (usesDistinct) { nested.append(QueryTokens.expression(ctx.DISTINCT())); nested.append(getDistinctCountSelection(QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA))); - } else { + } else if (StringUtils.hasText(primaryFromAlias)) { nested.append(QueryTokens.token(primaryFromAlias)); + } else { + if (ctx.select_item().isEmpty()) { + // cannot happen as per grammar, but you never know… + nested.append(QueryTokens.token("1")); + } else { + nested.append(visit(ctx.select_item().get(0))); + } } } else { - builder.append(QueryTokens.token(countProjection)); if (usesDistinct) { nested.append(QueryTokens.expression(ctx.DISTINCT())); } + nested.append(QueryTokens.token(countProjection)); } builder.appendInline(nested); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryIntrospector.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryIntrospector.java index fa7fa5ec8e..71f65523b8 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryIntrospector.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryIntrospector.java @@ -21,8 +21,6 @@ import java.util.Collections; import java.util.List; -import org.springframework.data.jpa.repository.query.EqlParser.Range_variable_declarationContext; - import org.jspecify.annotations.Nullable; /** @@ -61,8 +59,9 @@ public Void visitSelect_clause(EqlParser.Select_clauseContext ctx) { @Override public Void visitRange_variable_declaration(EqlParser.Range_variable_declarationContext ctx) { - if (primaryFromAlias == null) { - primaryFromAlias = capturePrimaryAlias(ctx); + if (primaryFromAlias == null && ctx.identification_variable() != null && !EqlQueryRenderer.isSubquery(ctx) + && !EqlQueryRenderer.isSetQuery(ctx)) { + primaryFromAlias = ctx.identification_variable().getText(); } return super.visitRange_variable_declaration(ctx); @@ -75,11 +74,6 @@ public Void visitConstructor_expression(EqlParser.Constructor_expressionContext return super.visitConstructor_expression(ctx); } - private static String capturePrimaryAlias(Range_variable_declarationContext ctx) { - return ctx.identification_variable() != null ? ctx.identification_variable().getText() - : ctx.entity_name().getText(); - } - private static List captureSelectItems(List selections, EqlQueryRenderer itemRenderer) { @@ -94,4 +88,5 @@ private static List captureSelectItems(List { + /** + * Is this AST tree a {@literal subquery}? + * + * @return boolean + */ + static boolean isSubquery(ParserRuleContext ctx) { + + if (ctx instanceof EqlParser.SubqueryContext) { + return true; + } else if (ctx instanceof EqlParser.Update_statementContext) { + return false; + } else if (ctx instanceof EqlParser.Delete_statementContext) { + return false; + } else { + return ctx.getParent() != null && isSubquery(ctx.getParent()); + } + } + + /** + * Is this AST tree a {@literal set} query that has been added through {@literal UNION|INTERSECT|EXCEPT}? + * + * @return boolean + */ + static boolean isSetQuery(ParserRuleContext ctx) { + + if (ctx instanceof EqlParser.Set_fuctionContext) { + return true; + } + + return ctx.getParent() != null && isSetQuery(ctx.getParent()); + } + @Override public QueryTokenStream visitStart(EqlParser.StartContext ctx) { return visit(ctx.ql_statement()); @@ -56,7 +90,7 @@ public QueryTokenStream visitQl_statement(EqlParser.Ql_statementContext ctx) { } @Override - public QueryTokenStream visitSelect_statement(EqlParser.Select_statementContext ctx) { + public QueryTokenStream visitSelectQuery(EqlParser.SelectQueryContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -86,6 +120,36 @@ public QueryTokenStream visitSelect_statement(EqlParser.Select_statementContext return builder; } + @Override + public QueryTokenStream visitFromQuery(EqlParser.FromQueryContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + 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())); + } + + if (ctx.orderby_clause() != null) { + builder.appendExpression(visit(ctx.orderby_clause())); + } + + if (ctx.set_fuction() != null) { + builder.appendExpression(visit(ctx.set_fuction())); + } + + return builder; + } + @Override public QueryTokenStream visitUpdate_statement(EqlParser.Update_statementContext ctx) { @@ -149,7 +213,9 @@ public QueryTokenStream visitIdentificationVariableDeclarationOrCollectionMember QueryRendererBuilder builder = QueryRenderer.builder(); builder.appendExpression(nested); - builder.appendExpression(visit(ctx.identification_variable())); + if (ctx.identification_variable() != null) { + builder.appendExpression(visit(ctx.identification_variable())); + } return builder; } else { @@ -185,7 +251,9 @@ public QueryTokenStream visitRange_variable_declaration(EqlParser.Range_variable builder.append(QueryTokens.expression(ctx.AS())); } - builder.appendExpression(visit(ctx.identification_variable())); + if (ctx.identification_variable() != null) { + builder.appendExpression(visit(ctx.identification_variable())); + } return builder; } @@ -309,6 +377,7 @@ public QueryTokenStream visitJoin_collection_valued_path_expression( EqlParser.Join_collection_valued_path_expressionContext ctx) { List items = new ArrayList<>(2 + ctx.single_valued_embeddable_object_field().size()); + if (ctx.identification_variable() != null) { items.add(ctx.identification_variable()); } @@ -348,7 +417,9 @@ public QueryTokenStream visitCollection_member_declaration(EqlParser.Collection_ builder.append(QueryTokens.expression(ctx.AS())); } - builder.appendExpression(visit(ctx.identification_variable())); + if (ctx.identification_variable() != null) { + builder.appendExpression(visit(ctx.identification_variable())); + } return builder; } @@ -823,22 +894,11 @@ public QueryTokenStream visitOrderby_item(EqlParser.Orderby_itemContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.state_field_path_expression() != null) { - builder.appendExpression(visit(ctx.state_field_path_expression())); - } else if (ctx.general_identification_variable() != null) { - builder.appendExpression(visit(ctx.general_identification_variable())); - } else if (ctx.result_variable() != null) { - builder.appendExpression(visit(ctx.result_variable())); - } else if (ctx.string_expression() != null) { - builder.appendExpression(visit(ctx.string_expression())); - } else if (ctx.scalar_expression() != null) { - builder.appendExpression(visit(ctx.scalar_expression())); - } + builder.appendExpression(visit(ctx.orderby_expression())); if (ctx.ASC() != null) { builder.append(QueryTokens.expression(ctx.ASC())); - } - if (ctx.DESC() != null) { + } else if (ctx.DESC() != null) { builder.append(QueryTokens.expression(ctx.DESC())); } @@ -849,6 +909,22 @@ public QueryTokenStream visitOrderby_item(EqlParser.Orderby_itemContext ctx) { return builder; } + @Override + public QueryTokenStream visitOrderby_expression(EqlParser.Orderby_expressionContext ctx) { + + if (ctx.state_field_path_expression() != null) { + return visit(ctx.state_field_path_expression()); + } else if (ctx.general_identification_variable() != null) { + return visit(ctx.general_identification_variable()); + } else if (ctx.string_expression() != null) { + return visit(ctx.string_expression()); + } else if (ctx.scalar_expression() != null) { + return visit(ctx.scalar_expression()); + } + + return QueryTokenStream.empty(); + } + @Override public QueryTokenStream visitNullsPrecedence(EqlParser.NullsPrecedenceContext ctx) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java index 30e9106d22..15cb6b5767 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java @@ -19,9 +19,9 @@ import java.util.List; -import org.springframework.data.domain.Sort; - import org.jspecify.annotations.Nullable; + +import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; import org.springframework.data.repository.query.ReturnedType; import org.springframework.util.Assert; @@ -54,7 +54,7 @@ class EqlSortedQueryTransformer extends EqlQueryRenderer { } @Override - public QueryTokenStream visitSelect_statement(EqlParser.Select_statementContext ctx) { + public QueryTokenStream visitSelectQuery(EqlParser.SelectQueryContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -76,7 +76,35 @@ public QueryTokenStream visitSelect_statement(EqlParser.Select_statementContext if (ctx.set_fuction() != null) { builder.appendExpression(visit(ctx.set_fuction())); } else { - doVisitOrderBy(builder, ctx); + doVisitOrderBy(builder, ctx.orderby_clause()); + } + + return builder; + } + + @Override + public QueryTokenStream visitFromQuery(EqlParser.FromQueryContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + 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())); + } + + if (ctx.set_fuction() != null) { + builder.appendExpression(visit(ctx.set_fuction())); + } else { + doVisitOrderBy(builder, ctx.orderby_clause()); } return builder; @@ -102,10 +130,10 @@ public QueryTokenStream visitSelect_clause(EqlParser.Select_clauseContext ctx) { return builder.append(dtoDelegate.transformSelectionList(tokenStream)); } - private void doVisitOrderBy(QueryRendererBuilder builder, EqlParser.Select_statementContext ctx) { + private void doVisitOrderBy(QueryRendererBuilder builder, EqlParser.Orderby_clauseContext ctx) { - if (ctx.orderby_clause() != null) { - QueryTokenStream existingOrder = visit(ctx.orderby_clause()); + if (ctx != null) { + QueryTokenStream existingOrder = visit(ctx); if (sort.isSorted()) { builder.appendInline(existingOrder); } else { @@ -117,7 +145,7 @@ private void doVisitOrderBy(QueryRendererBuilder builder, EqlParser.Select_state List sortBy = transformerSupport.orderBy(primaryFromAlias, sort); - if (ctx.orderby_clause() != null) { + if (ctx != null) { QueryRendererBuilder extension = QueryRenderer.builder().append(TOKEN_COMMA).append(sortBy); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java index e35b712589..6f9c3bb878 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java @@ -22,6 +22,7 @@ import org.springframework.data.jpa.repository.query.HqlParser.SelectClauseContext; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; import org.springframework.data.jpa.repository.query.QueryTransformers.CountSelectionTokenStream; +import org.springframework.util.StringUtils; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed HQL query into a @@ -107,7 +108,7 @@ public QueryRendererBuilder visitFromQuery(HqlParser.FromQueryContext ctx) { if (ctx.fromClause() != null) { builder.appendExpression(visit(ctx.fromClause())); - if(primaryFromAlias == null) { + if (primaryFromAlias == null) { builder.append(TOKEN_AS); builder.append(TOKEN_DOUBLE_UNDERSCORE); } @@ -150,7 +151,6 @@ public QueryRendererBuilder visitJoin(HqlParser.JoinContext ctx) { return builder; } - @Override public QueryTokenStream visitSelectClause(HqlParser.SelectClauseContext ctx) { @@ -165,11 +165,9 @@ public QueryTokenStream visitSelectClause(HqlParser.SelectClauseContext ctx) { boolean usesDistinct = ctx.DISTINCT() != null; QueryRendererBuilder nested = QueryRenderer.builder(); if (countProjection == null) { - QueryTokenStream selection = visit(ctx.selectionList()); if (usesDistinct) { - nested.append(QueryTokens.expression(ctx.DISTINCT())); - nested.append(getDistinctCountSelection(selection)); + nested.append(getDistinctCountSelection(visit(ctx.selectionList()))); } else { // with CTE primary alias fails with hibernate (WITH entities AS (…) SELECT count(c) FROM entities c) @@ -177,9 +175,7 @@ public QueryTokenStream visitSelectClause(HqlParser.SelectClauseContext ctx) { nested.append(QueryTokens.token("*")); } else { - if (selection.size() == 1) { - nested.append(selection); - } else if (primaryFromAlias != null) { + if (StringUtils.hasText(primaryFromAlias)) { nested.append(QueryTokens.token(primaryFromAlias)); } else { nested.append(QueryTokens.token("*")); @@ -187,10 +183,10 @@ public QueryTokenStream visitSelectClause(HqlParser.SelectClauseContext ctx) { } } } else { - builder.append(QueryTokens.token(countProjection)); if (usesDistinct) { nested.append(QueryTokens.expression(ctx.DISTINCT())); } + nested.append(QueryTokens.token(countProjection)); } builder.appendInline(nested); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryIntrospector.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryIntrospector.java index ba88ab2df1..c32bf27d3f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryIntrospector.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryIntrospector.java @@ -69,7 +69,8 @@ public Void visitCte(HqlParser.CteContext ctx) { @Override public Void visitRootEntity(HqlParser.RootEntityContext ctx) { - if (this.primaryFromAlias == null && ctx.variable() != null && !HqlQueryRenderer.isSubquery(ctx)) { + if (this.primaryFromAlias == null && ctx.variable() != null && !HqlQueryRenderer.isSubquery(ctx) + && !HqlQueryRenderer.isSetQuery(ctx)) { this.primaryFromAlias = capturePrimaryAlias(ctx.variable()); } @@ -79,7 +80,8 @@ public Void visitRootEntity(HqlParser.RootEntityContext ctx) { @Override public Void visitRootSubquery(HqlParser.RootSubqueryContext ctx) { - if (this.primaryFromAlias == null && ctx.variable() != null && !HqlQueryRenderer.isSubquery(ctx)) { + if (this.primaryFromAlias == null && ctx.variable() != null && !HqlQueryRenderer.isSubquery(ctx) + && !HqlQueryRenderer.isSetQuery(ctx)) { this.primaryFromAlias = capturePrimaryAlias(ctx.variable()); } @@ -89,7 +91,8 @@ public Void visitRootSubquery(HqlParser.RootSubqueryContext ctx) { @Override public Void visitRootFunction(HqlParser.RootFunctionContext ctx) { - if (this.primaryFromAlias == null && ctx.variable() != null && !HqlQueryRenderer.isSubquery(ctx)) { + if (this.primaryFromAlias == null && ctx.variable() != null && !HqlQueryRenderer.isSubquery(ctx) + && !HqlQueryRenderer.isSetQuery(ctx)) { this.primaryFromAlias = capturePrimaryAlias(ctx.variable()); this.hasFromFunction = true; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java index 01557b33d5..3e6f0175e1 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java @@ -38,7 +38,7 @@ class HqlQueryRenderer extends HqlBaseVisitor { /** - * Is this select clause a {@literal subquery}? + * Is this AST tree a {@literal subquery}? * * @return boolean */ @@ -59,6 +59,23 @@ static boolean isSubquery(ParserRuleContext ctx) { } } + /** + * Is this AST tree a {@literal set} query that has been added through {@literal UNION|INTERSECT|EXCEPT}? + * + * @return boolean + */ + static boolean isSetQuery(ParserRuleContext ctx) { + + if (ctx instanceof HqlParser.OrderedQueryContext + && ctx.getParent() instanceof HqlParser.QueryExpressionContext qec) { + if (qec.orderedQuery().indexOf(ctx) != 0) { + return true; + } + } + + return ctx.getParent() != null && isSetQuery(ctx.getParent()); + } + @Override public QueryTokenStream visitStart(HqlParser.StartContext ctx) { return visit(ctx.ql_statement()); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java index 6318d8acfd..af7686fac2 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java @@ -44,7 +44,7 @@ class JpqlCountQueryTransformer extends JpqlQueryRenderer { } @Override - public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) { + public QueryTokenStream visitSelectQuery(JpqlParser.SelectQueryContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -60,8 +60,48 @@ public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext if (ctx.having_clause() != null) { builder.appendExpression(visit(ctx.having_clause())); } - if (ctx.set_fuction() != null) { - builder.appendExpression(visit(ctx.set_fuction())); + + return builder; + } + + @Override + public QueryTokenStream visitFromQuery(JpqlParser.FromQueryContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + QueryRendererBuilder countBuilder = QueryRenderer.builder(); + countBuilder.append(TOKEN_SELECT_COUNT); + + if (countProjection != null) { + countBuilder.append(QueryTokens.token(countProjection)); + } else { + if (primaryFromAlias == null) { + countBuilder.append(TOKEN_DOUBLE_UNDERSCORE); + } else { + countBuilder.append(QueryTokens.token(primaryFromAlias)); + } + } + + countBuilder.append(TOKEN_CLOSE_PAREN); + + builder.appendExpression(countBuilder); + + if (ctx.from_clause() != null) { + builder.appendExpression(visit(ctx.from_clause())); + if (primaryFromAlias == null) { + builder.append(TOKEN_AS); + builder.append(TOKEN_DOUBLE_UNDERSCORE); + } + } + + 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; @@ -85,13 +125,18 @@ public QueryRendererBuilder visitSelect_clause(JpqlParser.Select_clauseContext c } else if (StringUtils.hasText(primaryFromAlias)) { nested.append(QueryTokens.token(primaryFromAlias)); } else { - throw new IllegalStateException("No primary alias present"); + if (ctx.select_item().isEmpty()) { + // cannot happen as per grammar, but you never know… + nested.append(QueryTokens.token("1")); + } else { + nested.append(visit(ctx.select_item().get(0))); + } } } else { - builder.append(QueryTokens.token(countProjection)); if (usesDistinct) { nested.append(QueryTokens.expression(ctx.DISTINCT())); } + nested.append(QueryTokens.token(countProjection)); } builder.appendInline(nested); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryIntrospector.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryIntrospector.java index 43f6f7fd1f..f819778ce6 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryIntrospector.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryIntrospector.java @@ -46,39 +46,34 @@ public QueryInformation getParsedQueryInformation() { } @Override - public Void visitRange_variable_declaration(JpqlParser.Range_variable_declarationContext ctx) { + public Void visitSelect_clause(JpqlParser.Select_clauseContext ctx) { - if (primaryFromAlias == null) { - primaryFromAlias = capturePrimaryAlias(ctx); + if (!projectionProcessed) { + projection = captureSelectItems(ctx.select_item(), renderer); + projectionProcessed = true; } - return super.visitRange_variable_declaration(ctx); + return super.visitSelect_clause(ctx); } @Override - public Void visitSelect_clause(JpqlParser.Select_clauseContext ctx) { + public Void visitRange_variable_declaration(JpqlParser.Range_variable_declarationContext ctx) { - if (!projectionProcessed) { - projection = captureSelectItems(ctx.select_item(), renderer); - projectionProcessed = true; + if (primaryFromAlias == null && ctx.identification_variable() != null && !JpqlQueryRenderer.isSubquery(ctx) + && !JpqlQueryRenderer.isSetQuery(ctx)) { + primaryFromAlias = ctx.identification_variable().getText(); } - return super.visitSelect_clause(ctx); + return super.visitRange_variable_declaration(ctx); } @Override public Void visitConstructor_expression(JpqlParser.Constructor_expressionContext ctx) { hasConstructorExpression = true; - return super.visitConstructor_expression(ctx); } - private static String capturePrimaryAlias(JpqlParser.Range_variable_declarationContext ctx) { - return ctx.identification_variable() != null ? ctx.identification_variable().getText() - : ctx.entity_name().getText(); - } - private static List captureSelectItems(List selections, JpqlQueryRenderer itemRenderer) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java index 03b87cdd34..75677568ca 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.List; +import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.tree.ParseTree; import org.springframework.data.jpa.repository.query.JpqlParser.NullsPrecedenceContext; @@ -35,11 +36,44 @@ * * @author Greg Turnquist * @author Christoph Strobl + * @author Mark Paluch * @since 3.1 */ @SuppressWarnings({ "ConstantConditions", "DuplicatedCode" }) class JpqlQueryRenderer extends JpqlBaseVisitor { + /** + * Is this AST tree a {@literal subquery}? + * + * @return boolean + */ + static boolean isSubquery(ParserRuleContext ctx) { + + if (ctx instanceof JpqlParser.SubqueryContext) { + return true; + } else if (ctx instanceof JpqlParser.Update_statementContext) { + return false; + } else if (ctx instanceof JpqlParser.Delete_statementContext) { + return false; + } else { + return ctx.getParent() != null && isSubquery(ctx.getParent()); + } + } + + /** + * Is this AST tree a {@literal set} query that has been added through {@literal UNION|INTERSECT|EXCEPT}? + * + * @return boolean + */ + static boolean isSetQuery(ParserRuleContext ctx) { + + if (ctx instanceof JpqlParser.Set_fuctionContext) { + return true; + } + + return ctx.getParent() != null && isSetQuery(ctx.getParent()); + } + @Override public QueryTokenStream visitStart(JpqlParser.StartContext ctx) { return visit(ctx.ql_statement()); @@ -60,7 +94,7 @@ public QueryTokenStream visitQl_statement(JpqlParser.Ql_statementContext ctx) { } @Override - public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) { + public QueryTokenStream visitSelectQuery(JpqlParser.SelectQueryContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -90,6 +124,36 @@ public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext return builder; } + @Override + public QueryTokenStream visitFromQuery(JpqlParser.FromQueryContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + 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())); + } + + if (ctx.orderby_clause() != null) { + builder.appendExpression(visit(ctx.orderby_clause())); + } + + if (ctx.set_fuction() != null) { + builder.appendExpression(visit(ctx.set_fuction())); + } + + return builder; + } + @Override public QueryTokenStream visitUpdate_statement(JpqlParser.Update_statementContext ctx) { @@ -173,7 +237,9 @@ public QueryTokenStream visitRange_variable_declaration(JpqlParser.Range_variabl builder.append(QueryTokens.expression(ctx.AS())); } - builder.appendExpression(visit(ctx.identification_variable())); + if (ctx.identification_variable() != null) { + builder.appendExpression(visit(ctx.identification_variable())); + } return builder; } @@ -297,9 +363,12 @@ public QueryTokenStream visitJoin_association_path_expression( public QueryTokenStream visitJoin_collection_valued_path_expression( JpqlParser.Join_collection_valued_path_expressionContext ctx) { - List items = new ArrayList<>(3 + ctx.single_valued_embeddable_object_field().size()); + List items = new ArrayList<>(2 + ctx.single_valued_embeddable_object_field().size()); + + if (ctx.identification_variable() != null) { + items.add(ctx.identification_variable()); + } - items.add(ctx.identification_variable()); items.addAll(ctx.single_valued_embeddable_object_field()); items.add(ctx.collection_valued_field()); @@ -333,7 +402,9 @@ public QueryTokenStream visitCollection_member_declaration(JpqlParser.Collection builder.append(QueryTokens.expression(ctx.AS())); } - builder.appendExpression(visit(ctx.identification_variable())); + if (ctx.identification_variable() != null) { + builder.appendExpression(visit(ctx.identification_variable())); + } return builder; } @@ -581,6 +652,7 @@ public QueryTokenStream visitDelete_clause(JpqlParser.Delete_clauseContext ctx) builder.append(QueryTokens.expression(ctx.DELETE())); builder.append(QueryTokens.expression(ctx.FROM())); builder.appendExpression(visit(ctx.entity_name())); + if (ctx.AS() != null) { builder.append(QueryTokens.expression(ctx.AS())); } @@ -794,7 +866,7 @@ public QueryTokenStream visitOrderby_clause(JpqlParser.Orderby_clauseContext ctx builder.append(QueryTokens.expression(ctx.ORDER())); builder.append(QueryTokens.expression(ctx.BY())); - builder.appendExpression(QueryTokenStream.concat(ctx.orderby_item(), this::visit, TOKEN_COMMA)); + builder.append(QueryTokenStream.concat(ctx.orderby_item(), this::visit, TOKEN_COMMA)); return builder; } @@ -804,13 +876,7 @@ public QueryTokenStream visitOrderby_item(JpqlParser.Orderby_itemContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.state_field_path_expression() != null) { - builder.appendExpression(visit(ctx.state_field_path_expression())); - } else if (ctx.general_identification_variable() != null) { - builder.appendExpression(visit(ctx.general_identification_variable())); - } else if (ctx.result_variable() != null) { - builder.appendExpression(visit(ctx.result_variable())); - } + builder.appendExpression(visit(ctx.orderby_expression())); if (ctx.ASC() != null) { builder.append(QueryTokens.expression(ctx.ASC())); @@ -819,12 +885,28 @@ public QueryTokenStream visitOrderby_item(JpqlParser.Orderby_itemContext ctx) { } if (ctx.nullsPrecedence() != null) { - builder.append(visit(ctx.nullsPrecedence())); + builder.appendExpression(visit(ctx.nullsPrecedence())); } return builder; } + @Override + public QueryTokenStream visitOrderby_expression(JpqlParser.Orderby_expressionContext ctx) { + + if (ctx.state_field_path_expression() != null) { + return visit(ctx.state_field_path_expression()); + } else if (ctx.general_identification_variable() != null) { + return visit(ctx.general_identification_variable()); + } else if (ctx.string_expression() != null) { + return visit(ctx.string_expression()); + } else if (ctx.scalar_expression() != null) { + return visit(ctx.scalar_expression()); + } + + return QueryTokenStream.empty(); + } + @Override public QueryTokenStream visitNullsPrecedence(NullsPrecedenceContext ctx) { @@ -1974,9 +2056,11 @@ public QueryTokenStream visitType_cast_function(JpqlParser.Type_cast_functionCon builder.append(QueryTokens.token(ctx.CAST())); builder.append(TOKEN_OPEN_PAREN); builder.appendExpression(visit(ctx.scalar_expression())); + if (ctx.AS() != null) { builder.append(QueryTokens.expression(ctx.AS())); } + builder.appendInline(visit(ctx.identification_variable())); if (!CollectionUtils.isEmpty(ctx.numeric_literal())) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java index 654fb7df88..70efd1af1b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java @@ -19,9 +19,9 @@ import java.util.List; -import org.springframework.data.domain.Sort; - import org.jspecify.annotations.Nullable; + +import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; import org.springframework.data.repository.query.ReturnedType; import org.springframework.util.Assert; @@ -53,7 +53,7 @@ class JpqlSortedQueryTransformer extends JpqlQueryRenderer { } @Override - public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) { + public QueryTokenStream visitSelectQuery(JpqlParser.SelectQueryContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -72,10 +72,38 @@ public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext builder.appendExpression(visit(ctx.having_clause())); } - if(ctx.set_fuction() != null) { + if (ctx.set_fuction() != null) { + builder.appendExpression(visit(ctx.set_fuction())); + } else { + doVisitOrderBy(builder, ctx.orderby_clause()); + } + + return builder; + } + + @Override + public QueryTokenStream visitFromQuery(JpqlParser.FromQueryContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + 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())); + } + + if (ctx.set_fuction() != null) { builder.appendExpression(visit(ctx.set_fuction())); } else { - doVisitOrderBy(builder, ctx); + doVisitOrderBy(builder, ctx.orderby_clause()); } return builder; @@ -101,10 +129,10 @@ public QueryTokenStream visitSelect_clause(JpqlParser.Select_clauseContext ctx) return builder.append(dtoDelegate.transformSelectionList(tokenStream)); } - private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Select_statementContext ctx) { + private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Orderby_clauseContext ctx) { - if (ctx.orderby_clause() != null) { - QueryTokenStream existingOrder = visit(ctx.orderby_clause()); + if (ctx != null) { + QueryTokenStream existingOrder = visit(ctx); if (sort.isSorted()) { builder.appendInline(existingOrder); } else { @@ -116,7 +144,7 @@ private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Select_stat List sortBy = transformerSupport.orderBy(primaryFromAlias, sort); - if (ctx.orderby_clause() != null) { + if (ctx != null) { QueryRendererBuilder extension = QueryRenderer.builder().append(TOKEN_COMMA).append(sortBy); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java index 17188c06fb..c6c71a528b 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java @@ -69,6 +69,266 @@ private String reduceWhitespace(String original) { .trim(); } + @Test + void selectQueries() { + + assertQuery("Select e FROM Employee e WHERE e.salary > 100000"); + assertQuery("Select e FROM Employee e WHERE e.id = :id"); + assertQuery("Select MAX(e.salary) FROM Employee e"); + assertQuery("Select e.firstName FROM Employee e"); + assertQuery("Select e.firstName, e.lastName FROM Employee e"); + } + + @Test + void selectClause() { + + assertQuery("SELECT COUNT(e) FROM Employee e"); + assertQuery("SELECT MAX(e.salary) FROM Employee e"); + assertQuery("SELECT NEW com.acme.reports.EmpReport(e.firstName, e.lastName, e.salary) FROM Employee e"); + } + + @Test + void fromClause() { + + assertQuery("SELECT e FROM Employee e"); + assertQuery("SELECT e, a FROM Employee e, MailingAddress a WHERE e.address = a.address"); + assertQuery("SELECT e FROM com.acme.Employee e"); + } + + @Test + void join() { + + assertQuery("SELECT e FROM Employee e JOIN e.address a WHERE a.city = :city"); + assertQuery("SELECT e FROM Employee e JOIN e.projects p JOIN e.projects p2 WHERE p.name = :p1 AND p2.name = :p2"); + } + + @Test + void joinFetch() { + + assertQuery("SELECT e FROM Employee e JOIN FETCH e.address"); + assertQuery("SELECT e FROM Employee e JOIN FETCH e.address a ORDER BY a.city"); + assertQuery("SELECT e FROM Employee e JOIN FETCH e.address AS a ORDER BY a.city"); + } + + @Test + void leftJoin() { + assertQuery("SELECT e FROM Employee e LEFT JOIN e.address a ORDER BY a.city"); + } + + @Test // GH-3277 + void numericLiterals() { + + assertQuery("SELECT e FROM Employee e WHERE e.id = 1234"); + assertQuery("SELECT e FROM Employee e WHERE e.id = 1234L"); + assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14"); + assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14F"); + assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14e32D"); + } + + @Test // GH-3308 + void newWithStrings() { + assertQuery("select new com.example.demo.SampleObject(se.id, se.sampleValue, \"java\") from SampleEntity se"); + } + + @Test + void orderByClause() { + + assertQuery("SELECT e FROM Employee e ORDER BY e.lastName ASC, e.firstName ASC"); // Typo in EQL document + assertQuery("SELECT e FROM Employee e LEFT JOIN e.manager m ORDER BY m.lastName NULLS FIRST"); + assertQuery("SELECT e FROM Employee e ORDER BY e.address"); + } + + @Test + void groupByClause() { + + assertQuery("SELECT AVG(e.salary), e.address.city FROM Employee e GROUP BY e.address.city"); + assertQuery("SELECT e, COUNT(p) FROM Employee e LEFT JOIN e.projects p GROUP BY e"); + } + + @Test + void havingClause() { + assertQuery( + "SELECT AVG(e.salary), e.address.city FROM Employee e GROUP BY e.address.city HAVING AVG(e.salary) > 100000"); + } + + @Test // GH-3136 + void union() { + + assertQuery(""" + SELECT MAX(e.salary) FROM Employee e WHERE e.address.city = :city1 + UNION SELECT MAX(e.salary) FROM Employee e WHERE e.address.city = :city2 + """); + assertQuery(""" + SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode1 + INTERSECT SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode2 + """); + assertQuery(""" + SELECT e FROM Employee e + EXCEPT SELECT e FROM Employee e WHERE e.salary > e.manager.salary + """); + } + + @Test + void whereClause() { + // TBD + } + + @Test + void updateQueries() { + assertQuery("UPDATE Employee e SET e.salary = 60000 WHERE e.salary = 50000"); + } + + @Test + void deleteQueries() { + assertQuery("DELETE FROM Employee e WHERE e.department IS NULL"); + } + + @Test + void literals() { + + assertQuery("SELECT e FROM Employee e WHERE e.name = 'Bob'"); + assertQuery("SELECT e FROM Employee e WHERE e.id = 1234"); + assertQuery("SELECT e FROM Employee e WHERE e.id = 1234L"); + assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14F"); + assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14e32D"); + assertQuery("SELECT e FROM Employee e WHERE e.active = TRUE"); + assertQuery("SELECT e FROM Employee e WHERE e.startDate = {d'2012-01-03'}"); + assertQuery("SELECT e FROM Employee e WHERE e.startTime = {t'09:00:00'}"); + assertQuery("SELECT e FROM Employee e WHERE e.version = {ts'2012-01-03 09:00:00.000000001'}"); + assertQuery("SELECT e FROM Employee e WHERE e.gender = org.acme.Gender.MALE"); + assertQuery("UPDATE Employee e SET e.manager = NULL WHERE e.manager = :manager"); + } + + @Test + void functionsInSelect() { + + 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 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'"); + assertQuery( + "select case when e.firstName = 'Bob' then 'Robert' when e.firstName = 'Jill' then 'Gillian' else '' end from Employee e where e.firstName = 'Bob' or e.firstName = 'Jill'"); + assertQuery( + "select e from Employee e where case when e.firstName = 'Bob' then 'Robert' when e.firstName = 'Jill' then 'Gillian' else '' end = 'Robert'"); + assertQuery("SELECT COALESCE(e.salary, 0) FROM Employee e"); + assertQuery("SELECT CONCAT(e.firstName, ' ', e.lastName) FROM Employee e"); + assertQuery("SELECT e.name, CURRENT_DATE FROM Employee e"); + assertQuery("SELECT e.name, CURRENT_TIME FROM Employee e"); + assertQuery("SELECT e.name, CURRENT_TIMESTAMP FROM Employee e"); + assertQuery("SELECT LENGTH(e.lastName) FROM Employee e"); + assertQuery("SELECT LOWER(e.lastName) FROM Employee e"); + assertQuery("SELECT MOD(e.hoursWorked, 8) FROM Employee e"); + assertQuery("SELECT NULLIF(e.salary, 0) FROM Employee e"); + assertQuery("SELECT SQRT(o.RESULT) FROM Output o"); + assertQuery("SELECT SUBSTRING(e.lastName, 0, 2) FROM Employee e"); + assertQuery( + "SELECT TRIM(TRAILING FROM e.lastName), TRIM(e.lastName), TRIM(LEADING '-' FROM e.lastName) FROM Employee e"); + assertQuery("SELECT UPPER(e.lastName) FROM Employee e"); + assertQuery("SELECT CAST(e.salary NUMERIC(10, 2)) FROM Employee e"); + assertQuery("SELECT EXTRACT(YEAR FROM e.startDate) FROM Employee e"); + } + + @Test + void functionsInWhere() { + + 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 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'"); + assertQuery("SELECT e FROM Employee e WHERE CURRENT_DATE > CURRENT_TIME"); + assertQuery("SELECT e FROM Employee e WHERE CURRENT_TIME > CURRENT_TIMESTAMP"); + assertQuery("SELECT e FROM Employee e WHERE LENGTH(e.lastName) > 0"); + assertQuery("SELECT e FROM Employee e WHERE LOWER(e.lastName) = 'bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE MOD(e.hoursWorked, 8) > 0"); + assertQuery("SELECT e FROM Employee e WHERE SQRT(o.RESULT) > 0.0"); + assertQuery("SELECT e FROM Employee e WHERE SUBSTRING(e.lastName, 0, 2) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE TRIM(TRAILING FROM e.lastName) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE TRIM(e.lastName) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE TRIM(LEADING '-' FROM e.lastName) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE UPPER(e.lastName) = 'BILBO'"); + assertQuery("SELECT e FROM Employee e WHERE CAST(e.salary NUMERIC(10, 2)) > 0.0"); + assertQuery("SELECT e FROM Employee e WHERE EXTRACT(YEAR FROM e.startDate) = '2023'"); + } + + @Test + void specialOperators() { + + assertQuery("SELECT toDo FROM Employee e JOIN e.toDoList toDo WHERE INDEX(toDo) = 1"); + assertQuery("SELECT p FROM Employee e JOIN e.priorities p WHERE KEY(p) = 'high'"); + assertQuery("SELECT e FROM Employee e WHERE SIZE(e.managedEmployees) < 2"); + assertQuery("SELECT e FROM Employee e WHERE e.managedEmployees IS EMPTY"); + assertQuery("SELECT e FROM Employee e WHERE 'write code' MEMBER OF e.responsibilities"); + assertQuery("SELECT p FROM Project p WHERE TYPE(p) = LargeProject"); + + /** + * NOTE: The following query has been altered to properly align with EclipseLink test code despite NOT matching + * their ref docs. See https://github.com/eclipse-ee4j/eclipselink/issues/1949 for more details. + */ + assertQuery("SELECT e FROM Employee e JOIN TREAT(e.projects AS LargeProject) p WHERE p.budget > 1000000"); + + assertQuery("SELECT p FROM Phone p WHERE FUNCTION('TO_NUMBER', p.areaCode) > 613"); + } + + @Test // GH-3314 + void isNullAndIsNotNull() { + + assertQuery("SELECT e FROM Employee e WHERE (e.active IS null OR e.active = true)"); + assertQuery("SELECT e FROM Employee e WHERE (e.active IS NULL OR e.active = true)"); + assertQuery("SELECT e FROM Employee e WHERE (e.active IS NOT null OR e.active = true)"); + assertQuery("SELECT e FROM Employee e WHERE (e.active IS NOT NULL OR e.active = true)"); + } + + @Test // GH-3136 + void intersect() { + + assertQuery(""" + SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode1 + INTERSECT SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode2 + """); + } + + @Test // GH-3136 + void except() { + + assertQuery(""" + SELECT e FROM Employee e + EXCEPT SELECT e FROM Employee e WHERE e.salary > e.manager.salary + """); + } + + @ParameterizedTest // GH-3136 + @ValueSource(strings = { "STRING", "INTEGER", "FLOAT", "DOUBLE" }) + void cast(String targetType) { + assertQuery("SELECT CAST(e.salary AS %s) FROM Employee e".formatted(targetType)); + } + + @ParameterizedTest // GH-3136 + @ValueSource(strings = { "LEFT", "RIGHT" }) + void leftRightStringFunctions(String keyword) { + assertQuery("SELECT %s(e.name, 3) FROM Employee e".formatted(keyword)); + } + + @Test // GH-3136 + void replaceStringFunctions() { + assertQuery("SELECT REPLACE(e.name, 'o', 'a') FROM Employee e"); + assertQuery("SELECT REPLACE(e.name, ' ', '_') FROM Employee e"); + } + + @Test // GH-3136 + void stringConcatWithPipes() { + assertQuery("SELECT e.firstname || e.lastname AS name FROM Employee e"); + } + /** * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#example */ @@ -1170,4 +1430,27 @@ void reservedWordsShouldWork() { assertQuery("select f from FooEntity f where f.size IN :sizes"); } + @Test // GH-3902 + void queryWithoutSelectShouldWork() { + + assertQuery("from Person p"); + assertQuery("from Person p WHERE p.name = 'John' ORDER BY p.name"); + } + + @Test // GH-3902 + void queryWithoutSelectAndIdentificationVariableShouldWork() { + + assertQuery("from Person"); + assertQuery("from Person WHERE name = 'John' ORDER BY name"); + assertQuery("from Person JOIN department WHERE name = 'John' ORDER BY name"); + } + + @Test // GH-3902 + void queryWithoutIdentificationVariableShouldWork() { + + assertQuery("SELECT name, lastname from Person"); + assertQuery("SELECT name, lastname from Person WHERE lastname = 'Doe' ORDER BY name, lastname"); + assertQuery("SELECT name, lastname from Person JOIN department"); + } + } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java index 8f93859699..520039d70b 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java @@ -37,6 +37,7 @@ * {@link JpaQueryEnhancer.EqlQueryParser}. * * @author Greg Turnquist + * @author Mark Paluch */ class EqlQueryTransformerTests { @@ -82,13 +83,11 @@ void nullFirstLastSorting() { assertThat(createQueryFor(original, Sort.unsorted())).isEqualTo(original); - assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsLast()))) - .startsWith(original) - .endsWithIgnoringCase("e.lastName DESC NULLS LAST"); + assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsLast()))).startsWith(original) + .endsWithIgnoringCase("e.lastName DESC NULLS LAST"); - assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsFirst()))) - .startsWith(original) - .endsWithIgnoringCase("e.lastName DESC NULLS FIRST"); + assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsFirst()))).startsWith(original) + .endsWithIgnoringCase("e.lastName DESC NULLS FIRST"); } @Test @@ -104,6 +103,32 @@ void applyCountToSimpleQuery() { assertThat(results).isEqualTo("SELECT count(e) FROM Employee e where e.name = :name"); } + @Test // GH-3902 + void applyCountToFromQuery() { + + // given + var original = "FROM Employee e where e.name = :name"; + + // when + var results = createCountQueryFor(original); + + // then + assertThat(results).isEqualTo("select count(e) FROM Employee e where e.name = :name"); + } + + @Test // GH-3902 + void applyCountToFromQueryWithoutIdentificationVariable() { + + // given + var original = "FROM Employee where name = :name"; + + // when + var results = createCountQueryFor(original); + + // then + assertThat(results).isEqualTo("select count(__) FROM Employee AS __ where name = :name"); + } + @Test void applyCountToMoreComplexQuery() { @@ -117,6 +142,12 @@ void applyCountToMoreComplexQuery() { assertThat(results).isEqualTo("SELECT count(e) FROM Employee e where e.name = :name"); } + @Test // GH-3902 + void usesPrimaryAliasOfMultiselectForCountQuery() { + assertCountQuery("SELECT e.foo, e.bar FROM Employee e where e.name = :name ORDER BY e.modified_date", + "SELECT count(e) FROM Employee e where e.name = :name"); + } + @Test void applyCountToAlreadySortedQuery() { @@ -143,8 +174,14 @@ void multipleAliasesShouldBeGathered() { assertThat(results).isEqualTo("select e from Employee e join e.manager m"); } - @Test + @Test // GH-3902 void createsCountQueryCorrectly() { + + assertCountQuery("SELECT id FROM Person", "SELECT count(id) FROM Person"); + assertCountQuery("SELECT p.id FROM Person p", "SELECT count(p) FROM Person p"); + assertCountQuery("SELECT id FROM Person p", "SELECT count(p) FROM Person p"); + assertCountQuery("SELECT id, name FROM Person", "SELECT count(id) FROM Person"); + assertCountQuery("SELECT id, name FROM Person p", "SELECT count(p) FROM Person p"); assertCountQuery(QUERY, COUNT_QUERY); } @@ -183,6 +220,14 @@ void createsCountQueryForQueriesWithSubSelects() { "select count(u) from User u left outer join u.roles r where r in (select r from Role r)"); } + @Test // GH-3902 + void createsCountQueryForQueriesWithoutVariableWithSubSelectsSelectQuery() { + + assertCountQuery( + "select name, (select foo from bar b) from User left outer join u.roles r where r in (select r from Role r)", + "select count(name) from User left outer join u.roles r where r in (select r from Role r)"); + } + @Test void createsCountQueryForAliasesCorrectly() { assertCountQuery("select u from User as u", "select count(u) from User as u"); @@ -193,7 +238,7 @@ void allowsShortJpaSyntax() { assertCountQuery(SIMPLE_QUERY, COUNT_QUERY); } - @Test // GH-2260 + @Test // GH-2260, GH-3902 void detectsAliasCorrectly() { assertThat(alias(QUERY)).isEqualTo("u"); @@ -210,6 +255,11 @@ void detectsAliasCorrectly() { 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"); + assertThat(alias("select u, (select u2 from User u2) from User u")).isEqualTo("u"); + assertThat(alias("select firstname from User where not exists (select u2 from User u2)")).isNull(); + assertThat(alias("select firstname from User UNION select lastname from User b")).isNull(); + assertThat(alias("select firstname from User UNION select lastname from User UNION select lastname from User b")) + .isNull(); } @Test // GH-2557 @@ -226,12 +276,12 @@ where exists (select u2 """).rewrite(new DefaultQueryRewriteInformation(sort, ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())))) .isEqualToIgnoringWhitespace(""" - select u - from user u - where exists (select u2 - from user u2 - ) - order by u.age desc"""); + select u + from user u + where exists (select u2 + from user u2 + ) + order by u.age desc"""); } @Test // GH-2563 @@ -643,20 +693,6 @@ void countProjectionDistinctQueryIncludesNewLineAfterEntityAndBeforeWhere() { "SELECT count(DISTINCT entity1) FROM Entity1 entity1 LEFT JOIN entity1.entity2 entity2 ON entity1.key = entity2.key where entity1.id = 1799"); } - @Test // GH-3269 - void createsCountQueryUsingAliasCorrectly() { - - 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 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"); - 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"); - } - @Test // GH-2393 void createCountQueryStartsWithWhitespace() { @@ -698,6 +734,36 @@ void countQueryUsesCorrectVariable() { .isEqualTo("SELECT count(us) FROM users_statuses us WHERE (user_created_at BETWEEN :fromDate AND :toDate)"); } + @Test // GH-3269 + void createsCountQueryUsingAliasCorrectly() { + + 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 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"); + 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"); + } + + @Test // GH-3902 + void createsCountQueryWithoutAlias() { + + assertCountQuery( + "SELECT this.quantity FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'", + "SELECT count(this.quantity) FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'"); + } + + @Test // GH-3902 + void createsCountQueryFromMultiselectWithoutAlias() { + + assertCountQuery( + "SELECT this.quantity, that.quantity FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'", + "SELECT count(this.quantity) FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'"); + } + @Test // GH-2496, GH-2522, GH-2537, GH-2045 void orderByShouldWorkWithSubSelectStatements() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java index 040c632dfb..0ed7f1ed75 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java @@ -2621,7 +2621,36 @@ void joinTwoFunctions() { from some_function(:date, :integerValue) d inner join some_function_single_param(:date) k on (d.idFunction = k.idFunctionSP) """); + } + + @Test // GH-3902 + void queryWithoutSelectShouldWork() { + + assertQuery("from Person p"); + assertQuery("from Person p WHERE p.name = 'John' ORDER BY p.name"); + } + + @Test // GH-3902 + void queryWithoutSelectAndIdentificationVariableShouldWork() { + + assertQuery("from Person"); + assertQuery("from Person WHERE name = 'John' ORDER BY name"); + assertQuery("from Person JOIN department WHERE name = 'John' ORDER BY name"); + assertQuery( + "from Person JOIN (select phone.number as n, phone.person as pp from Phone phone) WHERE name = 'John' ORDER BY name"); + assertQuery("from Person JOIN (select number, person from Phone) WHERE name = 'John' ORDER BY name"); + } + @Test // GH-3902 + void queryWithoutIdentificationVariableShouldWork() { + + assertQuery("SELECT name, lastname from Person"); + assertQuery("SELECT name, lastname from Person WHERE lastname = 'Doe' ORDER BY name, lastname"); + assertQuery("SELECT name, lastname from Person JOIN department"); + assertQuery( + "SELECT name, lastname from Person JOIN (select phone.number as n, phone.person as pp from Phone phone) WHERE name = 'John' ORDER BY name"); + assertQuery( + "SELECT name, lastname from Person JOIN (select number, person from Phone) WHERE name = 'John' ORDER BY name"); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java index 260a788d64..d1c5adfa48 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java @@ -109,6 +109,19 @@ void applyCountToSimpleQuery() { assertThat(results).isEqualTo("select count(e) FROM Employee e where e.name = :name"); } + @Test // GH-3902 + void applyCountToFromQueryWithoutIdentificationVariable() { + + // given + var original = "FROM Employee where name = :name"; + + // when + var results = createCountQueryFor(original); + + // then + assertThat(results).isEqualTo("select count(__) FROM Employee AS __ where name = :name"); + } + @Test // GH-3536 void shouldCreateCountQueryForDistinctCount() { @@ -139,6 +152,12 @@ void applyCountToMoreComplexQuery() { assertThat(results).isEqualTo("SELECT count(e) FROM Employee e where e.name = :name"); } + @Test // GH-3902 + void usesAsteriskAliasOfMultiselectForCountQuery() { + assertCountQuery("SELECT e.foo, e.bar FROM Employee e where e.name = :name ORDER BY e.modified_date", + "SELECT count(e) FROM Employee e where e.name = :name"); + } + @Test void applyCountToAlreadySortedQuery() { @@ -186,9 +205,9 @@ void multipleAliasesShouldBeGathered() { @Test void createsCountQueryCorrectly() { - assertCountQuery("SELECT id FROM Person", "SELECT count(id) FROM Person"); + assertCountQuery("SELECT id FROM Person", "SELECT count(*) FROM Person"); assertCountQuery("SELECT p.id FROM Person p", "SELECT count(p) FROM Person p"); - assertCountQuery("SELECT id FROM Person p", "SELECT count(id) FROM Person p"); + assertCountQuery("SELECT id FROM Person p", "SELECT count(p) FROM Person p"); assertCountQuery("SELECT id, name FROM Person", "SELECT count(*) FROM Person"); assertCountQuery("SELECT id, name FROM Person p", "SELECT count(p) FROM Person p"); assertCountQuery(QUERY, COUNT_QUERY); @@ -232,7 +251,15 @@ void createsCountQueryForQueriesWithSubSelectsSelectQuery() { "select count(u) from User u left outer join u.roles r where r in (select r from Role r)"); } - @Test + @Test // GH-3902 + void createsCountQueryForQueriesWithoutVariableWithSubSelectsSelectQuery() { + + assertCountQuery( + "select u, (select foo from bar b) from User left outer join u.roles r where r in (select r from Role r)", + "select count(*) from User left outer join u.roles r where r in (select r from Role r)"); + } + + @Test // GH-3902 void createsCountQueryForQueriesWithSubSelects() { assertCountQuery("from User u left outer join u.roles r where r in (select r from Role r) select u ", @@ -249,7 +276,7 @@ void allowsShortJpaSyntax() { assertCountQuery(SIMPLE_QUERY, COUNT_QUERY); } - @Test // GH-2260 + @Test // GH-2260, GH-3902 void detectsAliasCorrectly() { assertThat(alias(QUERY)).isEqualTo("u"); @@ -269,6 +296,11 @@ void detectsAliasCorrectly() { assertThat(alias( "SELECT e FROM DbEvent e WHERE TREAT(modifiedFrom AS date) IS NULL OR e.modificationDate >= :modifiedFrom")) .isEqualTo("e"); + assertThat(alias("select u, (select u2 from User u2) from User u")).isEqualTo("u"); + assertThat(alias("select firstname from User JOIN (select u2 from User u2) u2")).isNull(); + assertThat(alias("select firstname from User UNION select lastname from User b")).isNull(); + assertThat(alias("select firstname from User UNION select lastname from User UNION select lastname from User b")) + .isNull(); } @Test // GH-2557 @@ -285,12 +317,12 @@ where exists (select u2 """).rewrite(new DefaultQueryRewriteInformation(sort, ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())))) .isEqualToIgnoringWhitespace(""" - select u - from user u - where exists (select u2 - from user u2 - ) - order by u.age desc"""); + select u + from user u + where exists (select u2 + from user u2 + ) + order by u.age desc"""); } @Test // GH-2563 @@ -834,6 +866,38 @@ void countQueryUsesCorrectVariable() { .isEqualTo("SELECT count(us) FROM users_statuses us WHERE (user_created_at BETWEEN :fromDate AND :toDate)"); } + @Test // GH-3269, GH-3689 + void createsCountQueryUsingAliasCorrectly() { + + assertCountQuery("select distinct 1 as x from Employee", "select count(distinct 1) from Employee"); + assertCountQuery("SELECT DISTINCT abc AS x FROM T", "SELECT count(DISTINCT abc) FROM T"); + assertCountQuery("select distinct a as x, b as y from Employee", "select count(distinct a, b) from Employee"); + assertCountQuery("select distinct sum(amount) as x from Employee GROUP BY n", + "select count(distinct sum(amount)) from Employee 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 GROUP BY n"); + assertCountQuery("select distinct a, count(b) as c from Employee GROUP BY n", + "select count(distinct a, count(b)) from Employee GROUP BY n"); + assertCountQuery("select distinct substring(e.firstname, 1, position('a' in e.lastname)) as x from from Employee", + "select count(distinct substring(e.firstname, 1, position('a' in e.lastname))) from from Employee"); + } + + @Test // GH-3902 + void createsCountQueryWithoutAlias() { + + assertCountQuery( + "SELECT this.quantity FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'", + "SELECT count(*) FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'"); + } + + @Test // GH-3902 + void createsCountQueryFromMultiselectWithoutAlias() { + + assertCountQuery( + "SELECT this.quantity, that.quantity FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'", + "SELECT count(*) FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'"); + } + @Test // GH-2496, GH-2522, GH-2537, GH-2045 void orderByShouldWorkWithSubSelectStatements() { @@ -1118,22 +1182,6 @@ void aliasesShouldNotOverlapWithSortProperties() { "SELECT t3 FROM Test3 t3 JOIN t3.test2 x WHERE x.id = :test2Id order by t3.testDuplicateColumnName desc"); } - @Test // GH-3269, GH-3689 - void createsCountQueryUsingAliasCorrectly() { - - assertCountQuery("select distinct 1 as x from Employee", "select count(distinct 1) from Employee"); - assertCountQuery("SELECT DISTINCT abc AS x FROM T", "SELECT count(DISTINCT abc) FROM T"); - assertCountQuery("select distinct a as x, b as y from Employee", "select count(distinct a, b) from Employee"); - assertCountQuery("select distinct sum(amount) as x from Employee GROUP BY n", - "select count(distinct sum(amount)) from Employee 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 GROUP BY n"); - assertCountQuery("select distinct a, count(b) as c from Employee GROUP BY n", - "select count(distinct a, count(b)) from Employee GROUP BY n"); - assertCountQuery("select distinct substring(e.firstname, 1, position('a' in e.lastname)) as x from from Employee", - "select count(distinct substring(e.firstname, 1, position('a' in e.lastname))) from from Employee"); - } - @Test // GH-3864 void testCountFromFunctionWithAlias() { @@ -1148,7 +1196,7 @@ void testCountFromFunctionWithAlias() { } @Test // GH-3864 - void testCountFromFunctionNoAlias() { + void testCountFromMultiselectFunctionNoAlias() { // given var original = "select id, value from some_function(:date, :integerValue)"; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java index 3d9b1bf1b2..8fe266711e 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java @@ -44,7 +44,7 @@ class JpqlQueryRendererTests { 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}. + * Parse the query using {@link JpqlParser} then run it through the query-preserving {@link JpqlQueryRenderer}. */ private static String parseWithoutChanges(String query) { @@ -1279,23 +1279,31 @@ void typeShouldBeAValidParameter() { assertQuery("select te from TestEntity te where te.type = :type"); } - @Test // GH-3496 - void lateralShouldBeAValidParameter() { - - assertQuery("select e from Employee e where e.lateral = :_lateral"); - assertQuery("select te from TestEntity te where te.lateral = :lateral"); - } - @Test // GH-3061 void alternateNotEqualsOperatorShouldWork() { 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(""" @@ -1410,6 +1418,13 @@ void entityNameWithPackageContainingReservedWord(String reservedWord) { assertQuery(source); } + @Test // GH-3496 + void lateralShouldBeAValidParameter() { + + assertQuery("select e from Employee e where e.lateral = :_lateral"); + assertQuery("select te from TestEntity te where te.lateral = :lateral"); + } + @Test // GH-3834 void reservedWordsShouldWork() { @@ -1417,6 +1432,32 @@ void reservedWordsShouldWork() { assertQuery("select ie.object from ItemExample ie left join ie.object io where io.externalId = :externalId"); assertQuery("select ie from ItemExample ie left join ie.object io where io.object = :externalId"); assertQuery("select ie from ItemExample ie where ie.status = com.app.domain.object.Status.UP"); + assertQuery("select f from FooEntity f where upper(f.name) IN :names"); + assertQuery("select f from FooEntity f where f.size IN :sizes"); + } + + @Test // GH-3902 + void queryWithoutSelectShouldWork() { + + assertQuery("from Person p"); + assertQuery("from Person p WHERE p.name = 'John' ORDER BY p.name"); + } + + @Test // GH-3902 + void queryWithoutSelectAndIdentificationVariableShouldWork() { + + assertQuery("from Person"); + assertQuery("from Person WHERE name = 'John' ORDER BY name"); + assertQuery("from Person JOIN department WHERE name = 'John' ORDER BY name"); + } + + @Test // GH-3902 + void queryWithoutIdentificationVariableShouldWork() { + + assertQuery("SELECT name, lastname from Person"); + assertQuery("SELECT name, lastname from Person WHERE lastname = 'Doe' ORDER BY name, lastname"); + assertQuery("SELECT name, lastname from Person WHERE lastname = 'Doe' ORDER BY name, lastname"); + assertQuery("SELECT name, lastname from Person JOIN department"); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java index 39ed9b6d9d..69b8514ed3 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java @@ -83,13 +83,11 @@ void nullFirstLastSorting() { assertThat(createQueryFor(original, Sort.unsorted())).isEqualTo(original); - assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsLast()))) - .startsWith(original) - .endsWithIgnoringCase("e.lastName DESC NULLS LAST"); + assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsLast()))).startsWith(original) + .endsWithIgnoringCase("e.lastName DESC NULLS LAST"); - assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsFirst()))) - .startsWith(original) - .endsWithIgnoringCase("e.lastName DESC NULLS FIRST"); + assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsFirst()))).startsWith(original) + .endsWithIgnoringCase("e.lastName DESC NULLS FIRST"); } @Test @@ -105,6 +103,32 @@ void applyCountToSimpleQuery() { assertThat(results).isEqualTo("SELECT count(e) FROM Employee e where e.name = :name"); } + @Test // GH-3902 + void applyCountToFromQuery() { + + // given + var original = "FROM Employee e where e.name = :name"; + + // when + var results = createCountQueryFor(original); + + // then + assertThat(results).isEqualTo("select count(e) FROM Employee e where e.name = :name"); + } + + @Test // GH-3902 + void applyCountToFromQueryWithoutIdentificationVariable() { + + // given + var original = "FROM Employee where name = :name"; + + // when + var results = createCountQueryFor(original); + + // then + assertThat(results).isEqualTo("select count(__) FROM Employee AS __ where name = :name"); + } + @Test void applyCountToMoreComplexQuery() { @@ -118,6 +142,12 @@ void applyCountToMoreComplexQuery() { assertThat(results).isEqualTo("SELECT count(e) FROM Employee e where e.name = :name"); } + @Test // GH-3902 + void usesPrimaryAliasOfMultiselectForCountQuery() { + assertCountQuery("SELECT e.foo, e.bar FROM Employee e where e.name = :name ORDER BY e.modified_date", + "SELECT count(e) FROM Employee e where e.name = :name"); + } + @Test void applyCountToAlreadySortedQuery() { @@ -144,8 +174,14 @@ void multipleAliasesShouldBeGathered() { assertThat(results).isEqualTo("select e from Employee e join e.manager m"); } - @Test + @Test // GH-3902 void createsCountQueryCorrectly() { + + assertCountQuery("SELECT id FROM Person", "SELECT count(id) FROM Person"); + assertCountQuery("SELECT p.id FROM Person p", "SELECT count(p) FROM Person p"); + assertCountQuery("SELECT id FROM Person p", "SELECT count(p) FROM Person p"); + assertCountQuery("SELECT id, name FROM Person", "SELECT count(id) FROM Person"); + assertCountQuery("SELECT id, name FROM Person p", "SELECT count(p) FROM Person p"); assertCountQuery(QUERY, COUNT_QUERY); } @@ -184,6 +220,14 @@ void createsCountQueryForQueriesWithSubSelects() { "select count(u) from User u left outer join u.roles r where r in (select r from Role r)"); } + @Test // GH-3902 + void createsCountQueryForQueriesWithoutVariableWithSubSelectsSelectQuery() { + + assertCountQuery( + "select name, (select foo from bar b) from User left outer join u.roles r where r in (select r from Role r)", + "select count(name) from User left outer join u.roles r where r in (select r from Role r)"); + } + @Test void createsCountQueryForAliasesCorrectly() { assertCountQuery("select u from User as u", "select count(u) from User as u"); @@ -194,7 +238,7 @@ void allowsShortJpaSyntax() { assertCountQuery(SIMPLE_QUERY, COUNT_QUERY); } - @Test // GH-2260 + @Test // GH-2260, GH-3902 void detectsAliasCorrectly() { assertThat(alias(QUERY)).isEqualTo("u"); @@ -211,6 +255,11 @@ void detectsAliasCorrectly() { 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"); + assertThat(alias("select u, (select u2 from User u2) from User u")).isEqualTo("u"); + assertThat(alias("select firstname from User where not exists (select u2 from User u2)")).isNull(); + assertThat(alias("select firstname from User UNION select lastname from User b")).isNull(); + assertThat(alias("select firstname from User UNION select lastname from User UNION select lastname from User b")) + .isNull(); } @Test // GH-2557 @@ -218,7 +267,6 @@ void applySortingAccountsForNewlinesInSubselect() { Sort sort = Sort.by(Sort.Order.desc("age")); - assertThat(newParser(""" select u from user u @@ -228,12 +276,12 @@ where exists (select u2 """).rewrite(new DefaultQueryRewriteInformation(sort, ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())))) .isEqualToIgnoringWhitespace(""" - select u - from user u - where exists (select u2 - from user u2 - ) - order by u.age desc"""); + select u + from user u + where exists (select u2 + from user u2 + ) + order by u.age desc"""); } @Test // GH-2563 @@ -490,9 +538,6 @@ void detectsAliasWithGroupAndOrderBy() { @Test // DATAJPA-1500 void createCountQuerySupportsWhitespaceCharacters() { - // - // - // assertThat(createCountQueryFor(""" select user from User user where user.age = 18 @@ -568,10 +613,6 @@ void appliesSortCorrectlyForSimpleField() { @Test void createCountQuerySupportsLineBreakRightAfterDistinct() { - // - // - // - // assertThat(createCountQueryFor(""" select distinct @@ -595,7 +636,6 @@ void detectsAliasWithGroupAndOrderByWithLineBreaks() { .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"); @@ -706,6 +746,22 @@ void createsCountQueryUsingAliasCorrectly() { "select count(distinct a, count(b)) from Employee e GROUP BY n"); } + @Test // GH-3902 + void createsCountQueryWithoutAlias() { + + assertCountQuery( + "SELECT this.quantity FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'", + "SELECT count(this.quantity) FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'"); + } + + @Test // GH-3902 + void createsCountQueryFromMultiselectWithoutAlias() { + + assertCountQuery( + "SELECT this.quantity, that.quantity FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'", + "SELECT count(this.quantity) FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'"); + } + @Test // GH-2496, GH-2522, GH-2537, GH-2045 void orderByShouldWorkWithSubSelectStatements() { @@ -795,7 +851,8 @@ void sortShouldBeAppendedToFullSelectOnlyInCaseOfSetOperator() { String source = "SELECT tb FROM Test tb WHERE (tb.type='A') UNION SELECT tb FROM Test tb WHERE (tb.type='B')"; String target = createQueryFor(source, Sort.by("Type").ascending()); - assertThat(target).isEqualTo("SELECT tb FROM Test tb WHERE (tb.type = 'A') UNION SELECT tb FROM Test tb WHERE (tb.type = 'B') order by tb.Type asc"); + assertThat(target).isEqualTo( + "SELECT tb FROM Test tb WHERE (tb.type = 'A') UNION SELECT tb FROM Test tb WHERE (tb.type = 'B') order by tb.Type asc"); } static Stream queriesWithReservedWordsAsIdentifiers() {