From 8ba9452ba15e7c7921d37ac13511fac81a7be103 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 21 Jun 2024 12:10:10 +0200 Subject: [PATCH 01/11] Prepare issue branch. --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 617cc011d3..8167941d6c 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.4.0-SNAPSHOT + 3.4.x-GH-3136-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 8b836ae2f3..a281bfe7df 100755 --- a/spring-data-envers/pom.xml +++ b/spring-data-envers/pom.xml @@ -5,12 +5,12 @@ org.springframework.data spring-data-envers - 3.4.0-SNAPSHOT + 3.4.x-GH-3136-SNAPSHOT org.springframework.data spring-data-jpa-parent - 3.4.0-SNAPSHOT + 3.4.x-GH-3136-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index a90c1f7282..06e8ff9406 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 - 3.4.0-SNAPSHOT + 3.4.x-GH-3136-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 3006702796..bf5f3a5842 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-jpa - 3.4.0-SNAPSHOT + 3.4.x-GH-3136-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-jpa-parent - 3.4.0-SNAPSHOT + 3.4.x-GH-3136-SNAPSHOT ../pom.xml From b92791e9e5f01e877c691211766d24c7cad6ab51 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 21 Jun 2024 18:14:07 +0200 Subject: [PATCH 02/11] Initial draft --- .../data/jpa/repository/query/Jpql.g4 | 31 ++++++++- .../query/JpqlCountQueryTransformer.java | 12 +++- .../repository/query/JpqlQueryRenderer.java | 68 ++++++++++++++++++- .../query/JpqlSortedQueryTransformer.java | 12 +++- .../repository/query/JpqlComplianceTests.java | 27 ++++++++ 5 files changed, 144 insertions(+), 6 deletions(-) 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 a7f319b793..90843264d8 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 @@ -42,8 +42,22 @@ ql_statement | delete_statement ; +select_query + : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (relation_fuctions)? + ; + select_statement - : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? + : select_query + ; + +relation_fuctions + : union_clause + | intersect_clause + | except_clause + ; + +relation_fuctions_select + : (ALL)? select_query ; update_statement @@ -234,6 +248,18 @@ orderby_clause : ORDER BY orderby_item (',' orderby_item)* ; +union_clause + : UNION relation_fuctions_select + ; + +intersect_clause + : INTERSECT relation_fuctions_select + ; + +except_clause + : EXCEPT relation_fuctions_select + ; + // TODO Error in spec BNF, correctly shown elsewhere in spec. orderby_item : (state_field_path_expression | general_identification_variable | result_variable ) (ASC | DESC)? @@ -874,6 +900,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; @@ -887,6 +914,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; @@ -929,6 +957,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/JpqlCountQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java index 6ceb6e171a..2c26df1357 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 @@ -42,7 +42,17 @@ class JpqlCountQueryTransformer extends JpqlQueryRenderer { } @Override - public QueryRenderer.QueryRendererBuilder visitSelect_statement(JpqlParser.Select_statementContext ctx) { + public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) { + + if(ctx.select_query() != null) { + return visitSelect_query(ctx.select_query()); + } + + return QueryTokenStream.empty(); + } + + @Override + public QueryTokenStream visitSelect_query(JpqlParser.Select_queryContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); 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 fe8fac1bfa..57cc97e402 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 @@ -15,14 +15,27 @@ */ package org.springframework.data.jpa.repository.query; -import static org.springframework.data.jpa.repository.query.QueryTokens.*; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_CLOSE_PAREN; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_COLON; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_COMMA; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_DOT; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_EQUALS; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_OPEN_PAREN; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_QUESTION_MARK; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_SPACE; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_CLOSE_PAREN; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_OPEN_PAREN; import java.util.ArrayList; import java.util.List; import org.antlr.v4.runtime.tree.ParseTree; +import org.springframework.data.jpa.repository.query.JpqlParser.Except_clauseContext; +import org.springframework.data.jpa.repository.query.JpqlParser.Intersect_clauseContext; +import org.springframework.data.jpa.repository.query.JpqlParser.Relation_fuctions_selectContext; import org.springframework.data.jpa.repository.query.JpqlParser.Reserved_wordContext; +import org.springframework.data.jpa.repository.query.JpqlParser.Union_clauseContext; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; /** @@ -54,8 +67,17 @@ public QueryTokenStream visitQl_statement(JpqlParser.Ql_statementContext ctx) { } } - @Override - public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) { + @Override + public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) { + + if(ctx.select_query() != null) { + return visitSelect_query(ctx.select_query()); + } + + return QueryTokenStream.empty(); + } + + public QueryTokenStream visitSelect_query(JpqlParser.Select_queryContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -78,6 +100,10 @@ public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext builder.appendExpression(visit(ctx.orderby_clause())); } + if(ctx.relation_fuctions() != null) { + builder.appendExpression(visit(ctx.relation_fuctions())); + } + return builder; } @@ -775,6 +801,42 @@ public QueryTokenStream visitOrderby_clause(JpqlParser.Orderby_clauseContext ctx return builder; } + @Override + public QueryTokenStream visitUnion_clause(Union_clauseContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + builder.append(QueryTokens.expression(ctx.UNION())); + builder.appendExpression(visit(ctx.relation_fuctions_select())); + return builder; + } + + @Override + public QueryTokenStream visitIntersect_clause(Intersect_clauseContext ctx) { + QueryRendererBuilder builder = QueryRenderer.builder(); + builder.append(QueryTokens.expression(ctx.INTERSECT())); + builder.appendExpression(visit(ctx.relation_fuctions_select())); + return builder; + } + + @Override + public QueryTokenStream visitExcept_clause(Except_clauseContext ctx) { + QueryRendererBuilder builder = QueryRenderer.builder(); + builder.append(QueryTokens.expression(ctx.EXCEPT())); + builder.appendExpression(visit(ctx.relation_fuctions_select())); + return builder; + } + + @Override + public QueryTokenStream visitRelation_fuctions_select(Relation_fuctions_selectContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + if(ctx.ALL() != null) { + builder.append(QueryTokens.expression(ctx.ALL())); + } + builder.appendExpression(visit(ctx.select_query())); + return builder; + } + @Override public QueryTokenStream visitOrderby_item(JpqlParser.Orderby_itemContext ctx) { 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 a545171bbf..9cd43176de 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 @@ -50,6 +50,16 @@ class JpqlSortedQueryTransformer extends JpqlQueryRenderer { @Override public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) { + if(ctx.select_query() != null) { + return visitSelect_query(ctx.select_query()); + } + + return QueryTokenStream.empty(); + } + + @Override + public QueryTokenStream visitSelect_query(JpqlParser.Select_queryContext ctx) { + QueryRendererBuilder builder = QueryRenderer.builder(); builder.appendExpression(visit(ctx.select_clause())); @@ -72,7 +82,7 @@ public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext return builder; } - private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Select_statementContext ctx) { + private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Select_queryContext ctx) { if (ctx.orderby_clause() != null) { QueryTokenStream existingOrder = visit(ctx.orderby_clause()); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java index aadf2c2589..2c371966fe 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java @@ -70,4 +70,31 @@ void newWithStrings() { assertQuery("select new com.example.demo.SampleObject(se.id, se.sampleValue, \"java\") from SampleEntity se"); } + @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 + """); + } + + @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 + """); + } + } From 557b8e8585cae6294975d4bed3cb9c39ee709a6e Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 24 Jun 2024 11:52:34 +0200 Subject: [PATCH 03/11] Simplify? --- .../data/jpa/repository/query/Jpql.g4 | 30 +++++-------- .../repository/query/JpqlQueryRenderer.java | 45 ++++--------------- 2 files changed, 20 insertions(+), 55 deletions(-) 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 90843264d8..571846c9f5 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,21 +43,25 @@ ql_statement ; select_query - : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (relation_fuctions)? + : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (set_fuction)? ; select_statement : select_query ; -relation_fuctions - : union_clause - | intersect_clause - | except_clause +setOperator + : UNION ALL? + | INTERSECT ALL? + | EXCEPT ALL? ; -relation_fuctions_select - : (ALL)? select_query +set_fuction + : setOperator set_function_select + ; + +set_function_select + : select_query ; update_statement @@ -248,18 +252,6 @@ orderby_clause : ORDER BY orderby_item (',' orderby_item)* ; -union_clause - : UNION relation_fuctions_select - ; - -intersect_clause - : INTERSECT relation_fuctions_select - ; - -except_clause - : EXCEPT relation_fuctions_select - ; - // TODO Error in spec BNF, correctly shown elsewhere in spec. orderby_item : (state_field_path_expression | general_identification_variable | result_variable ) (ASC | DESC)? 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 57cc97e402..63c70d41da 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 @@ -30,12 +30,8 @@ import java.util.List; import org.antlr.v4.runtime.tree.ParseTree; - -import org.springframework.data.jpa.repository.query.JpqlParser.Except_clauseContext; -import org.springframework.data.jpa.repository.query.JpqlParser.Intersect_clauseContext; -import org.springframework.data.jpa.repository.query.JpqlParser.Relation_fuctions_selectContext; import org.springframework.data.jpa.repository.query.JpqlParser.Reserved_wordContext; -import org.springframework.data.jpa.repository.query.JpqlParser.Union_clauseContext; +import org.springframework.data.jpa.repository.query.JpqlParser.Set_fuctionContext; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; /** @@ -100,8 +96,8 @@ public QueryTokenStream visitSelect_query(JpqlParser.Select_queryContext ctx) { builder.appendExpression(visit(ctx.orderby_clause())); } - if(ctx.relation_fuctions() != null) { - builder.appendExpression(visit(ctx.relation_fuctions())); + if(ctx.set_fuction() != null) { + builder.appendExpression(visit(ctx.set_fuction())); } return builder; @@ -802,38 +798,15 @@ public QueryTokenStream visitOrderby_clause(JpqlParser.Orderby_clauseContext ctx } @Override - public QueryTokenStream visitUnion_clause(Union_clauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.UNION())); - builder.appendExpression(visit(ctx.relation_fuctions_select())); - return builder; - } - - @Override - public QueryTokenStream visitIntersect_clause(Intersect_clauseContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.INTERSECT())); - builder.appendExpression(visit(ctx.relation_fuctions_select())); - return builder; - } - - @Override - public QueryTokenStream visitExcept_clause(Except_clauseContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.EXCEPT())); - builder.appendExpression(visit(ctx.relation_fuctions_select())); - return builder; - } + public QueryTokenStream visitSet_fuction(Set_fuctionContext ctx) { - @Override - public QueryTokenStream visitRelation_fuctions_select(Relation_fuctions_selectContext ctx) { + QueryRendererBuilder builder = QueryRenderer.builder(); - QueryRendererBuilder builder = QueryRenderer.builder(); - if(ctx.ALL() != null) { - builder.append(QueryTokens.expression(ctx.ALL())); + builder.append(QueryTokens.expression(ctx.setOperator().getStart())); + if(ctx.setOperator().ALL() != null) { + builder.append(QueryTokens.expression(ctx.setOperator().ALL())); } - builder.appendExpression(visit(ctx.select_query())); + builder.appendExpression(visit(ctx.set_function_select().select_query())); return builder; } From 9bd22fdfed5aec5e933a74008844cf44ea6413e9 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 24 Jun 2024 16:33:25 +0200 Subject: [PATCH 04/11] Allow parsing CAST in JPQL queries. --- .../data/jpa/repository/query/Jpql.g4 | 19 +++++++++++++++ .../repository/query/JpqlQueryRenderer.java | 24 +++++++++++++++++++ .../repository/query/JpqlComplianceTests.java | 8 +++++++ 3 files changed, 51 insertions(+) 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 571846c9f5..f4849cab35 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 @@ -311,6 +311,7 @@ scalar_expression | datetime_expression | boolean_expression | case_expression + | cast_expression | entity_type_expression ; @@ -605,6 +606,10 @@ nullif_expression : NULLIF '(' scalar_expression ',' scalar_expression ')' ; +cast_expression + : CAST '(' string_expression AS type_literal ')' + ; + /******************* Gaps in the spec. *******************/ @@ -675,6 +680,14 @@ numeric_literal | LONGLITERAL ; +type_literal + : STRING + | INTEGER + | LONG + | FLOAT + | DOUBLE + ; + boolean_literal : TRUE | FALSE @@ -875,6 +888,7 @@ BETWEEN : B E T W E E N; BOTH : B O T H; BY : B Y; CASE : C A S E; +CAST : C A S T; CEILING : C E I L I N G; COALESCE : C O A L E S C E; CONCAT : C O N C A T; @@ -887,6 +901,7 @@ DATETIME : D A T E T I M E ; DELETE : D E L E T E; DESC : D E S C; DISTINCT : D I S T I N C T; +DOUBLE : D O U B L E; END : E N D; ELSE : E L S E; EMPTY : E M P T Y; @@ -898,6 +913,7 @@ EXP : E X P; EXTRACT : E X T R A C T; FALSE : F A L S E; FETCH : F E T C H; +FLOAT : F L O A T; FLOOR : F L O O R; FROM : F R O M; FUNCTION : F U N C T I O N; @@ -906,6 +922,7 @@ HAVING : H A V I N G; IN : I N; INDEX : I N D E X; INNER : I N N E R; +INTEGER : I N T E G E R; INTERSECT : I N T E R S E C T; IS : I S; JOIN : J O I N; @@ -917,6 +934,7 @@ LIKE : L I K E; LN : L N; LOCAL : L O C A L; LOCATE : L O C A T E; +LONG : L O N G; LOWER : L O W E R; MAX : M A X; MEMBER : M E M B E R; @@ -940,6 +958,7 @@ SIGN : S I G N; SIZE : S I Z E; SOME : S O M E; SQRT : S Q R T; +STRING : S T R I N G; SUBSTRING : S U B S T R I N G; SUM : S U M; THEN : T H E N; 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 63c70d41da..7d27988776 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 @@ -30,8 +30,10 @@ import java.util.List; import org.antlr.v4.runtime.tree.ParseTree; +import org.springframework.data.jpa.repository.query.JpqlParser.Cast_expressionContext; import org.springframework.data.jpa.repository.query.JpqlParser.Reserved_wordContext; import org.springframework.data.jpa.repository.query.JpqlParser.Set_fuctionContext; +import org.springframework.data.jpa.repository.query.JpqlParser.Type_literalContext; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; /** @@ -945,6 +947,8 @@ public QueryTokenStream visitScalar_expression(JpqlParser.Scalar_expressionConte return visit(ctx.case_expression()); } else if (ctx.entity_type_expression() != null) { return visit(ctx.entity_type_expression()); + } else if (ctx.cast_expression() != null) { + return (visit(ctx.cast_expression())); } return QueryTokenStream.empty(); @@ -1882,6 +1886,26 @@ public QueryTokenStream visitCase_expression(JpqlParser.Case_expressionContext c } } + @Override + public QueryRendererBuilder visitCast_expression(Cast_expressionContext ctx) { + QueryRendererBuilder builder = QueryRenderer.builder(); + builder.append(QueryTokens.token(ctx.CAST())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.string_expression())); + builder.append(QueryTokens.expression(ctx.AS())); + builder.appendInline(visit(ctx.type_literal())); + builder.append(TOKEN_CLOSE_PAREN); + return builder; + } + + @Override + public QueryRendererBuilder visitType_literal(Type_literalContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + ctx.children.forEach(it -> builder.append(QueryTokens.expression(it.getText()))); + return builder; + } + @Override public QueryTokenStream visitGeneral_case_expression(JpqlParser.General_case_expressionContext ctx) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java index 2c371966fe..9ca9147b6e 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java @@ -20,6 +20,8 @@ import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; /** * Test to verify compliance of {@link JpqlParser} with standard SQL. Other than {@link JpqlSpecificationTests} tests in @@ -97,4 +99,10 @@ void except() { """); } + @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)); + } + } From 2b886b1cd2866571dc8107f78b238057b58f7e10 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 25 Jun 2024 11:34:00 +0200 Subject: [PATCH 05/11] Support LEFT & RIGHT keywords in JPQL query parsing. --- .../data/jpa/repository/query/Jpql.g4 | 5 +++++ .../jpa/repository/query/JpqlQueryRenderer.java | 14 ++++++++++++++ .../jpa/repository/query/JpqlComplianceTests.java | 6 ++++++ 3 files changed, 25 insertions(+) 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 f4849cab35..19d5a00283 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 @@ -534,6 +534,8 @@ functions_returning_strings | TRIM '(' ((trim_specification)? (trim_character)? FROM)? string_expression ')' | LOWER '(' string_expression ')' | UPPER '(' string_expression ')' + | LEFT '(' string_expression ',' arithmetic_expression ')' + | RIGHT '(' string_expression ',' arithmetic_expression ')' ; trim_specification @@ -631,6 +633,7 @@ identification_variable | ORDER | OUTER | POWER + | RIGHT | FLOOR | SIGN | TIME @@ -819,6 +822,7 @@ reserved_word |ORDER |OUTER |POWER + |RIGHT |ROUND |SELECT |SET @@ -950,6 +954,7 @@ ON : O N; OR : O R; ORDER : O R D E R; OUTER : O U T E R; +RIGHT : R I G H T; POWER : P O W E R; ROUND : R O U N D; SELECT : S E L E C T; 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 7d27988776..175f7e6095 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 @@ -1784,6 +1784,20 @@ public QueryTokenStream visitFunctions_returning_strings(JpqlParser.Functions_re builder.append(TOKEN_OPEN_PAREN); builder.append(visit(ctx.string_expression(0))); builder.append(TOKEN_CLOSE_PAREN); + } else if (ctx.LEFT() != null) { + builder.append(QueryTokens.token(ctx.LEFT())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.string_expression(0))); + builder.append(TOKEN_COMMA); + builder.appendInline(visit(ctx.arithmetic_expression(0))); + builder.append(TOKEN_CLOSE_PAREN); + } else if (ctx.RIGHT() != null) { + builder.append(QueryTokens.token(ctx.RIGHT())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.string_expression(0))); + builder.append(TOKEN_COMMA); + builder.appendInline(visit(ctx.arithmetic_expression(0))); + builder.append(TOKEN_CLOSE_PAREN); } return builder; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java index 9ca9147b6e..332bbfdf78 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java @@ -105,4 +105,10 @@ 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)); + } + } From bc3e8442a86c7ca620fea90ba39f966853d152c4 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 25 Jun 2024 11:50:34 +0200 Subject: [PATCH 06/11] Support REPLACE keyword in JPQL query parsing. --- .../springframework/data/jpa/repository/query/Jpql.g4 | 3 +++ .../data/jpa/repository/query/JpqlQueryRenderer.java | 9 +++++++++ .../data/jpa/repository/query/JpqlComplianceTests.java | 6 ++++++ 3 files changed, 18 insertions(+) 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 19d5a00283..b33aeb7cc1 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 @@ -533,6 +533,7 @@ functions_returning_strings | SUBSTRING '(' string_expression ',' arithmetic_expression (',' arithmetic_expression)? ')' | TRIM '(' ((trim_specification)? (trim_character)? FROM)? string_expression ')' | LOWER '(' string_expression ')' + | REPLACE '(' string_expression ',' string_expression ',' string_expression ')' | UPPER '(' string_expression ')' | LEFT '(' string_expression ',' arithmetic_expression ')' | RIGHT '(' string_expression ',' arithmetic_expression ')' @@ -822,6 +823,7 @@ reserved_word |ORDER |OUTER |POWER + |REPLACE |RIGHT |ROUND |SELECT @@ -954,6 +956,7 @@ ON : O N; OR : O R; ORDER : O R D E R; OUTER : O U T E R; +REPLACE : R E P L A C E; RIGHT : R I G H T; POWER : P O W E R; ROUND : R O U N D; 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 175f7e6095..9a9c5d629b 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 @@ -1798,6 +1798,15 @@ public QueryTokenStream visitFunctions_returning_strings(JpqlParser.Functions_re builder.append(TOKEN_COMMA); builder.appendInline(visit(ctx.arithmetic_expression(0))); builder.append(TOKEN_CLOSE_PAREN); + } else if (ctx.REPLACE() != null) { + builder.append(QueryTokens.token(ctx.REPLACE())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.string_expression(0))); + builder.append(TOKEN_COMMA); + builder.appendInline(visit(ctx.string_expression(1))); + builder.append(TOKEN_COMMA); + builder.appendInline(visit(ctx.string_expression(2))); + builder.append(TOKEN_CLOSE_PAREN); } return builder; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java index 332bbfdf78..2da770a771 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java @@ -111,4 +111,10 @@ 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"); + } + } From 1089d30f0fe35a780626096b049bccc92e58e088 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 25 Jun 2024 12:06:31 +0200 Subject: [PATCH 07/11] Support string concatination with double pipe in JPQL parsing. --- .../org/springframework/data/jpa/repository/query/Jpql.g4 | 1 + .../data/jpa/repository/query/JpqlQueryRenderer.java | 7 +++++++ .../data/jpa/repository/query/JpqlComplianceTests.java | 5 +++++ 3 files changed, 13 insertions(+) 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 b33aeb7cc1..c21312c9aa 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 @@ -450,6 +450,7 @@ string_expression | case_expression | function_invocation | '(' subquery ')' + | string_expression '||' string_expression ; datetime_expression 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 9a9c5d629b..6ce42dddad 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 @@ -22,6 +22,7 @@ import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_EQUALS; import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_OPEN_PAREN; import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_QUESTION_MARK; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_DOUBLE_PIPE; import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_SPACE; import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_CLOSE_PAREN; import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_OPEN_PAREN; @@ -35,6 +36,7 @@ import org.springframework.data.jpa.repository.query.JpqlParser.Set_fuctionContext; import org.springframework.data.jpa.repository.query.JpqlParser.Type_literalContext; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; +import org.springframework.util.ObjectUtils; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders a JPQL query without making any changes. @@ -1464,6 +1466,11 @@ public QueryTokenStream visitString_expression(JpqlParser.String_expressionConte builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.subquery())); builder.append(TOKEN_CLOSE_PAREN); + } else if (!ObjectUtils.isEmpty(ctx.string_expression())) { + + builder.appendInline(visit(ctx.string_expression(0))); + builder.append(TOKEN_DOUBLE_PIPE); + builder.appendExpression(visit(ctx.string_expression(1))); } return builder; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java index 2da770a771..58bb071648 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java @@ -117,4 +117,9 @@ void replaceStringFunctions() { assertQuery("SELECT REPLACE(e.name, ' ', '_') FROM Employee e"); } + @Test // GH-3136 + void stringConcatWithPipes() { + assertQuery("SELECT e.firstname || e.lastname AS name FROM Employee e"); + } + } From d1b87e25de83921fc227b428d7b2e8357e25bcf6 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 26 Jun 2024 14:10:03 +0200 Subject: [PATCH 08/11] Add missing tests for hibernate parser --- .../jpa/repository/query/HqlQueryRendererTests.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 8a23e279cd..6f0f211643 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 @@ -1684,4 +1684,16 @@ void entityNameWithPackageContainingReservedWord(String reservedWord) { String source = "select new com.company.%s.thing.stuff.ClassName(e.id) from Experience e".formatted(reservedWord); assertQuery(source); } + + @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"); + } } From 258aeb5a62c52cc672a2271aefed10f46a99d20a Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 26 Jun 2024 15:02:22 +0200 Subject: [PATCH 09/11] Update EQL parsing --- .../data/jpa/repository/query/Eql.g4 | 27 +++++++++- .../repository/query/EqlQueryRenderer.java | 51 +++++++++++++++++++ .../query/EqlQueryRendererTests.java | 50 ++++++++++++++++++ 3 files changed, 127 insertions(+), 1 deletion(-) 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 3ed025efb5..a7c84a83b9 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 @@ -309,6 +309,7 @@ scalar_expression | datetime_expression | boolean_expression | case_expression + | cast_expression | entity_type_expression ; @@ -450,6 +451,7 @@ string_expression | case_expression | function_invocation | '(' subquery ')' + | string_expression '||' string_expression ; datetime_expression @@ -534,6 +536,9 @@ functions_returning_strings | TRIM '(' ((trim_specification)? (trim_character)? FROM)? string_expression ')' | LOWER '(' string_expression ')' | UPPER '(' string_expression ')' + | REPLACE '(' string_expression ',' string_expression ',' string_expression ')' + | LEFT '(' string_expression ',' arithmetic_expression ')' + | RIGHT '(' string_expression ',' arithmetic_expression ')' ; trim_specification @@ -609,6 +614,18 @@ nullif_expression : NULLIF '(' scalar_expression ',' scalar_expression ')' ; +cast_expression + : CAST '(' string_expression AS type_literal ')' + ; + +type_literal + : STRING + | INTEGER + | LONG + | FLOAT + | DOUBLE + ; + /******************* Gaps in the spec. *******************/ @@ -630,6 +647,7 @@ identification_variable | ORDER | OUTER | POWER + | RIGHT | FLOOR | SIGN | TIME @@ -811,6 +829,8 @@ reserved_word |OR |ORDER |OUTER + |REPLACE + |RIGHT |POWER |ROUND |SELECT @@ -894,6 +914,7 @@ DATETIME : D A T E T I M E ; DELETE : D E L E T E; DESC : D E S C; DISTINCT : D I S T I N C T; +DOUBLE : D O U B L E; END : E N D; ELSE : E L S E; EMPTY : E M P T Y; @@ -906,6 +927,7 @@ EXTRACT : E X T R A C T; FALSE : F A L S E; FETCH : F E T C H; FIRST : F I R S T; +FLOAT : F L O A T; FLOOR : F L O O R; FROM : F R O M; FUNCTION : F U N C T I O N; @@ -914,6 +936,7 @@ HAVING : H A V I N G; IN : I N; INDEX : I N D E X; INNER : I N N E R; +INTEGER : I N T E G E R; INTERSECT : I N T E R S E C T; IS : I S; JOIN : J O I N; @@ -944,6 +967,8 @@ ORDER : O R D E R; OUTER : O U T E R; POWER : P O W E R; REGEXP : R E G E X P; +REPLACE : R E P L A C E; +RIGHT : R I G H T; ROUND : R O U N D; SELECT : S E L E C T; SET : S E T; @@ -951,6 +976,7 @@ SIGN : S I G N; SIZE : S I Z E; SOME : S O M E; SQRT : S Q R T; +STRING : S T R I N G; SUBSTRING : S U B S T R I N G; SUM : S U M; THEN : T H E N; @@ -970,7 +996,6 @@ WHERE : W H E R E; EQUAL : '=' ; NOT_EQUAL : '<>' | '!=' ; - CHARACTER : '\'' (~ ('\'' | '\\')) '\'' ; IDENTIFICATION_VARIABLE : ('a' .. 'z' | 'A' .. 'Z' | '\u0080' .. '\ufffe' | '$' | '_') ('a' .. 'z' | 'A' .. 'Z' | '\u0080' .. '\ufffe' | '0' .. '9' | '$' | '_')* ; STRINGLITERAL : '\'' (~ ('\'' | '\\')|'\\')* '\'' ; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java index 17e6e51a55..13250e9a69 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java @@ -23,6 +23,7 @@ import org.antlr.v4.runtime.tree.ParseTree; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; +import org.springframework.util.ObjectUtils; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders an EQL query without making any changes. @@ -1008,6 +1009,8 @@ public QueryTokenStream visitScalar_expression(EqlParser.Scalar_expressionContex builder.append(visit(ctx.case_expression())); } else if (ctx.entity_type_expression() != null) { builder.append(visit(ctx.entity_type_expression())); + } else if (ctx.cast_expression() != null) { + return (visit(ctx.cast_expression())); } return builder; @@ -1595,6 +1598,11 @@ public QueryTokenStream visitString_expression(EqlParser.String_expressionContex builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.subquery())); builder.append(TOKEN_CLOSE_PAREN); + } else if (!ObjectUtils.isEmpty(ctx.string_expression())) { + + builder.appendInline(visit(ctx.string_expression(0))); + builder.append(TOKEN_DOUBLE_PIPE); + builder.appendExpression(visit(ctx.string_expression(1))); } return builder; @@ -1926,6 +1934,29 @@ public QueryTokenStream visitFunctions_returning_strings(EqlParser.Functions_ret builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.string_expression(0))); builder.append(TOKEN_CLOSE_PAREN); + } else if (ctx.LEFT() != null) { + builder.append(QueryTokens.token(ctx.LEFT())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.string_expression(0))); + builder.append(TOKEN_COMMA); + builder.appendInline(visit(ctx.arithmetic_expression(0))); + builder.append(TOKEN_CLOSE_PAREN); + } else if (ctx.RIGHT() != null) { + builder.append(QueryTokens.token(ctx.RIGHT())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.string_expression(0))); + builder.append(TOKEN_COMMA); + builder.appendInline(visit(ctx.arithmetic_expression(0))); + builder.append(TOKEN_CLOSE_PAREN); + } else if (ctx.REPLACE() != null) { + builder.append(QueryTokens.token(ctx.REPLACE())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.string_expression(0))); + builder.append(TOKEN_COMMA); + builder.appendInline(visit(ctx.string_expression(1))); + builder.append(TOKEN_COMMA); + builder.appendInline(visit(ctx.string_expression(2))); + builder.append(TOKEN_CLOSE_PAREN); } return builder; @@ -2055,6 +2086,26 @@ public QueryTokenStream visitCase_expression(EqlParser.Case_expressionContext ct } } + @Override + public QueryRendererBuilder visitCast_expression(EqlParser.Cast_expressionContext ctx) { + QueryRendererBuilder builder = QueryRenderer.builder(); + builder.append(QueryTokens.token(ctx.CAST())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.string_expression())); + builder.append(QueryTokens.expression(ctx.AS())); + builder.appendInline(visit(ctx.type_literal())); + builder.append(TOKEN_CLOSE_PAREN); + return builder; + } + + @Override + public QueryRendererBuilder visitType_literal(EqlParser.Type_literalContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + ctx.children.forEach(it -> builder.append(QueryTokens.expression(it.getText()))); + return builder; + } + @Override public QueryTokenStream visitGeneral_case_expression(EqlParser.General_case_expressionContext ctx) { 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 91a4bb761e..7163774bcc 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 @@ -1046,4 +1046,54 @@ void lateralShouldBeAValidParameter() { assertQuery("select e from Employee e where e.lateral = :_lateral"); assertQuery("select te from TestEntity te where te.lateral = :lateral"); } + + @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 + """); + } + + @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"); + } } From 5953c2797668b6d9b45f72f168dcf3dfa3240406 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 27 Jun 2024 09:13:05 +0200 Subject: [PATCH 10/11] Alter existing cast function for eql and remove newly introduced --- .../data/jpa/repository/query/Eql.g4 | 11 ++-- .../repository/query/EqlQueryRenderer.java | 29 ++++------ .../repository/query/EqlComplianceTests.java | 51 +++++++++++++++++ .../query/EqlQueryRendererTests.java | 57 ------------------- 4 files changed, 67 insertions(+), 81 deletions(-) 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 a7c84a83b9..b41427a202 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 @@ -309,7 +309,7 @@ scalar_expression | datetime_expression | boolean_expression | case_expression - | cast_expression + | cast_function | entity_type_expression ; @@ -548,7 +548,7 @@ trim_specification ; cast_function - : CAST '(' single_valued_path_expression identification_variable ('(' numeric_literal (',' numeric_literal)* ')')? ')' + : CAST '(' single_valued_path_expression (identification_variable)? identification_variable ('(' numeric_literal (',' numeric_literal)* ')')? ')' ; function_invocation @@ -614,10 +614,6 @@ nullif_expression : NULLIF '(' scalar_expression ',' scalar_expression ')' ; -cast_expression - : CAST '(' string_expression AS type_literal ')' - ; - type_literal : STRING | INTEGER @@ -638,6 +634,7 @@ trim_character identification_variable : IDENTIFICATION_VARIABLE | f=(COUNT + | AS | DATE | FROM | INNER @@ -653,6 +650,7 @@ identification_variable | TIME | TYPE | VALUE) + | type_literal ; constructor_name @@ -949,6 +947,7 @@ LIKE : L I K E; LN : L N; LOCAL : L O C A L; LOCATE : L O C A T E; +LONG : L O N G; LOWER : L O W E R; MAX : M A X; MEMBER : M E M B E R; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java index 13250e9a69..34e085ba47 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java @@ -1009,8 +1009,8 @@ public QueryTokenStream visitScalar_expression(EqlParser.Scalar_expressionContex builder.append(visit(ctx.case_expression())); } else if (ctx.entity_type_expression() != null) { builder.append(visit(ctx.entity_type_expression())); - } else if (ctx.cast_expression() != null) { - return (visit(ctx.cast_expression())); + } else if (ctx.cast_function() != null) { + return (visit(ctx.cast_function())); } return builder; @@ -1935,6 +1935,7 @@ public QueryTokenStream visitFunctions_returning_strings(EqlParser.Functions_ret builder.appendInline(visit(ctx.string_expression(0))); builder.append(TOKEN_CLOSE_PAREN); } else if (ctx.LEFT() != null) { + builder.append(QueryTokens.token(ctx.LEFT())); builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.string_expression(0))); @@ -1942,6 +1943,7 @@ public QueryTokenStream visitFunctions_returning_strings(EqlParser.Functions_ret builder.appendInline(visit(ctx.arithmetic_expression(0))); builder.append(TOKEN_CLOSE_PAREN); } else if (ctx.RIGHT() != null) { + builder.append(QueryTokens.token(ctx.RIGHT())); builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.string_expression(0))); @@ -1949,6 +1951,7 @@ public QueryTokenStream visitFunctions_returning_strings(EqlParser.Functions_ret builder.appendInline(visit(ctx.arithmetic_expression(0))); builder.append(TOKEN_CLOSE_PAREN); } else if (ctx.REPLACE() != null) { + builder.append(QueryTokens.token(ctx.REPLACE())); builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.string_expression(0))); @@ -1983,9 +1986,9 @@ public QueryTokenStream visitCast_function(EqlParser.Cast_functionContext ctx) { builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.single_valued_path_expression())); builder.append(TOKEN_SPACE); - builder.appendInline(visit(ctx.identification_variable())); + builder.appendInline(QueryTokenStream.concat(ctx.identification_variable(), this::visit, TOKEN_SPACE)); - if (ctx.numeric_literal() != null) { + if (!ObjectUtils.isEmpty(ctx.numeric_literal())) { builder.append(TOKEN_OPEN_PAREN); builder.appendInline(QueryTokenStream.concat(ctx.numeric_literal(), this::visit, TOKEN_COMMA)); @@ -2086,18 +2089,6 @@ public QueryTokenStream visitCase_expression(EqlParser.Case_expressionContext ct } } - @Override - public QueryRendererBuilder visitCast_expression(EqlParser.Cast_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.token(ctx.CAST())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.string_expression())); - builder.append(QueryTokens.expression(ctx.AS())); - builder.appendInline(visit(ctx.type_literal())); - builder.append(TOKEN_CLOSE_PAREN); - return builder; - } - @Override public QueryRendererBuilder visitType_literal(EqlParser.Type_literalContext ctx) { @@ -2226,9 +2217,11 @@ public QueryTokenStream visitIdentification_variable(EqlParser.Identification_va return QueryRendererBuilder.from(QueryTokens.expression(ctx.IDENTIFICATION_VARIABLE())); } else if (ctx.f != null) { return QueryRendererBuilder.from(QueryTokens.expression(ctx.f)); - } else { - return QueryRenderer.builder(); + } else if (ctx.type_literal() != null) { + return visit(ctx.type_literal()); } + + return QueryRenderer.builder(); } @Override diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java index bbfbffe1ab..a0c556ee99 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java @@ -20,6 +20,8 @@ import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer; /** @@ -414,4 +416,53 @@ void isNullAndIsNotNull() { 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-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-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 jpqlCast(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"); + } } 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 7163774bcc..242ec81b83 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 @@ -1039,61 +1039,4 @@ void entityNameWithPackageContainingReservedWord(String reservedWord) { String source = "select new com.company.%s.thing.stuff.ClassName(e.id) from Experience e".formatted(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-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 - """); - } - - @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"); - } } From 5bd8055b96c1cbe2fa345bd9649ab35d4bce06f2 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 28 Jun 2024 11:46:42 +0200 Subject: [PATCH 11/11] Make sure sorting is rendered correctly for JPQL query using set operator. --- .../jpa/repository/query/JpqlCountQueryTransformer.java | 3 +++ .../jpa/repository/query/JpqlSortedQueryTransformer.java | 6 +++++- .../jpa/repository/query/JpqlQueryTransformerTests.java | 9 +++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) 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 2c26df1357..63f56862be 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 @@ -68,6 +68,9 @@ public QueryTokenStream visitSelect_query(JpqlParser.Select_queryContext ctx) { if (ctx.having_clause() != null) { builder.appendExpression(visit(ctx.having_clause())); } + if(ctx.set_fuction() != null) { + builder.appendExpression(visit(ctx.set_fuction())); + } return builder; } 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 9cd43176de..f182ad2039 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 @@ -77,7 +77,11 @@ public QueryTokenStream visitSelect_query(JpqlParser.Select_queryContext ctx) { builder.appendExpression(visit(ctx.having_clause())); } - doVisitOrderBy(builder, ctx); + if(ctx.set_fuction() != null) { + builder.appendExpression(visit(ctx.set_fuction())); + } else { + doVisitOrderBy(builder, ctx); + } return builder; } 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 eb04b8de07..547e93f9c7 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 @@ -776,6 +776,15 @@ void sortingRecognizesJoinAliases() { """); } + @Test // GH-3427 + 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"); + } + static Stream queriesWithReservedWordsAsIdentifiers() { return Stream.of( //