From b98e3705d5aebb7978bde626028e59891058be50 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 9 May 2025 15:51:17 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20JPQL=20and=20EQL=20`CAST(=E2=80=A6)`=20fu?= =?UTF-8?q?nction=20parsing.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now define arithmetic, string, and typed cast functions to support all potential variants of casting supported by JPQL and EQL. Closes #3863 --- .../data/jpa/repository/query/Eql.g4 | 22 +- .../data/jpa/repository/query/Jpql.g4 | 40 ++- .../repository/query/EqlQueryRenderer.java | 65 ++++- .../repository/query/JpqlQueryRenderer.java | 275 +++++++++++++----- .../query/EqlQueryRendererTests.java | 9 + .../query/JpqlQueryRendererTests.java | 29 +- 6 files changed, 333 insertions(+), 107 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 9a1136ddd..17fcc1ece 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 @@ -441,7 +441,8 @@ arithmetic_primary | functions_returning_numerics | aggregate_expression | case_expression - | cast_function + | arithmetic_cast_function + | type_cast_function | function_invocation | '(' subquery ')' ; @@ -454,6 +455,8 @@ string_expression | aggregate_expression | case_expression | function_invocation + | string_cast_function + | type_cast_function | '(' subquery ')' ; @@ -547,8 +550,16 @@ trim_specification | BOTH ; -cast_function - : CAST '(' single_valued_path_expression identification_variable ('(' numeric_literal (',' numeric_literal)* ')')? ')' +arithmetic_cast_function + : CAST '(' string_expression (AS)? f=(INTEGER|LONG|FLOAT|DOUBLE) ')' + ; + +type_cast_function + : CAST '(' scalar_expression (AS)? identification_variable ('(' numeric_literal (',' numeric_literal)* ')')? ')' + ; + +string_cast_function + : CAST '(' scalar_expression (AS)? STRING ')' ; function_invocation @@ -903,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; @@ -916,6 +928,7 @@ FALSE : F A L S E; FETCH : F E T C H; FIRST : F I R S T; FLOOR : F L O O R; +FLOAT : F L O A T; FROM : F R O M; FUNCTION : F U N C T I O N; GROUP : G R O U P; @@ -925,6 +938,7 @@ 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; +INTEGER : I N T E G E R; JOIN : J O I N; KEY : K E Y; LAST : L A S T; @@ -935,6 +949,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; @@ -961,6 +976,7 @@ SIZE : S I Z E; SOME : S O M E; SQRT : S Q R T; SUBSTRING : S U B S T R I N G; +STRING : S T R I N G; SUM : S U M; THEN : T H E N; TIME : T I M E; 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 98a0df214..140abdbbd 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 @@ -387,13 +387,15 @@ all_or_any_expression ; comparison_expression - : string_expression comparison_operator (string_expression | all_or_any_expression) - | boolean_expression op=(EQUAL | NOT_EQUAL) (boolean_expression | all_or_any_expression) - | enum_expression op=(EQUAL | NOT_EQUAL) (enum_expression | all_or_any_expression) - | datetime_expression comparison_operator (datetime_expression | all_or_any_expression) - | entity_expression op=(EQUAL | NOT_EQUAL) (entity_expression | all_or_any_expression) - | arithmetic_expression comparison_operator (arithmetic_expression | all_or_any_expression) - | entity_type_expression op=(EQUAL | NOT_EQUAL) entity_type_expression + : string_expression comparison_operator (string_expression | all_or_any_expression) #StringComparison + | boolean_expression op=(EQUAL | NOT_EQUAL) (boolean_expression | all_or_any_expression) #BooleanComparison + | boolean_expression #DirectBooleanCheck + | enum_expression op=(EQUAL | NOT_EQUAL) (enum_expression | all_or_any_expression) #EnumComparison + | datetime_expression comparison_operator (datetime_expression | all_or_any_expression) #DatetimeComparison + | entity_expression op=(EQUAL | NOT_EQUAL) (entity_expression | all_or_any_expression) #EntityComparison + | arithmetic_expression comparison_operator (arithmetic_expression | all_or_any_expression) #ArithmeticComparison + | entity_type_expression op=(EQUAL | NOT_EQUAL) entity_type_expression #EntityTypeComparison + | string_expression REGEXP string_literal #RegexpComparison ; comparison_operator @@ -427,6 +429,8 @@ arithmetic_primary | functions_returning_numerics | aggregate_expression | case_expression + | arithmetic_cast_function + | type_cast_function | function_invocation | '(' subquery ')' ; @@ -439,6 +443,8 @@ string_expression | aggregate_expression | case_expression | function_invocation + | string_cast_function + | type_cast_function | '(' subquery ')' ; @@ -532,6 +538,17 @@ trim_specification | BOTH ; +arithmetic_cast_function + : CAST '(' string_expression (AS)? f=(INTEGER|LONG|FLOAT|DOUBLE) ')' + ; + +type_cast_function + : CAST '(' scalar_expression (AS)? identification_variable ('(' numeric_literal (',' numeric_literal)* ')')? ')' + ; + +string_cast_function + : CAST '(' scalar_expression (AS)? STRING ')' + ; function_invocation : FUNCTION '(' function_name (',' function_arg)* ')' @@ -750,6 +767,7 @@ reserved_word |BOTH |BY |CASE + |CAST |CEILING |COALESCE |CONCAT @@ -870,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; @@ -882,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; @@ -894,6 +914,7 @@ FALSE : F A L S E; FETCH : F E T C H; FIRST : F I R S T; FLOOR : F L O O R; +FLOAT : F L O A T; FROM : F R O M; FUNCTION : F U N C T I O N; GROUP : G R O U P; @@ -901,7 +922,9 @@ 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; +INTEGER : I N T E G E R; JOIN : J O I N; KEY : K E Y; LAST : L A S T; @@ -912,6 +935,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; @@ -929,6 +953,7 @@ OR : O R; 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; ROUND : R O U N D; SELECT : S E L E C T; SET : S E T; @@ -937,6 +962,7 @@ SIZE : S I Z E; SOME : S O M E; SQRT : S Q R T; SUBSTRING : S U B S T R I N G; +STRING : S T R I N G; SUM : S U M; THEN : T H E N; TIME : T I M E; 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 8225545c8..c5f5236a7 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 java.util.List; import org.antlr.v4.runtime.tree.ParseTree; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; +import org.springframework.util.CollectionUtils; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders an EQL query without making any changes. @@ -49,7 +50,7 @@ class EqlQueryRenderer extends EqlBaseVisitor { } else if (ctx.delete_statement() != null) { return visit(ctx.delete_statement()); } else { - return QueryRenderer.builder(); + return QueryTokenStream.empty(); } } @@ -1312,14 +1313,14 @@ class EqlQueryRenderer extends EqlBaseVisitor { QueryRendererBuilder builder = QueryRenderer.builder(); if (ctx.identification_variable() != null) { - builder.append(visit(ctx.identification_variable())); + return visit(ctx.identification_variable()); } else if (ctx.input_parameter() != null) { - builder.append(visit(ctx.input_parameter())); + return visit(ctx.input_parameter()); } else if (ctx.literal() != null) { - builder.append(visit(ctx.literal())); + return visit(ctx.literal()); } - return builder; + return QueryTokenStream.empty(); } @Override @@ -1565,8 +1566,10 @@ class EqlQueryRenderer extends EqlBaseVisitor { builder.append(visit(ctx.aggregate_expression())); } else if (ctx.case_expression() != null) { builder.append(visit(ctx.case_expression())); - } else if (ctx.cast_function() != null) { - builder.append(visit(ctx.cast_function())); + } else if (ctx.arithmetic_cast_function() != null) { + builder.append(visit(ctx.arithmetic_cast_function())); + } else if (ctx.type_cast_function() != null) { + builder.append(visit(ctx.type_cast_function())); } else if (ctx.function_invocation() != null) { builder.append(visit(ctx.function_invocation())); } else if (ctx.subquery() != null) { @@ -1596,6 +1599,10 @@ class EqlQueryRenderer extends EqlBaseVisitor { builder.append(visit(ctx.aggregate_expression())); } else if (ctx.case_expression() != null) { builder.append(visit(ctx.case_expression())); + } else if (ctx.string_cast_function() != null) { + builder.append(visit(ctx.string_cast_function())); + } else if (ctx.type_cast_function() != null) { + builder.append(visit(ctx.type_cast_function())); } else if (ctx.function_invocation() != null) { builder.append(visit(ctx.function_invocation())); } else if (ctx.subquery() != null) { @@ -1952,17 +1959,36 @@ class EqlQueryRenderer extends EqlBaseVisitor { } @Override - public QueryTokenStream visitCast_function(EqlParser.Cast_functionContext ctx) { + public QueryTokenStream visitArithmetic_cast_function(EqlParser.Arithmetic_cast_functionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); builder.append(QueryTokens.token(ctx.CAST())); builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.single_valued_path_expression())); - builder.append(TOKEN_SPACE); + builder.appendExpression(visit(ctx.string_expression())); + if (ctx.AS() != null) { + builder.append(QueryTokens.expression(ctx.AS())); + } + builder.append(QueryTokens.token(ctx.f)); + builder.append(TOKEN_CLOSE_PAREN); + + return builder; + } + + @Override + public QueryTokenStream visitType_cast_function(EqlParser.Type_cast_functionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + 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 (ctx.numeric_literal() != null) { + if (!CollectionUtils.isEmpty(ctx.numeric_literal())) { builder.append(TOKEN_OPEN_PAREN); builder.appendInline(QueryTokenStream.concat(ctx.numeric_literal(), this::visit, TOKEN_COMMA)); @@ -1973,6 +1999,23 @@ class EqlQueryRenderer extends EqlBaseVisitor { return builder; } + @Override + public QueryTokenStream visitString_cast_function(EqlParser.String_cast_functionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + 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.append(QueryTokens.token(ctx.STRING())); + builder.append(TOKEN_CLOSE_PAREN); + + return builder; + } + @Override public QueryTokenStream visitFunction_invocation(EqlParser.Function_invocationContext ctx) { 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 fad4187df..a68932cbe 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 @@ -25,6 +25,7 @@ import org.antlr.v4.runtime.tree.ParseTree; import org.springframework.data.jpa.repository.query.JpqlParser.NullsPrecedenceContext; import org.springframework.data.jpa.repository.query.JpqlParser.Reserved_wordContext; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; +import org.springframework.util.CollectionUtils; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders a JPQL query without making any changes. @@ -1273,115 +1274,174 @@ class JpqlQueryRenderer extends JpqlBaseVisitor { } @Override - public QueryTokenStream visitComparison_expression(JpqlParser.Comparison_expressionContext ctx) { + public QueryTokenStream visitStringComparison(JpqlParser.StringComparisonContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - if (!ctx.string_expression().isEmpty()) { + builder.appendInline(visit(ctx.string_expression(0))); + builder.append(visit(ctx.comparison_operator())); - builder.appendExpression(visit(ctx.string_expression(0))); - builder.appendExpression(visit(ctx.comparison_operator())); - - if (ctx.string_expression(1) != null) { - builder.appendExpression(visit(ctx.string_expression(1))); - } else { - builder.appendExpression(visit(ctx.all_or_any_expression())); - } - } else if (!ctx.boolean_expression().isEmpty()) { - - builder.appendInline(visit(ctx.boolean_expression(0))); - builder.append(QueryTokens.ventilated(ctx.op)); - - if (ctx.boolean_expression(1) != null) { - builder.appendExpression(visit(ctx.boolean_expression(1))); - } else { - builder.appendExpression(visit(ctx.all_or_any_expression())); - } - } else if (!ctx.enum_expression().isEmpty()) { - - builder.appendInline(visit(ctx.enum_expression(0))); - builder.append(QueryTokens.ventilated(ctx.op)); - - if (ctx.enum_expression(1) != null) { - builder.appendExpression(visit(ctx.enum_expression(1))); - } else { - builder.appendExpression(visit(ctx.all_or_any_expression())); - } - } else if (!ctx.datetime_expression().isEmpty()) { - - builder.appendExpression(visit(ctx.datetime_expression(0))); - builder.appendExpression(visit(ctx.comparison_operator())); - - if (ctx.datetime_expression(1) != null) { - builder.appendExpression(visit(ctx.datetime_expression(1))); - } else { - builder.appendExpression(visit(ctx.all_or_any_expression())); - } - } else if (!ctx.entity_expression().isEmpty()) { - - builder.appendInline(visit(ctx.entity_expression(0))); - builder.append(QueryTokens.ventilated(ctx.op)); - - if (ctx.entity_expression(1) != null) { - builder.appendExpression(visit(ctx.entity_expression(1))); - } else { - builder.appendExpression(visit(ctx.all_or_any_expression())); - } - } else if (!ctx.arithmetic_expression().isEmpty()) { - - builder.appendExpression(visit(ctx.arithmetic_expression(0))); - builder.appendExpression(visit(ctx.comparison_operator())); - - if (ctx.arithmetic_expression(1) != null) { - builder.appendExpression(visit(ctx.arithmetic_expression(1))); - } else { - builder.appendExpression(visit(ctx.all_or_any_expression())); - } - } else if (!ctx.entity_type_expression().isEmpty()) { - - builder.appendInline(visit(ctx.entity_type_expression(0))); - builder.append(QueryTokens.ventilated(ctx.op)); - builder.appendExpression(visit(ctx.entity_type_expression(1))); + if (ctx.string_expression(1) != null) { + builder.append(visit(ctx.string_expression(1))); + } else { + builder.append(visit(ctx.all_or_any_expression())); } return builder; } + @Override + public QueryTokenStream visitBooleanComparison(JpqlParser.BooleanComparisonContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendInline(visit(ctx.boolean_expression(0))); + builder.append(QueryTokens.ventilated(ctx.op)); + + if (ctx.boolean_expression(1) != null) { + builder.append(visit(ctx.boolean_expression(1))); + } else { + builder.append(visit(ctx.all_or_any_expression())); + } + + return builder; + } + + @Override + public QueryTokenStream visitDirectBooleanCheck(JpqlParser.DirectBooleanCheckContext ctx) { + return visit(ctx.boolean_expression()); + } + + @Override + public QueryTokenStream visitEnumComparison(JpqlParser.EnumComparisonContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendInline(visit(ctx.enum_expression(0))); + builder.append(QueryTokens.ventilated(ctx.op)); + + if (ctx.enum_expression(1) != null) { + builder.append(visit(ctx.enum_expression(1))); + } else { + builder.append(visit(ctx.all_or_any_expression())); + } + + return builder; + } + + @Override + public QueryTokenStream visitDatetimeComparison(JpqlParser.DatetimeComparisonContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendInline(visit(ctx.datetime_expression(0))); + builder.append(QueryTokens.ventilated(ctx.comparison_operator().op)); + + if (ctx.datetime_expression(1) != null) { + builder.append(visit(ctx.datetime_expression(1))); + } else { + builder.append(visit(ctx.all_or_any_expression())); + } + + return builder; + } + + @Override + public QueryTokenStream visitEntityComparison(JpqlParser.EntityComparisonContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendExpression(visit(ctx.entity_expression(0))); + builder.append(QueryTokens.expression(ctx.op)); + + if (ctx.entity_expression(1) != null) { + builder.append(visit(ctx.entity_expression(1))); + } else { + builder.append(visit(ctx.all_or_any_expression())); + } + + return builder; + } + + @Override + public QueryTokenStream visitArithmeticComparison(JpqlParser.ArithmeticComparisonContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(visit(ctx.arithmetic_expression(0))); + builder.append(visit(ctx.comparison_operator())); + + if (ctx.arithmetic_expression(1) != null) { + builder.append(visit(ctx.arithmetic_expression(1))); + } else { + builder.append(visit(ctx.all_or_any_expression())); + } + + return builder; + } + + @Override + public QueryTokenStream visitEntityTypeComparison(JpqlParser.EntityTypeComparisonContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendInline(visit(ctx.entity_type_expression(0))); + builder.append(QueryTokens.ventilated(ctx.op)); + builder.append(visit(ctx.entity_type_expression(1))); + + return builder; + } + + @Override + public QueryTokenStream visitRegexpComparison(JpqlParser.RegexpComparisonContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendExpression(visit(ctx.string_expression())); + builder.append(QueryTokens.expression(ctx.REGEXP())); + builder.appendExpression(visit(ctx.string_literal())); + + return builder; + } + @Override public QueryTokenStream visitComparison_operator(JpqlParser.Comparison_operatorContext ctx) { - return QueryRenderer.from(QueryTokens.token(ctx.op)); + return QueryRendererBuilder.from(QueryTokens.ventilated(ctx.op)); } @Override public QueryTokenStream visitArithmetic_expression(JpqlParser.Arithmetic_expressionContext ctx) { + QueryRendererBuilder builder = QueryRenderer.builder(); + if (ctx.arithmetic_expression() != null) { - QueryRendererBuilder builder = QueryRenderer.builder(); builder.append(visit(ctx.arithmetic_expression())); - builder.append(QueryTokens.ventilated(ctx.op)); + builder.append(QueryTokens.expression(ctx.op)); builder.append(visit(ctx.arithmetic_term())); - return builder; } else { - return visit(ctx.arithmetic_term()); + builder.append(visit(ctx.arithmetic_term())); } + + return builder; } @Override public QueryTokenStream visitArithmetic_term(JpqlParser.Arithmetic_termContext ctx) { + QueryRendererBuilder builder = QueryRenderer.builder(); + if (ctx.arithmetic_term() != null) { - QueryRendererBuilder builder = QueryRenderer.builder(); builder.appendInline(visit(ctx.arithmetic_term())); builder.append(QueryTokens.ventilated(ctx.op)); builder.append(visit(ctx.arithmetic_factor())); - - return builder; } else { - return visit(ctx.arithmetic_factor()); + builder.append(visit(ctx.arithmetic_factor())); } + + return builder; } @Override @@ -1392,8 +1452,7 @@ class JpqlQueryRenderer extends JpqlBaseVisitor { if (ctx.op != null) { builder.append(QueryTokens.token(ctx.op)); } - - builder.append(visit(ctx.arithmetic_primary())); + builder.appendInline(visit(ctx.arithmetic_primary())); return builder; } @@ -1420,6 +1479,10 @@ class JpqlQueryRenderer extends JpqlBaseVisitor { builder.append(visit(ctx.aggregate_expression())); } else if (ctx.case_expression() != null) { builder.append(visit(ctx.case_expression())); + } else if (ctx.arithmetic_cast_function() != null) { + builder.append(visit(ctx.arithmetic_cast_function())); + } else if (ctx.type_cast_function() != null) { + builder.append(visit(ctx.type_cast_function())); } else if (ctx.function_invocation() != null) { builder.append(visit(ctx.function_invocation())); } else if (ctx.subquery() != null) { @@ -1449,6 +1512,10 @@ class JpqlQueryRenderer extends JpqlBaseVisitor { builder.append(visit(ctx.aggregate_expression())); } else if (ctx.case_expression() != null) { builder.append(visit(ctx.case_expression())); + } else if (ctx.string_cast_function() != null) { + builder.append(visit(ctx.string_cast_function())); + } else if (ctx.type_cast_function() != null) { + builder.append(visit(ctx.type_cast_function())); } else if (ctx.function_invocation() != null) { builder.append(visit(ctx.function_invocation())); } else if (ctx.subquery() != null) { @@ -1793,6 +1860,62 @@ class JpqlQueryRenderer extends JpqlBaseVisitor { } } + @Override + public QueryTokenStream visitArithmetic_cast_function(JpqlParser.Arithmetic_cast_functionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.token(ctx.CAST())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendExpression(visit(ctx.string_expression())); + if (ctx.AS() != null) { + builder.append(QueryTokens.expression(ctx.AS())); + } + builder.append(QueryTokens.token(ctx.f)); + builder.append(TOKEN_CLOSE_PAREN); + + return builder; + } + + @Override + public QueryTokenStream visitType_cast_function(JpqlParser.Type_cast_functionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + 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())) { + + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(QueryTokenStream.concat(ctx.numeric_literal(), this::visit, TOKEN_COMMA)); + builder.append(TOKEN_CLOSE_PAREN); + } + builder.append(TOKEN_CLOSE_PAREN); + + return builder; + } + + @Override + public QueryTokenStream visitString_cast_function(JpqlParser.String_cast_functionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.token(ctx.CAST())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendExpression(visit(ctx.scalar_expression())); + builder.append(QueryTokens.expression(ctx.AS())); + builder.append(QueryTokens.token(ctx.STRING())); + builder.append(TOKEN_CLOSE_PAREN); + + return builder; + } + @Override public QueryTokenStream visitFunction_invocation(JpqlParser.Function_invocationContext 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 f954bcc74..51b5bd419 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 @@ -338,6 +338,15 @@ class EqlQueryRendererTests { """); } + @Test // GH-3024, GH-3863 + void casting() { + + assertQuery(""" + select cast(i as string) from Item i where cast(i.date as date) <= cast(:currentDateTime as date) + """); + assertQuery("SELECT e FROM Employee e WHERE CAST(e.salary NUMERIC(10, 2)) > 0.0"); + } + @Test void pathExpressionsNamedParametersExample() { 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 33bef4a39..a7a6b2449 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 @@ -291,7 +291,7 @@ class JpqlQueryRendererTests { assertQuery(""" SELECT b.name, b.ISBN FROM Order o JOIN TREAT(o.product AS Book) b - """); + """); } @Test @@ -300,7 +300,7 @@ class JpqlQueryRendererTests { assertQuery(""" SELECT e FROM Employee e JOIN TREAT(e.projects AS LargeProject) lp WHERE lp.budget > 1000 - """); + """); } /** @@ -315,7 +315,7 @@ class JpqlQueryRendererTests { WHERE TREAT(p AS LargeProject).budget > 1000 OR TREAT(p AS SmallProject).name LIKE 'Persist%' OR p.description LIKE "cost overrun" - """); + """); } @Test @@ -326,7 +326,7 @@ class JpqlQueryRendererTests { WHERE TREAT(p AS LargeProject).budget > 1000 OR TREAT(p AS SmallProject).name LIKE 'Persist%' OR p.description LIKE 'cost overrun' - """); + """); } @Test @@ -336,7 +336,16 @@ class JpqlQueryRendererTests { SELECT e FROM Employee e WHERE TREAT(e AS Exempt).vacationDays > 10 OR TREAT(e AS Contractor).hours > 100 - """); + """); + } + + @Test // GH-3024, GH-3863 + void casting() { + + assertQuery(""" + select cast(i as string) from Item i where cast(i.date as date) <= cast(:currentDateTime as date) + """); + assertQuery("SELECT e FROM Employee e WHERE CAST(e.salary NUMERIC(10, 2)) > 0.0"); } @Test @@ -400,7 +409,7 @@ class JpqlQueryRendererTests { WHERE emp.salary > ALL (SELECT m.salary FROM Manager m WHERE m.department = emp.department) - """); + """); } @Test @@ -412,7 +421,7 @@ class JpqlQueryRendererTests { WHERE EXISTS (SELECT spouseEmp FROM Employee spouseEmp WHERE spouseEmp = emp.spouse) - """); + """); } @Test @@ -480,7 +489,7 @@ class JpqlQueryRendererTests { WHEN e.rating = 2 THEN e.salary * 1.05 ELSE e.salary * 1.01 END - """); + """); } @Test @@ -493,7 +502,7 @@ class JpqlQueryRendererTests { WHEN 2 THEN e.salary * 1.05 ELSE e.salary * 1.01 END - """); + """); } @Test @@ -533,7 +542,7 @@ class JpqlQueryRendererTests { SELECT e FROM Employee e WHERE TYPE(e) IN (Exempt, Contractor) - """); + """); } @Test