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 e87d523b12..b91748b91d 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,13 @@ ql_statement ; select_statement - : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? + : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (setOperator_with_select_statement)* + ; + +setOperator_with_select_statement + : INTERSECT select_statement + | UNION select_statement + | EXCEPT select_statement ; update_statement @@ -434,6 +440,7 @@ string_expression | aggregate_expression | case_expression | function_invocation + | string_expression op='||' string_expression | '(' subquery ')' ; @@ -878,6 +885,7 @@ ELSE : E L S E; EMPTY : E M P T Y; ENTRY : E N T R Y; ESCAPE : E S C A P E; +EXCEPT : E X C E P T; EXISTS : E X I S T S; EXP : E X P; EXTRACT : E X T R A C T; @@ -892,6 +900,7 @@ HAVING : H A V I N G; IN : I N; INDEX : I N D E X; INNER : I N N E R; +INTERSECT : I N T E R S E C T; IS : I S; JOIN : J O I N; KEY : K E Y; @@ -936,6 +945,7 @@ TREAT : T R E A T; TRIM : T R I M; TRUE : T R U E; TYPE : T Y P E; +UNION : U N I O N; UPDATE : U P D A T E; UPPER : U P P E R; VALUE : V A L U E; 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 f743d50669..5fb061f476 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 @@ -79,6 +79,29 @@ public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext builder.appendExpression(visit(ctx.orderby_clause())); } + ctx.setOperator_with_select_statement().forEach(setOperatorWithSelectStatementContext -> { + tokens.addAll(visit(setOperatorWithSelectStatementContext)); + }); + + return tokens; + } + + @Override + public List visitSetOperator_with_select_statement( + JpqlParser.SetOperator_with_select_statementContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.INTERSECT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.INTERSECT())); + } else if (ctx.UNION() != null) { + tokens.add(new JpaQueryParsingToken(ctx.UNION())); + } else if (ctx.EXCEPT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.EXCEPT())); + } + + tokens.addAll(visit(ctx.select_statement())); + return builder; } @@ -799,6 +822,25 @@ public QueryTokenStream visitOrderby_item(JpqlParser.Orderby_itemContext ctx) { if (ctx.nullsPrecedence() != null) { builder.append(visit(ctx.nullsPrecedence())); } + if (ctx.nullsPrecedence() != null) { + tokens.addAll(visit(ctx.nullsPrecedence())); + } + + return tokens; + } + + @Override + public List visitNullsPrecedence(JpqlParser.NullsPrecedenceContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.NULLS())); + + if (ctx.FIRST() != null) { + tokens.add(new JpaQueryParsingToken(ctx.FIRST())); + } else if (ctx.LAST() != null) { + tokens.add(new JpaQueryParsingToken(ctx.LAST())); + } return builder; } @@ -1441,6 +1483,11 @@ public QueryTokenStream visitString_expression(JpqlParser.String_expressionConte builder.append(visit(ctx.case_expression())); } else if (ctx.function_invocation() != null) { builder.append(visit(ctx.function_invocation())); + } else if (ctx.op != null) { + + tokens.addAll(visit(ctx.string_expression(0))); + tokens.add(new JpaQueryParsingToken(ctx.op)); + tokens.addAll(visit(ctx.string_expression(1))); } else if (ctx.subquery() != null) { builder.append(TOKEN_OPEN_PAREN); 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 067a3adbe4..57c0b43b67 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 @@ -1646,42 +1646,36 @@ void hqlQueries() { @Test // GH-2962 void orderByWithNullsFirstOrLastShouldWork() { - assertThatNoException().isThrownBy(() -> { - parseWithoutChanges(""" - select a, - case - when a.geaendertAm is null then a.erstelltAm - else a.geaendertAm end as mutationAm - from Element a - where a.erstelltDurch = :variable - order by mutationAm desc nulls first - """); - }); - - assertThatNoException().isThrownBy(() -> { - parseWithoutChanges(""" - select a, - case - when a.geaendertAm is null then a.erstelltAm - else a.geaendertAm end as mutationAm - from Element a - where a.erstelltDurch = :variable - order by mutationAm desc nulls last + assertQuery(""" + select a, + case + when a.geaendertAm is null then a.erstelltAm + else a.geaendertAm end as mutationAm + from Element a + where a.erstelltDurch = :variable + order by mutationAm desc nulls first + """); + + assertQuery(""" + select a, + case + when a.geaendertAm is null then a.erstelltAm + else a.geaendertAm end as mutationAm + from Element a + where a.erstelltDurch = :variable + order by mutationAm desc nulls last """); - }); } @Test // GH-2964 void roundFunctionShouldWorkLikeAnyOtherFunction() { - assertThatNoException().isThrownBy(() -> { - parseWithoutChanges(""" - select round(count(ri) * 100 / max(ri.receipt.positions), 0) as perc - from StockOrderItem oi - right join StockReceiptItem ri - on ri.article = oi.article - """); - }); + assertQuery(""" + select round(count(ri)*100/max(ri.receipt.positions), 0) as perc + from StockOrderItem oi + right join StockReceiptItem ri + on ri.article = oi.article + """); } @Test // GH-3711 @@ -1831,6 +1825,42 @@ void powerShouldBeLegalInAQuery() { assertQuery("select e.power.id from MyEntity e"); } + @Test // GH-3136 + void doublePipeShouldBeValidAsAStringConcatOperator() { + + assertQuery(""" + select e.name || ' ' || e.title + from Employee e + """); + } + + @Test // GH-3136 + void additionalStringOperationsShouldWork() { + + assertQuery(""" + select + replace(e.name, 'Baggins', 'Proudfeet'), + left(e.role, 4), + right(e.home, 5), + cast(e.distance_from_home, int) + from Employee e + """); + } + + @Test // GH-3136 + void combinedSelectStatementsShouldWork() { + + assertQuery(""" + select e from Employee e where e.last_name = 'Baggins' + intersect + select e from Employee e where e.first_name = 'Samwise' + union + select e from Employee e where e.home = 'The Shire' + except + select e from Employee e where e.home = 'Isengard' + """); + } + @Test // GH-3219 void extractFunctionShouldSupportAdditionalExtensions() { 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 c50f07c596..3ec9ab7c9b 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 @@ -1010,6 +1010,59 @@ void powerShouldBeLegalInAQuery() { assertQuery("select e.power.id from MyEntity e"); } + @Test // GH-3136 + void doublePipeShouldBeValidAsAStringConcatOperator() { + + assertQuery(""" + select e.name || ' ' || e.title + from Employee e + """); + } + + @Test // GH-3136 + void combinedSelectStatementsShouldWork() { + + assertQuery(""" + select e from Employee e where e.last_name = 'Baggins' + intersect + select e from Employee e where e.first_name = 'Samwise' + union + select e from Employee e where e.home = 'The Shire' + except + select e from Employee e where e.home = 'Isengard' + """); + } + + @Disabled + @Test // GH-3136 + void additionalStringOperationsShouldWork() { + + assertQuery(""" + select + replace(e.name, 'Baggins', 'Proudfeet'), + left(e.role, 4), + right(e.home, 5), + cast(e.distance_from_home, int) + from Employee e + """); + } + + @Test // GH-3136 + void orderByWithNullsFirstOrLastShouldWork() { + + assertQuery(""" + select a + from Element a + order by mutationAm desc nulls first + """); + + assertQuery(""" + select a + from Element a + order by mutationAm desc nulls last + """); + } + @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",