From dde9e07c24eeeaf222c17d639071367c187cb11d Mon Sep 17 00:00:00 2001 From: "Greg L. Turnquist" Date: Thu, 21 Sep 2023 15:12:32 -0500 Subject: [PATCH] Implement EQL parser. Implement support for EclipseLink Query Language (EQL), handling the various extensions it offers. Closes #3170 Original pull request: #3176 --- .../data/jpa/repository/query/Eql.g4 | 897 ++++++ .../jpa/provider/PersistenceProvider.java | 17 +- .../data/jpa/provider/PresenceDetector.java | 27 + .../jpa/repository/query/EqlQueryParser.java | 132 + .../repository/query/EqlQueryRenderer.java | 2540 +++++++++++++++++ .../repository/query/EqlQueryTransformer.java | 243 ++ .../repository/query/JpaQueryEnhancer.java | 15 + .../query/JpaQueryParsingToken.java | 6 + .../query/QueryEnhancerFactory.java | 19 +- .../repository/query/EqlComplianceTests.java | 403 +++ .../EqlParserQueryEnhancerUnitTests.java | 54 + .../query/EqlQueryRendererTests.java | 995 +++++++ .../query/EqlQueryTransformerTests.java | 783 +++++ .../query/EqlSpecificationTests.java | 887 ++++++ .../query/JpqlQueryRendererTests.java | 83 +- 15 files changed, 7056 insertions(+), 45 deletions(-) create mode 100644 spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PresenceDetector.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryParser.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryTransformer.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlSpecificationTests.java 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 new file mode 100644 index 000000000..a0aa24491 --- /dev/null +++ b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 @@ -0,0 +1,897 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +grammar Eql; + +@header { +/** + * Implementation of EclipseLink Query Language (EQL) + * See: + * * https://eclipse.dev/eclipselink/documentation/3.0/jpa/extensions/jpql.htm + * * https://wiki.eclipse.org/EclipseLink/UserGuide/JPA/Basic_JPA_Development/Querying/JPQL + * + * @author Greg Turnquist + * @since 3.2 + */ +} + +/* + Parser rules + */ + +start + : ql_statement EOF + ; + +ql_statement + : select_statement + | update_statement + | delete_statement + ; + +select_statement + : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (setOperator select_statement)* + ; + +setOperator + : UNION ALL? + | INTERSECT ALL? + | EXCEPT ALL? + ; + +update_statement + : update_clause (where_clause)? + ; + +delete_statement + : delete_clause (where_clause)? + ; + +from_clause + : FROM identification_variable_declaration (',' identificationVariableDeclarationOrCollectionMemberDeclaration )* + ; + +// This parser rule is needed to iterate over these two types from #from_clause +identificationVariableDeclarationOrCollectionMemberDeclaration + : identification_variable_declaration + | collection_member_declaration + | '(' subquery ')' identification_variable + ; + +identification_variable_declaration + : range_variable_declaration (join | fetch_join)* + ; + +range_variable_declaration + : (entity_name|function_invocation) AS? identification_variable + ; + +join + : join_spec join_association_path_expression AS? identification_variable? join_condition? + ; + +fetch_join + : join_spec FETCH join_association_path_expression AS? identification_variable? join_condition? + ; + +join_spec + : ((LEFT (OUTER)?) | INNER)? JOIN + ; + +join_condition + : ON conditional_expression + ; + +join_association_path_expression + : join_collection_valued_path_expression + | join_single_valued_path_expression + | TREAT '(' join_collection_valued_path_expression AS subtype ')' + | TREAT '(' join_single_valued_path_expression AS subtype ')' + ; + +join_collection_valued_path_expression + : (identification_variable '.')? (single_valued_embeddable_object_field '.')* collection_valued_field + ; + +join_single_valued_path_expression + : (identification_variable '.')? (single_valued_embeddable_object_field '.')* single_valued_object_field + ; + +collection_member_declaration + : IN '(' collection_valued_path_expression ')' AS? identification_variable + ; + +qualified_identification_variable + : map_field_identification_variable + | ENTRY '(' identification_variable ')' + ; + +map_field_identification_variable + : KEY '(' identification_variable ')' + | VALUE '(' identification_variable ')' + ; + +single_valued_path_expression + : qualified_identification_variable + | TREAT '(' qualified_identification_variable AS subtype ')' + | state_field_path_expression + | single_valued_object_path_expression + ; + +general_identification_variable + : identification_variable + | map_field_identification_variable + ; + +general_subpath + : simple_subpath + | treated_subpath ('.' single_valued_object_field)* + ; + +simple_subpath + : general_identification_variable + | general_identification_variable ('.' single_valued_object_field)* + ; + +treated_subpath + : TREAT '(' general_subpath AS subtype ')' + ; + +state_field_path_expression + : general_subpath '.' state_field + ; + +state_valued_path_expression + : state_field_path_expression + | general_identification_variable + ; + +single_valued_object_path_expression + : general_subpath '.' single_valued_object_field + ; + +collection_valued_path_expression + : general_subpath '.' collection_value_field // BNF at end of spec has a typo + ; + +update_clause + : UPDATE entity_name (AS? identification_variable)? SET update_item (',' update_item)* + ; + +update_item + : (identification_variable '.')? (single_valued_embeddable_object_field '.')* (state_field | single_valued_object_field) EQUAL new_value + ; + +new_value + : scalar_expression + | simple_entity_expression + | NULL + ; + +delete_clause + : DELETE FROM entity_name (AS? identification_variable)? + ; + +select_clause + : SELECT (DISTINCT)? select_item (',' select_item)* + ; + +select_item + : select_expression (AS? result_variable)? + ; + +select_expression + : single_valued_path_expression + | scalar_expression + | aggregate_expression + | identification_variable + | OBJECT '(' identification_variable ')' + | constructor_expression + ; + +constructor_expression + : NEW constructor_name '(' constructor_item (',' constructor_item)* ')' + ; + +constructor_item + : single_valued_path_expression + | scalar_expression + | aggregate_expression + | identification_variable + ; + +aggregate_expression + : (AVG | MAX | MIN | SUM) '(' (DISTINCT)? state_valued_path_expression ')' + | COUNT '(' (DISTINCT)? (identification_variable | state_valued_path_expression | single_valued_object_path_expression) ')' + | function_invocation + ; + +where_clause + : WHERE conditional_expression + ; + +groupby_clause + : GROUP BY groupby_item (',' groupby_item)* + ; + +groupby_item + : single_valued_path_expression + | identification_variable + | scalar_expression + ; + +having_clause + : HAVING conditional_expression + ; + +orderby_clause + : ORDER BY orderby_item (',' orderby_item)* + ; + +// TODO Error in spec BNF, correctly shown elsewhere in spec. +orderby_item + : state_field_path_expression (ASC | DESC)? nullsPrecedence? + | general_identification_variable (ASC | DESC)? nullsPrecedence? + | result_variable (ASC | DESC)? nullsPrecedence? + | string_expression (ASC | DESC)? nullsPrecedence? + | scalar_expression (ASC | DESC)? nullsPrecedence? + | + ; + +nullsPrecedence + : NULLS (FIRST | LAST) + ; + +subquery + : simple_select_clause subquery_from_clause (where_clause)? (groupby_clause)? (having_clause)? + ; + +subquery_from_clause + : FROM subselect_identification_variable_declaration (',' (subselect_identification_variable_declaration | collection_member_declaration))* + ; + +subselect_identification_variable_declaration + : identification_variable_declaration + | derived_path_expression AS? identification_variable (join)* + | derived_collection_member_declaration + ; + +derived_path_expression + : general_derived_path '.' single_valued_object_field + | general_derived_path '.' collection_valued_field + ; + +general_derived_path + : simple_derived_path + | treated_derived_path ('.' single_valued_object_field)* + ; + +simple_derived_path + : superquery_identification_variable ('.' single_valued_object_field)* + ; + +treated_derived_path + : TREAT '(' general_derived_path AS subtype ')' + ; + +derived_collection_member_declaration + : IN superquery_identification_variable '.' (single_valued_object_field '.')* collection_valued_field + ; + +simple_select_clause + : SELECT (DISTINCT)? simple_select_expression + ; + +simple_select_expression + : single_valued_path_expression + | scalar_expression + | aggregate_expression + | identification_variable + ; + +scalar_expression + : arithmetic_expression + | string_expression + | enum_expression + | datetime_expression + | boolean_expression + | case_expression + | entity_type_expression + ; + +conditional_expression + : conditional_term + | conditional_expression OR conditional_term + ; + +conditional_term + : conditional_factor + | conditional_term AND conditional_factor + ; + +conditional_factor + : (NOT)? conditional_primary + ; + +conditional_primary + : simple_cond_expression + | '(' conditional_expression ')' + ; + +simple_cond_expression + : comparison_expression + | between_expression + | in_expression + | like_expression + | null_comparison_expression + | empty_collection_comparison_expression + | collection_member_expression + | exists_expression + ; + +between_expression + : arithmetic_expression (NOT)? BETWEEN arithmetic_expression AND arithmetic_expression + | string_expression (NOT)? BETWEEN string_expression AND string_expression + | datetime_expression (NOT)? BETWEEN datetime_expression AND datetime_expression + ; + +in_expression + : (state_valued_path_expression | type_discriminator) (NOT)? IN (('(' in_item (',' in_item)* ')') | ( '(' subquery ')') | collection_valued_input_parameter) + ; + +in_item + : literal + | single_valued_input_parameter + ; + +like_expression + : string_expression (NOT)? LIKE pattern_value (ESCAPE escape_character)? + ; + +null_comparison_expression + : (single_valued_path_expression | input_parameter | nullif_expression) IS (NOT)? NULL + ; + +empty_collection_comparison_expression + : collection_valued_path_expression IS (NOT)? EMPTY + ; + +collection_member_expression + : entity_or_value_expression (NOT)? MEMBER (OF)? collection_valued_path_expression + ; + +entity_or_value_expression + : single_valued_object_path_expression + | state_field_path_expression + | simple_entity_or_value_expression + ; + +simple_entity_or_value_expression + : identification_variable + | input_parameter + | literal + ; + +exists_expression + : (NOT)? EXISTS '(' subquery ')' + ; + +all_or_any_expression + : (ALL | ANY | SOME) '(' subquery ')' + ; + +comparison_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 + : op=EQUAL + | op='>' + | op='>=' + | op='<' + | op='<=' + | op=NOT_EQUAL + ; + +arithmetic_expression + : arithmetic_term + | arithmetic_expression op=('+' | '-') arithmetic_term + ; + +arithmetic_term + : arithmetic_factor + | arithmetic_term op=('*' | '/') arithmetic_factor + ; + +arithmetic_factor + : op=('+' | '-')? arithmetic_primary + ; + +arithmetic_primary + : state_valued_path_expression + | numeric_literal + | '(' arithmetic_expression ')' + | input_parameter + | functions_returning_numerics + | aggregate_expression + | case_expression + | cast_function + | function_invocation + | '(' subquery ')' + ; + +string_expression + : state_valued_path_expression + | string_literal + | input_parameter + | functions_returning_strings + | aggregate_expression + | case_expression + | function_invocation + | '(' subquery ')' + ; + +datetime_expression + : state_valued_path_expression + | input_parameter + | functions_returning_datetime + | aggregate_expression + | case_expression + | function_invocation + | date_time_timestamp_literal + | '(' subquery ')' + ; + +boolean_expression + : state_valued_path_expression + | boolean_literal + | input_parameter + | case_expression + | function_invocation + | '(' subquery ')' + ; + +enum_expression + : state_valued_path_expression + | enum_literal + | input_parameter + | case_expression + | '(' subquery ')' + ; + +entity_expression + : single_valued_object_path_expression + | simple_entity_expression + ; + +simple_entity_expression + : identification_variable + | input_parameter + ; + +entity_type_expression + : type_discriminator + | entity_type_literal + | input_parameter + ; + +type_discriminator + : TYPE '(' (general_identification_variable | single_valued_object_path_expression | input_parameter) ')' + ; + +functions_returning_numerics + : LENGTH '(' string_expression ')' + | LOCATE '(' string_expression ',' string_expression (',' arithmetic_expression)? ')' + | ABS '(' arithmetic_expression ')' + | CEILING '(' arithmetic_expression ')' + | EXP '(' arithmetic_expression ')' + | FLOOR '(' arithmetic_expression ')' + | LN '(' arithmetic_expression ')' + | SIGN '(' arithmetic_expression ')' + | SQRT '(' arithmetic_expression ')' + | MOD '(' arithmetic_expression '/' arithmetic_expression ')' + | POWER '(' arithmetic_expression ',' arithmetic_expression ')' + | ROUND '(' arithmetic_expression ',' arithmetic_expression ')' + | SIZE '(' collection_valued_path_expression ')' + | INDEX '(' identification_variable ')' + | extract_datetime_field + ; + +functions_returning_datetime + : CURRENT_DATE + | CURRENT_TIME + | CURRENT_TIMESTAMP + | LOCAL DATE + | LOCAL TIME + | LOCAL DATETIME + | extract_datetime_part + ; + +functions_returning_strings + : CONCAT '(' string_expression ',' string_expression (',' string_expression)* ')' + | SUBSTRING '(' string_expression ',' arithmetic_expression (',' arithmetic_expression)? ')' + | TRIM '(' ((trim_specification)? (trim_character)? FROM)? string_expression ')' + | LOWER '(' string_expression ')' + | UPPER '(' string_expression ')' + ; + +trim_specification + : LEADING + | TRAILING + | BOTH + ; + +cast_function + : CAST '(' single_valued_path_expression identification_variable ('(' numeric_literal (',' numeric_literal)* ')')? ')' + ; + +function_invocation + : (FUNCTION|identification_variable) '(' function_name (',' function_arg)* ')' + ; + +extract_datetime_field + : EXTRACT '(' datetime_field FROM datetime_expression ')' + ; + +datetime_field + : identification_variable + ; + +extract_datetime_part + : EXTRACT '(' datetime_part FROM datetime_expression ')' + ; + +datetime_part + : identification_variable + ; + +function_arg + : literal + | state_valued_path_expression + | input_parameter + | scalar_expression + ; + +case_expression + : general_case_expression + | simple_case_expression + | coalesce_expression + | nullif_expression + ; + +general_case_expression + : CASE when_clause (when_clause)* ELSE scalar_expression END + ; + +when_clause + : WHEN conditional_expression THEN scalar_expression + ; + +simple_case_expression + : CASE case_operand simple_when_clause (simple_when_clause)* ELSE scalar_expression END + ; + +case_operand + : state_valued_path_expression + | type_discriminator + ; + +simple_when_clause + : WHEN scalar_expression THEN scalar_expression + ; + +coalesce_expression + : COALESCE '(' scalar_expression (',' scalar_expression)+ ')' + ; + +nullif_expression + : NULLIF '(' scalar_expression ',' scalar_expression ')' + ; + +/******************* + Gaps in the spec. + *******************/ + +trim_character + : CHARACTER + | character_valued_input_parameter + ; + +identification_variable + : IDENTIFICATION_VARIABLE + | f=(COUNT + | DATE + | FROM + | INNER + | KEY + | LEFT + | NEW + | ORDER + | OUTER + | POWER + | FLOOR + | SIGN + | TIME + | TYPE + | VALUE) + ; + +constructor_name + : state_field_path_expression + ; + +literal + : STRINGLITERAL + | INTLITERAL + | FLOATLITERAL + | LONGLITERAL + | boolean_literal + | entity_type_literal + ; + +input_parameter + : '?' INTLITERAL + | ':' identification_variable + ; + +pattern_value + : string_expression + ; + +date_time_timestamp_literal + : STRINGLITERAL + | DATELITERAL + | TIMELITERAL + | TIMESTAMPLITERAL + ; + +entity_type_literal + : identification_variable + ; + +escape_character + : CHARACTER + | character_valued_input_parameter // + ; + +numeric_literal + : INTLITERAL + | FLOATLITERAL + | LONGLITERAL + ; + +boolean_literal + : TRUE + | FALSE + ; + +enum_literal + : state_field_path_expression + ; + +string_literal + : CHARACTER + | STRINGLITERAL + ; + +single_valued_embeddable_object_field + : identification_variable + ; + +subtype + : identification_variable + ; + +collection_valued_field + : identification_variable + ; + +single_valued_object_field + : identification_variable + ; + +state_field + : identification_variable + ; + +collection_value_field + : identification_variable + ; + +entity_name + : identification_variable ('.' identification_variable)* // Hibernate sometimes expands the entity name to FQDN when using named queries + ; + +result_variable + : identification_variable + ; + +superquery_identification_variable + : identification_variable + ; + +collection_valued_input_parameter + : input_parameter + ; + +single_valued_input_parameter + : input_parameter + ; + +function_name + : string_literal + ; + +character_valued_input_parameter + : CHARACTER + | input_parameter + ; + +/* + Lexer rules + */ + + +WS : [ \t\r\n] -> skip ; + +// Build up case-insentive tokens + +fragment A: 'a' | 'A'; +fragment B: 'b' | 'B'; +fragment C: 'c' | 'C'; +fragment D: 'd' | 'D'; +fragment E: 'e' | 'E'; +fragment F: 'f' | 'F'; +fragment G: 'g' | 'G'; +fragment H: 'h' | 'H'; +fragment I: 'i' | 'I'; +fragment J: 'j' | 'J'; +fragment K: 'k' | 'K'; +fragment L: 'l' | 'L'; +fragment M: 'm' | 'M'; +fragment N: 'n' | 'N'; +fragment O: 'o' | 'O'; +fragment P: 'p' | 'P'; +fragment Q: 'q' | 'Q'; +fragment R: 'r' | 'R'; +fragment S: 's' | 'S'; +fragment T: 't' | 'T'; +fragment U: 'u' | 'U'; +fragment V: 'v' | 'V'; +fragment W: 'w' | 'W'; +fragment X: 'x' | 'X'; +fragment Y: 'y' | 'Y'; +fragment Z: 'z' | 'Z'; + +// The following are reserved identifiers: + +ABS : A B S; +ALL : A L L; +AND : A N D; +ANY : A N Y; +AS : A S; +ASC : A S C; +AVG : A V G; +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; +COUNT : C O U N T; +CURRENT_DATE : C U R R E N T '_' D A T E; +CURRENT_TIME : C U R R E N T '_' T I M E; +CURRENT_TIMESTAMP : C U R R E N T '_' T I M E S T A M P; +DATE : D A T E; +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; +END : E N D; +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; +FALSE : F A L S E; +FETCH : F E T C H; +FIRST : F I R S T; +FLOOR : F L O O R; +FROM : F R O M; +FUNCTION : F U N C T I O N; +GROUP : G R O U P; +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; +LAST : L A S T; +LEADING : L E A D I N G; +LEFT : L E F T; +LENGTH : L E N G T H; +LIKE : L I K E; +LN : L N; +LOCAL : L O C A L; +LOCATE : L O C A T E; +LOWER : L O W E R; +MAX : M A X; +MEMBER : M E M B E R; +MIN : M I N; +MOD : M O D; +NEW : N E W; +NOT : N O T; +NULL : N U L L; +NULLIF : N U L L I F; +NULLS : N U L L S; +OBJECT : O B J E C T; +OF : O F; +ON : O N; +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; +SIGN : S I G N; +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; +SUM : S U M; +THEN : T H E N; +TIME : T I M E; +TRAILING : T R A I L I N G; +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; +WHEN : W H E N; +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 : '\'' (~ ('\'' | '\\')|'\\')* '\'' ; +FLOATLITERAL : ('0' .. '9')* '.' ('0' .. '9')+ (E ('0' .. '9')+)* (F|D)?; +INTLITERAL : ('0' .. '9')+ ; +LONGLITERAL : ('0' .. '9')+ L; +DATELITERAL : '{' D STRINGLITERAL '}'; +TIMELITERAL : '{' T STRINGLITERAL '}'; +TIMESTAMPLITERAL : '{' T S STRINGLITERAL '}'; \ No newline at end of file diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java index 076f447c3..379e30b55 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java @@ -55,7 +55,7 @@ import org.springframework.util.ConcurrentReferenceHashMap; * @author Greg Turnquist * @author Yuriy Tsarkov */ -public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, QueryComment { +public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, QueryComment, PresenceDetector { /** * Hibernate persistence provider. @@ -110,6 +110,7 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer public String getCommentHintKey() { return "org.hibernate.comment"; } + }, /** @@ -203,6 +204,8 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer private final Iterable entityManagerClassNames; private final Iterable metamodelClassNames; + private boolean present; + /** * Creates a new {@link PersistenceProvider}. * @@ -214,6 +217,13 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer this.entityManagerClassNames = entityManagerClassNames; this.metamodelClassNames = metamodelClassNames; + + this.present = false; + entityManagerClassNames.forEach(entityManagerClassName -> { + if (ClassUtils.isPresent(entityManagerClassName, PersistenceProvider.class.getClassLoader())) { + this.present = true; + } + }); } /** @@ -330,6 +340,11 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer : value; } + @Override + public boolean isPresent() { + return this.present; + } + /** * Holds the PersistenceProvider specific interface names. * diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PresenceDetector.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PresenceDetector.java new file mode 100644 index 000000000..e6bd19e70 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PresenceDetector.java @@ -0,0 +1,27 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + + */ +package org.springframework.data.jpa.provider; + +/** + * Define operations needed to detect the presence of a given JPA provider. + * + * @author Greg Turnquist + * @since 3.2 + */ +interface PresenceDetector { + boolean isPresent(); +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryParser.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryParser.java new file mode 100644 index 000000000..25e832618 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryParser.java @@ -0,0 +1,132 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import java.util.List; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.ParserRuleContext; +import org.springframework.data.domain.Sort; +import org.springframework.lang.Nullable; + +/** + * Implements the {@code EQL} parsing operations of a {@link JpaQueryParserSupport} using the ANTLR-generated + * {@link EqlParser} and {@link EqlQueryTransformer}. + * + * @author Greg Turnquist + * @since 3.2 + */ +class EqlQueryParser extends JpaQueryParserSupport { + + EqlQueryParser(String query) { + super(query); + } + + /** + * Convenience method to parse a EQL query. Will throw a {@link BadJpqlGrammarException} if the query is invalid. + * + * @param query + * @return a parsed query, ready for postprocessing + */ + public static ParserRuleContext parseQuery(String query) { + + EqlLexer lexer = new EqlLexer(CharStreams.fromString(query)); + EqlParser parser = new EqlParser(new CommonTokenStream(lexer)); + + configureParser(query, lexer, parser); + + return parser.start(); + } + + /** + * Parse the query using {@link #parseQuery(String)}. + * + * @return a parsed query + */ + @Override + protected ParserRuleContext parse(String query) { + return parseQuery(query); + } + + /** + * Use the {@link EqlQueryTransformer} to transform the original query into a query with the {@link Sort} applied. + * + * @param parsedQuery + * @param sort can be {@literal null} + * @return list of {@link JpaQueryParsingToken}s + */ + @Override + protected List applySort(ParserRuleContext parsedQuery, Sort sort) { + return new EqlQueryTransformer(sort).visit(parsedQuery); + } + + /** + * Use the {@link EqlQueryTransformer} to transform the original query into a count query. + * + * @param parsedQuery + * @param countProjection + * @return list of {@link JpaQueryParsingToken}s + */ + @Override + protected List doCreateCountQuery(ParserRuleContext parsedQuery, + @Nullable String countProjection) { + return new EqlQueryTransformer(true, countProjection).visit(parsedQuery); + } + + /** + * Run the parsed query through {@link EqlQueryTransformer} to find the primary FROM clause's alias. + * + * @param parsedQuery + * @return can be {@literal null} + */ + @Override + protected String doFindAlias(ParserRuleContext parsedQuery) { + + EqlQueryTransformer transformVisitor = new EqlQueryTransformer(); + transformVisitor.visit(parsedQuery); + return transformVisitor.getAlias(); + } + + /** + * Use {@link EqlQueryTransformer} to find the projection of the query. + * + * @param parsedQuery + * @return + */ + @Override + protected List doFindProjection(ParserRuleContext parsedQuery) { + + EqlQueryTransformer transformVisitor = new EqlQueryTransformer(); + transformVisitor.visit(parsedQuery); + return transformVisitor.getProjection(); + } + + /** + * Use {@link EqlQueryTransformer} to detect if the query uses a {@code new com.example.Dto()} DTO constructor in the + * primary select clause. + * + * @param parsedQuery + * @return Guaranteed to be {@literal true} or {@literal false}. + */ + @Override + protected boolean doCheckForConstructor(ParserRuleContext parsedQuery) { + + EqlQueryTransformer transformVisitor = new EqlQueryTransformer(); + transformVisitor.visit(parsedQuery); + return transformVisitor.hasConstructorExpression(); + } +} 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 new file mode 100644 index 000000000..91ede2389 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java @@ -0,0 +1,2540 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import static org.springframework.data.jpa.repository.query.JpaQueryParsingToken.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders an EQL query without making any changes. + * + * @author Greg Turnquist + * @since 3.2 + */ +class EqlQueryRenderer extends EqlBaseVisitor> { + + @Override + public List visitStart(EqlParser.StartContext ctx) { + return visit(ctx.ql_statement()); + } + + @Override + public List visitQl_statement(EqlParser.Ql_statementContext ctx) { + + if (ctx.select_statement() != null) { + return visit(ctx.select_statement()); + } else if (ctx.update_statement() != null) { + return visit(ctx.update_statement()); + } else if (ctx.delete_statement() != null) { + return visit(ctx.delete_statement()); + } else { + return List.of(); + } + } + + @Override + public List visitSelect_statement(EqlParser.Select_statementContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.select_clause())); + tokens.addAll(visit(ctx.from_clause())); + + if (ctx.where_clause() != null) { + tokens.addAll(visit(ctx.where_clause())); + } + + if (ctx.groupby_clause() != null) { + tokens.addAll(visit(ctx.groupby_clause())); + } + + if (ctx.having_clause() != null) { + tokens.addAll(visit(ctx.having_clause())); + } + + if (ctx.orderby_clause() != null) { + tokens.addAll(visit(ctx.orderby_clause())); + } + + for (int i = 0; i < ctx.setOperator().size(); i++) { + + tokens.addAll(visit(ctx.setOperator(i))); + tokens.addAll(visit(ctx.select_statement(i))); + } + + return tokens; + } + + @Override + public List visitSetOperator(EqlParser.SetOperatorContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.UNION() != null) { + tokens.add(new JpaQueryParsingToken(ctx.UNION())); + } else if (ctx.INTERSECT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.INTERSECT())); + } else if (ctx.EXCEPT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.EXCEPT())); + } + + if (ctx.ALL() != null) { + tokens.add(new JpaQueryParsingToken(ctx.ALL())); + } + + return tokens; + } + + @Override + public List visitUpdate_statement(EqlParser.Update_statementContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.update_clause())); + + if (ctx.where_clause() != null) { + tokens.addAll(visit(ctx.where_clause())); + } + + return tokens; + } + + @Override + public List visitDelete_statement(EqlParser.Delete_statementContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.delete_clause())); + + if (ctx.where_clause() != null) { + tokens.addAll(visit(ctx.where_clause())); + } + + return tokens; + } + + @Override + public List visitFrom_clause(EqlParser.From_clauseContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.FROM(), true)); + tokens.addAll(visit(ctx.identification_variable_declaration())); + + ctx.identificationVariableDeclarationOrCollectionMemberDeclaration() + .forEach(identificationVariableDeclarationOrCollectionMemberDeclarationContext -> { + NOSPACE(tokens); + tokens.add(TOKEN_COMMA); + tokens.addAll(visit(identificationVariableDeclarationOrCollectionMemberDeclarationContext)); + }); + SPACE(tokens); + + return tokens; + } + + @Override + public List visitIdentificationVariableDeclarationOrCollectionMemberDeclaration( + EqlParser.IdentificationVariableDeclarationOrCollectionMemberDeclarationContext ctx) { + + if (ctx.identification_variable_declaration() != null) { + return visit(ctx.identification_variable_declaration()); + } else if (ctx.collection_member_declaration() != null) { + return visit(ctx.collection_member_declaration()); + } else if (ctx.subquery() != null) { + + List tokens = new ArrayList<>(); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.subquery())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + tokens.addAll(visit(ctx.identification_variable())); + + return tokens; + } else { + return List.of(); + } + } + + @Override + public List visitIdentification_variable_declaration( + EqlParser.Identification_variable_declarationContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.range_variable_declaration())); + + ctx.join().forEach(joinContext -> { + tokens.addAll(visit(joinContext)); + }); + ctx.fetch_join().forEach(fetchJoinContext -> { + tokens.addAll(visit(fetchJoinContext)); + }); + + return tokens; + } + + @Override + public List visitRange_variable_declaration(EqlParser.Range_variable_declarationContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.entity_name() != null) { + tokens.addAll(visit(ctx.entity_name())); + } else if (ctx.function_invocation() != null) { + tokens.addAll(visit(ctx.function_invocation())); + } + + if (ctx.AS() != null) { + tokens.add(new JpaQueryParsingToken(ctx.AS())); + } + + tokens.addAll(visit(ctx.identification_variable())); + + return tokens; + } + + @Override + public List visitJoin(EqlParser.JoinContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.join_spec())); + tokens.addAll(visit(ctx.join_association_path_expression())); + if (ctx.AS() != null) { + tokens.add(new JpaQueryParsingToken(ctx.AS())); + } + if (ctx.identification_variable() != null) { + tokens.addAll(visit(ctx.identification_variable())); + } + if (ctx.join_condition() != null) { + tokens.addAll(visit(ctx.join_condition())); + } + + return tokens; + } + + @Override + public List visitFetch_join(EqlParser.Fetch_joinContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.join_spec())); + tokens.add(new JpaQueryParsingToken(ctx.FETCH())); + tokens.addAll(visit(ctx.join_association_path_expression())); + + if (ctx.AS() != null) { + tokens.add(new JpaQueryParsingToken(ctx.AS())); + } + if (ctx.identification_variable() != null) { + tokens.addAll(visit(ctx.identification_variable())); + } + if (ctx.join_condition() != null) { + tokens.addAll(visit(ctx.join_condition())); + } + + return tokens; + } + + @Override + public List visitJoin_spec(EqlParser.Join_specContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.LEFT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.LEFT())); + } + if (ctx.OUTER() != null) { + tokens.add(new JpaQueryParsingToken(ctx.OUTER())); + } + if (ctx.INNER() != null) { + tokens.add(new JpaQueryParsingToken(ctx.INNER())); + } + if (ctx.JOIN() != null) { + tokens.add(new JpaQueryParsingToken(ctx.JOIN())); + } + + return tokens; + } + + @Override + public List visitJoin_condition(EqlParser.Join_conditionContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.ON())); + tokens.addAll(visit(ctx.conditional_expression())); + + return tokens; + } + + @Override + public List visitJoin_association_path_expression( + EqlParser.Join_association_path_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.TREAT() == null) { + + if (ctx.join_collection_valued_path_expression() != null) { + tokens.addAll(visit(ctx.join_collection_valued_path_expression())); + } else if (ctx.join_single_valued_path_expression() != null) { + tokens.addAll(visit(ctx.join_single_valued_path_expression())); + } + } else { + if (ctx.join_collection_valued_path_expression() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.TREAT(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.join_collection_valued_path_expression())); + tokens.add(new JpaQueryParsingToken(ctx.AS())); + tokens.addAll(visit(ctx.subtype())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.join_single_valued_path_expression() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.TREAT(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.join_single_valued_path_expression())); + tokens.add(new JpaQueryParsingToken(ctx.AS())); + tokens.addAll(visit(ctx.subtype())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } + } + + return tokens; + } + + @Override + public List visitJoin_collection_valued_path_expression( + EqlParser.Join_collection_valued_path_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.identification_variable() != null) { + + tokens.addAll(visit(ctx.identification_variable())); + NOSPACE(tokens); + tokens.add(TOKEN_DOT); + } + + ctx.single_valued_embeddable_object_field().forEach(singleValuedEmbeddableObjectFieldContext -> { + tokens.addAll(visit(singleValuedEmbeddableObjectFieldContext)); + NOSPACE(tokens); + tokens.add(TOKEN_DOT); + }); + + tokens.addAll(visit(ctx.collection_valued_field())); + + return tokens; + } + + @Override + public List visitJoin_single_valued_path_expression( + EqlParser.Join_single_valued_path_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.identification_variable() != null) { + + tokens.addAll(visit(ctx.identification_variable())); + tokens.add(TOKEN_DOT); + } + + ctx.single_valued_embeddable_object_field().forEach(singleValuedEmbeddableObjectFieldContext -> { + tokens.addAll(visit(singleValuedEmbeddableObjectFieldContext)); + tokens.add(TOKEN_DOT); + }); + + tokens.addAll(visit(ctx.single_valued_object_field())); + + return tokens; + } + + @Override + public List visitCollection_member_declaration( + EqlParser.Collection_member_declarationContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.IN(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.collection_valued_path_expression())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + + if (ctx.AS() != null) { + tokens.add(new JpaQueryParsingToken(ctx.AS())); + } + + tokens.addAll(visit(ctx.identification_variable())); + + return tokens; + } + + @Override + public List visitQualified_identification_variable( + EqlParser.Qualified_identification_variableContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.map_field_identification_variable() != null) { + tokens.addAll(visit(ctx.map_field_identification_variable())); + } else if (ctx.identification_variable() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.ENTRY())); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.identification_variable())); + tokens.add(TOKEN_CLOSE_PAREN); + } + + return tokens; + } + + @Override + public List visitMap_field_identification_variable( + EqlParser.Map_field_identification_variableContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.KEY() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.KEY(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.identification_variable())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.VALUE() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.VALUE(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.identification_variable())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } + + return tokens; + } + + @Override + public List visitSingle_valued_path_expression( + EqlParser.Single_valued_path_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.qualified_identification_variable() != null) { + tokens.addAll(visit(ctx.qualified_identification_variable())); + } else if (ctx.qualified_identification_variable() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.TREAT(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.qualified_identification_variable())); + tokens.add(new JpaQueryParsingToken(ctx.AS())); + tokens.addAll(visit(ctx.subtype())); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.state_field_path_expression() != null) { + tokens.addAll(visit(ctx.state_field_path_expression())); + } else if (ctx.single_valued_object_path_expression() != null) { + tokens.addAll(visit(ctx.single_valued_object_path_expression())); + } + + return tokens; + } + + @Override + public List visitGeneral_identification_variable( + EqlParser.General_identification_variableContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.identification_variable() != null) { + tokens.addAll(visit(ctx.identification_variable())); + } else if (ctx.map_field_identification_variable() != null) { + tokens.addAll(visit(ctx.map_field_identification_variable())); + } + + return tokens; + } + + @Override + public List visitGeneral_subpath(EqlParser.General_subpathContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.simple_subpath() != null) { + tokens.addAll(visit(ctx.simple_subpath())); + } else if (ctx.treated_subpath() != null) { + + tokens.addAll(visit(ctx.treated_subpath())); + + ctx.single_valued_object_field().forEach(singleValuedObjectFieldContext -> { + tokens.add(TOKEN_DOT); + tokens.addAll(visit(singleValuedObjectFieldContext)); + }); + } + + return tokens; + } + + @Override + public List visitSimple_subpath(EqlParser.Simple_subpathContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.general_identification_variable())); + NOSPACE(tokens); + + ctx.single_valued_object_field().forEach(singleValuedObjectFieldContext -> { + tokens.add(TOKEN_DOT); + tokens.addAll(visit(singleValuedObjectFieldContext)); + NOSPACE(tokens); + }); + SPACE(tokens); + + return tokens; + } + + @Override + public List visitTreated_subpath(EqlParser.Treated_subpathContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.TREAT(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.general_subpath())); + SPACE(tokens); + tokens.add(new JpaQueryParsingToken(ctx.AS())); + tokens.addAll(visit(ctx.subtype())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + + return tokens; + } + + @Override + public List visitState_field_path_expression(EqlParser.State_field_path_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.general_subpath())); + NOSPACE(tokens); + tokens.add(TOKEN_DOT); + tokens.addAll(visit(ctx.state_field())); + + return tokens; + } + + @Override + public List visitState_valued_path_expression( + EqlParser.State_valued_path_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.state_field_path_expression() != null) { + tokens.addAll(visit(ctx.state_field_path_expression())); + } else if (ctx.general_identification_variable() != null) { + tokens.addAll(visit(ctx.general_identification_variable())); + } + + return tokens; + } + + @Override + public List visitSingle_valued_object_path_expression( + EqlParser.Single_valued_object_path_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.general_subpath())); + NOSPACE(tokens); + tokens.add(TOKEN_DOT); + tokens.addAll(visit(ctx.single_valued_object_field())); + + return tokens; + } + + @Override + public List visitCollection_valued_path_expression( + EqlParser.Collection_valued_path_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.general_subpath())); + NOSPACE(tokens); + tokens.add(TOKEN_DOT); + tokens.addAll(visit(ctx.collection_value_field())); + + return tokens; + } + + @Override + public List visitUpdate_clause(EqlParser.Update_clauseContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.UPDATE())); + tokens.addAll(visit(ctx.entity_name())); + + if (ctx.AS() != null) { + tokens.add(new JpaQueryParsingToken(ctx.AS())); + } + if (ctx.identification_variable() != null) { + tokens.addAll(visit(ctx.identification_variable())); + } + + tokens.add(new JpaQueryParsingToken(ctx.SET())); + + ctx.update_item().forEach(updateItemContext -> { + tokens.addAll(visit(updateItemContext)); + NOSPACE(tokens); + tokens.add(TOKEN_COMMA); + }); + CLIP(tokens); + SPACE(tokens); + + return tokens; + } + + @Override + public List visitUpdate_item(EqlParser.Update_itemContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.identification_variable() != null) { + tokens.addAll(visit(ctx.identification_variable())); + NOSPACE(tokens); + tokens.add(TOKEN_DOT); + } + + ctx.single_valued_embeddable_object_field().forEach(singleValuedEmbeddableObjectFieldContext -> { + tokens.addAll(visit(singleValuedEmbeddableObjectFieldContext)); + NOSPACE(tokens); + tokens.add(TOKEN_DOT); + }); + + if (ctx.state_field() != null) { + tokens.addAll(visit(ctx.state_field())); + } else if (ctx.single_valued_object_field() != null) { + tokens.addAll(visit(ctx.single_valued_object_field())); + } + + tokens.add(TOKEN_EQUALS); + tokens.addAll(visit(ctx.new_value())); + + return tokens; + } + + @Override + public List visitNew_value(EqlParser.New_valueContext ctx) { + + if (ctx.scalar_expression() != null) { + return visit(ctx.scalar_expression()); + } else if (ctx.simple_entity_expression() != null) { + return visit(ctx.simple_entity_expression()); + } else if (ctx.NULL() != null) { + return List.of(new JpaQueryParsingToken(ctx.NULL())); + } else { + return List.of(); + } + } + + @Override + public List visitDelete_clause(EqlParser.Delete_clauseContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.DELETE())); + tokens.add(new JpaQueryParsingToken(ctx.FROM())); + tokens.addAll(visit(ctx.entity_name())); + if (ctx.AS() != null) { + tokens.add(new JpaQueryParsingToken(ctx.AS())); + } + if (ctx.identification_variable() != null) { + tokens.addAll(visit(ctx.identification_variable())); + } + + return tokens; + } + + @Override + public List visitSelect_clause(EqlParser.Select_clauseContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.SELECT())); + + if (ctx.DISTINCT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.DISTINCT())); + } + + ctx.select_item().forEach(selectItemContext -> { + tokens.addAll(visit(selectItemContext)); + NOSPACE(tokens); + tokens.add(TOKEN_COMMA); + }); + CLIP(tokens); + SPACE(tokens); + + return tokens; + } + + @Override + public List visitSelect_item(EqlParser.Select_itemContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.select_expression())); + SPACE(tokens); + + if (ctx.AS() != null) { + tokens.add(new JpaQueryParsingToken(ctx.AS())); + } + + if (ctx.result_variable() != null) { + tokens.addAll(visit(ctx.result_variable())); + } + + return tokens; + } + + @Override + public List visitSelect_expression(EqlParser.Select_expressionContext ctx) { + + if (ctx.single_valued_path_expression() != null) { + return visit(ctx.single_valued_path_expression()); + } else if (ctx.scalar_expression() != null) { + return visit(ctx.scalar_expression()); + } else if (ctx.aggregate_expression() != null) { + return visit(ctx.aggregate_expression()); + } else if (ctx.identification_variable() != null) { + + if (ctx.OBJECT() == null) { + return visit(ctx.identification_variable()); + } else { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.OBJECT(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.identification_variable())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + + return tokens; + } + } else if (ctx.constructor_expression() != null) { + return visit(ctx.constructor_expression()); + } else { + return List.of(); + } + } + + @Override + public List visitConstructor_expression(EqlParser.Constructor_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.NEW())); + tokens.addAll(visit(ctx.constructor_name())); + tokens.add(TOKEN_OPEN_PAREN); + + ctx.constructor_item().forEach(constructorItemContext -> { + tokens.addAll(visit(constructorItemContext)); + NOSPACE(tokens); + tokens.add(TOKEN_COMMA); + }); + CLIP(tokens); + + tokens.add(TOKEN_CLOSE_PAREN); + + return tokens; + } + + @Override + public List visitConstructor_item(EqlParser.Constructor_itemContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.single_valued_path_expression() != null) { + tokens.addAll(visit(ctx.single_valued_path_expression())); + } else if (ctx.scalar_expression() != null) { + tokens.addAll(visit(ctx.scalar_expression())); + } else if (ctx.aggregate_expression() != null) { + tokens.addAll(visit(ctx.aggregate_expression())); + } else if (ctx.identification_variable() != null) { + tokens.addAll(visit(ctx.identification_variable())); + } + + return tokens; + } + + @Override + public List visitAggregate_expression(EqlParser.Aggregate_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.AVG() != null || ctx.MAX() != null || ctx.MIN() != null || ctx.SUM() != null) { + + if (ctx.AVG() != null) { + tokens.add(new JpaQueryParsingToken(ctx.AVG(), false)); + } + if (ctx.MAX() != null) { + tokens.add(new JpaQueryParsingToken(ctx.MAX(), false)); + } + if (ctx.MIN() != null) { + tokens.add(new JpaQueryParsingToken(ctx.MIN(), false)); + } + if (ctx.SUM() != null) { + tokens.add(new JpaQueryParsingToken(ctx.SUM(), false)); + } + + tokens.add(TOKEN_OPEN_PAREN); + + if (ctx.DISTINCT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.DISTINCT())); + } + + tokens.addAll(visit(ctx.state_valued_path_expression())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.COUNT() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.COUNT(), false)); + tokens.add(TOKEN_OPEN_PAREN); + if (ctx.DISTINCT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.DISTINCT())); + } + if (ctx.identification_variable() != null) { + tokens.addAll(visit(ctx.identification_variable())); + } else if (ctx.state_valued_path_expression() != null) { + tokens.addAll(visit(ctx.state_valued_path_expression())); + } else if (ctx.single_valued_object_path_expression() != null) { + tokens.addAll(visit(ctx.single_valued_object_path_expression())); + } + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.function_invocation() != null) { + tokens.addAll(visit(ctx.function_invocation())); + } + + return tokens; + } + + @Override + public List visitWhere_clause(EqlParser.Where_clauseContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.WHERE(), true)); + tokens.addAll(visit(ctx.conditional_expression())); + + return tokens; + } + + @Override + public List visitGroupby_clause(EqlParser.Groupby_clauseContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.GROUP())); + tokens.add(new JpaQueryParsingToken(ctx.BY())); + ctx.groupby_item().forEach(groupbyItemContext -> { + tokens.addAll(visit(groupbyItemContext)); + NOSPACE(tokens); + tokens.add(TOKEN_COMMA); + }); + CLIP(tokens); + SPACE(tokens); + + return tokens; + } + + @Override + public List visitGroupby_item(EqlParser.Groupby_itemContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.single_valued_path_expression() != null) { + tokens.addAll(visit(ctx.single_valued_path_expression())); + } else if (ctx.identification_variable() != null) { + tokens.addAll(visit(ctx.identification_variable())); + } else if (ctx.scalar_expression() != null) { + tokens.addAll(visit(ctx.scalar_expression())); + } + + return tokens; + } + + @Override + public List visitHaving_clause(EqlParser.Having_clauseContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.HAVING())); + tokens.addAll(visit(ctx.conditional_expression())); + + return tokens; + } + + @Override + public List visitOrderby_clause(EqlParser.Orderby_clauseContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.ORDER())); + tokens.add(new JpaQueryParsingToken(ctx.BY())); + + ctx.orderby_item().forEach(orderbyItemContext -> { + tokens.addAll(visit(orderbyItemContext)); + NOSPACE(tokens); + tokens.add(TOKEN_COMMA); + }); + CLIP(tokens); + + return tokens; + } + + @Override + public List visitOrderby_item(EqlParser.Orderby_itemContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.state_field_path_expression() != null) { + tokens.addAll(visit(ctx.state_field_path_expression())); + } else if (ctx.general_identification_variable() != null) { + tokens.addAll(visit(ctx.general_identification_variable())); + } else if (ctx.result_variable() != null) { + tokens.addAll(visit(ctx.result_variable())); + } else if (ctx.string_expression() != null) { + tokens.addAll(visit(ctx.string_expression())); + } else if (ctx.scalar_expression() != null) { + tokens.addAll(visit(ctx.scalar_expression())); + } + + if (ctx.ASC() != null) { + tokens.add(new JpaQueryParsingToken(ctx.ASC())); + } + if (ctx.DESC() != null) { + tokens.add(new JpaQueryParsingToken(ctx.DESC())); + } + + if (ctx.nullsPrecedence() != null) { + tokens.addAll(visit(ctx.nullsPrecedence())); + } + + return tokens; + } + + @Override + public List visitNullsPrecedence(EqlParser.NullsPrecedenceContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(TOKEN_NULLS); + + if (ctx.FIRST() != null) { + tokens.add(TOKEN_FIRST); + } else if (ctx.LAST() != null) { + tokens.add(TOKEN_LAST); + } + + return tokens; + } + + @Override + public List visitSubquery(EqlParser.SubqueryContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.simple_select_clause())); + tokens.addAll(visit(ctx.subquery_from_clause())); + if (ctx.where_clause() != null) { + tokens.addAll(visit(ctx.where_clause())); + } + if (ctx.groupby_clause() != null) { + tokens.addAll(visit(ctx.groupby_clause())); + } + if (ctx.having_clause() != null) { + tokens.addAll(visit(ctx.having_clause())); + } + + return tokens; + } + + @Override + public List visitSubquery_from_clause(EqlParser.Subquery_from_clauseContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.FROM())); + ctx.subselect_identification_variable_declaration().forEach(subselectIdentificationVariableDeclarationContext -> { + tokens.addAll(visit(subselectIdentificationVariableDeclarationContext)); + NOSPACE(tokens); + tokens.add(TOKEN_COMMA); + }); + CLIP(tokens); + SPACE(tokens); + + return tokens; + } + + @Override + public List visitSubselect_identification_variable_declaration( + EqlParser.Subselect_identification_variable_declarationContext ctx) { + return super.visitSubselect_identification_variable_declaration(ctx); + } + + @Override + public List visitDerived_path_expression(EqlParser.Derived_path_expressionContext ctx) { + return super.visitDerived_path_expression(ctx); + } + + @Override + public List visitGeneral_derived_path(EqlParser.General_derived_pathContext ctx) { + return super.visitGeneral_derived_path(ctx); + } + + @Override + public List visitSimple_derived_path(EqlParser.Simple_derived_pathContext ctx) { + return super.visitSimple_derived_path(ctx); + } + + @Override + public List visitTreated_derived_path(EqlParser.Treated_derived_pathContext ctx) { + return super.visitTreated_derived_path(ctx); + } + + @Override + public List visitDerived_collection_member_declaration( + EqlParser.Derived_collection_member_declarationContext ctx) { + return super.visitDerived_collection_member_declaration(ctx); + } + + @Override + public List visitSimple_select_clause(EqlParser.Simple_select_clauseContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.SELECT())); + if (ctx.DISTINCT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.DISTINCT())); + } + tokens.addAll(visit(ctx.simple_select_expression())); + + return tokens; + } + + @Override + public List visitSimple_select_expression(EqlParser.Simple_select_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.single_valued_path_expression() != null) { + tokens.addAll(visit(ctx.single_valued_path_expression())); + } else if (ctx.scalar_expression() != null) { + tokens.addAll(visit(ctx.scalar_expression())); + } else if (ctx.aggregate_expression() != null) { + tokens.addAll(visit(ctx.aggregate_expression())); + } else if (ctx.identification_variable() != null) { + tokens.addAll(visit(ctx.identification_variable())); + } + + return tokens; + } + + @Override + public List visitScalar_expression(EqlParser.Scalar_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.arithmetic_expression() != null) { + tokens.addAll(visit(ctx.arithmetic_expression())); + } else if (ctx.string_expression() != null) { + tokens.addAll(visit(ctx.string_expression())); + } else if (ctx.enum_expression() != null) { + tokens.addAll(visit(ctx.enum_expression())); + } else if (ctx.datetime_expression() != null) { + tokens.addAll(visit(ctx.datetime_expression())); + } else if (ctx.boolean_expression() != null) { + tokens.addAll(visit(ctx.boolean_expression())); + } else if (ctx.case_expression() != null) { + tokens.addAll(visit(ctx.case_expression())); + } else if (ctx.entity_type_expression() != null) { + tokens.addAll(visit(ctx.entity_type_expression())); + } + + return tokens; + } + + @Override + public List visitConditional_expression(EqlParser.Conditional_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.conditional_expression() != null) { + tokens.addAll(visit(ctx.conditional_expression())); + tokens.add(new JpaQueryParsingToken(ctx.OR())); + tokens.addAll(visit(ctx.conditional_term())); + } else { + tokens.addAll(visit(ctx.conditional_term())); + } + + return tokens; + } + + @Override + public List visitConditional_term(EqlParser.Conditional_termContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.conditional_term() != null) { + tokens.addAll(visit(ctx.conditional_term())); + tokens.add(new JpaQueryParsingToken(ctx.AND())); + tokens.addAll(visit(ctx.conditional_factor())); + } else { + tokens.addAll(visit(ctx.conditional_factor())); + } + + return tokens; + } + + @Override + public List visitConditional_factor(EqlParser.Conditional_factorContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.NOT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.NOT())); + } + + EqlParser.Conditional_primaryContext conditionalPrimary = ctx.conditional_primary(); + List visitedConditionalPrimary = visit(conditionalPrimary); + tokens.addAll(visitedConditionalPrimary); + + return tokens; + } + + @Override + public List visitConditional_primary(EqlParser.Conditional_primaryContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.simple_cond_expression() != null) { + tokens.addAll(visit(ctx.simple_cond_expression())); + } else if (ctx.conditional_expression() != null) { + + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.conditional_expression())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } + + return tokens; + } + + @Override + public List visitSimple_cond_expression(EqlParser.Simple_cond_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.comparison_expression() != null) { + tokens.addAll(visit(ctx.comparison_expression())); + } else if (ctx.between_expression() != null) { + tokens.addAll(visit(ctx.between_expression())); + } else if (ctx.in_expression() != null) { + tokens.addAll(visit(ctx.in_expression())); + } else if (ctx.like_expression() != null) { + tokens.addAll(visit(ctx.like_expression())); + } else if (ctx.null_comparison_expression() != null) { + tokens.addAll(visit(ctx.null_comparison_expression())); + } else if (ctx.empty_collection_comparison_expression() != null) { + tokens.addAll(visit(ctx.empty_collection_comparison_expression())); + } else if (ctx.collection_member_expression() != null) { + tokens.addAll(visit(ctx.collection_member_expression())); + } else if (ctx.exists_expression() != null) { + tokens.addAll(visit(ctx.exists_expression())); + } + + return tokens; + } + + @Override + public List visitBetween_expression(EqlParser.Between_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.arithmetic_expression(0) != null) { + + tokens.addAll(visit(ctx.arithmetic_expression(0))); + + if (ctx.NOT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.NOT())); + } + + tokens.add(new JpaQueryParsingToken(ctx.BETWEEN())); + tokens.addAll(visit(ctx.arithmetic_expression(1))); + tokens.add(new JpaQueryParsingToken(ctx.AND())); + tokens.addAll(visit(ctx.arithmetic_expression(2))); + + } else if (ctx.string_expression(0) != null) { + + tokens.addAll(visit(ctx.string_expression(0))); + + if (ctx.NOT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.NOT())); + } + + tokens.add(new JpaQueryParsingToken(ctx.BETWEEN())); + tokens.addAll(visit(ctx.string_expression(1))); + tokens.add(new JpaQueryParsingToken(ctx.AND())); + tokens.addAll(visit(ctx.string_expression(2))); + + } else if (ctx.datetime_expression(0) != null) { + + tokens.addAll(visit(ctx.datetime_expression(0))); + + if (ctx.NOT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.NOT())); + } + + tokens.add(new JpaQueryParsingToken(ctx.BETWEEN())); + tokens.addAll(visit(ctx.datetime_expression(1))); + tokens.add(new JpaQueryParsingToken(ctx.AND())); + tokens.addAll(visit(ctx.datetime_expression(2))); + } + + return tokens; + } + + @Override + public List visitIn_expression(EqlParser.In_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.state_valued_path_expression() != null) { + tokens.addAll(visit(ctx.state_valued_path_expression())); + } + if (ctx.type_discriminator() != null) { + tokens.addAll(visit(ctx.type_discriminator())); + } + if (ctx.NOT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.NOT())); + } + if (ctx.IN() != null) { + tokens.add(new JpaQueryParsingToken(ctx.IN())); + } + + if (ctx.in_item() != null && !ctx.in_item().isEmpty()) { + + tokens.add(TOKEN_OPEN_PAREN); + + ctx.in_item().forEach(inItemContext -> { + + tokens.addAll(visit(inItemContext)); + NOSPACE(tokens); + tokens.add(TOKEN_COMMA); + }); + CLIP(tokens); + + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.subquery() != null) { + + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.subquery())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.collection_valued_input_parameter() != null) { + tokens.addAll(visit(ctx.collection_valued_input_parameter())); + } + + return tokens; + } + + @Override + public List visitIn_item(EqlParser.In_itemContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.literal() != null) { + tokens.addAll(visit(ctx.literal())); + } else if (ctx.single_valued_input_parameter() != null) { + tokens.addAll(visit(ctx.single_valued_input_parameter())); + } + + return tokens; + } + + @Override + public List visitLike_expression(EqlParser.Like_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.string_expression())); + if (ctx.NOT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.NOT())); + } + tokens.add(new JpaQueryParsingToken(ctx.LIKE())); + tokens.addAll(visit(ctx.pattern_value())); + + if (ctx.ESCAPE() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.ESCAPE())); + tokens.addAll(visit(ctx.escape_character())); + } + + return tokens; + } + + @Override + public List visitNull_comparison_expression(EqlParser.Null_comparison_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.single_valued_path_expression() != null) { + tokens.addAll(visit(ctx.single_valued_path_expression())); + } else if (ctx.input_parameter() != null) { + tokens.addAll(visit(ctx.input_parameter())); + } else if (ctx.nullif_expression() != null) { + tokens.addAll(visit(ctx.nullif_expression())); + } + + tokens.add(new JpaQueryParsingToken(ctx.IS())); + + if (ctx.NOT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.NOT())); + } + tokens.add(new JpaQueryParsingToken(ctx.NULL())); + + return tokens; + } + + @Override + public List visitEmpty_collection_comparison_expression( + EqlParser.Empty_collection_comparison_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.collection_valued_path_expression())); + tokens.add(new JpaQueryParsingToken(ctx.IS())); + if (ctx.NOT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.NOT())); + } + tokens.add(new JpaQueryParsingToken(ctx.EMPTY())); + + return tokens; + } + + @Override + public List visitCollection_member_expression( + EqlParser.Collection_member_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.entity_or_value_expression())); + if (ctx.NOT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.NOT())); + } + tokens.add(new JpaQueryParsingToken(ctx.MEMBER())); + if (ctx.OF() != null) { + tokens.add(new JpaQueryParsingToken(ctx.OF())); + } + tokens.addAll(visit(ctx.collection_valued_path_expression())); + + return tokens; + } + + @Override + public List visitEntity_or_value_expression(EqlParser.Entity_or_value_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.single_valued_object_path_expression() != null) { + tokens.addAll(visit(ctx.single_valued_object_path_expression())); + } else if (ctx.state_field_path_expression() != null) { + tokens.addAll(visit(ctx.state_field_path_expression())); + } else if (ctx.simple_entity_or_value_expression() != null) { + tokens.addAll(visit(ctx.simple_entity_or_value_expression())); + } + + return tokens; + } + + @Override + public List visitSimple_entity_or_value_expression( + EqlParser.Simple_entity_or_value_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.identification_variable() != null) { + tokens.addAll(visit(ctx.identification_variable())); + } else if (ctx.input_parameter() != null) { + tokens.addAll(visit(ctx.input_parameter())); + } else if (ctx.literal() != null) { + tokens.addAll(visit(ctx.literal())); + } + + return tokens; + } + + @Override + public List visitExists_expression(EqlParser.Exists_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.NOT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.NOT())); + } + tokens.add(new JpaQueryParsingToken(ctx.EXISTS())); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.subquery())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + + return tokens; + } + + @Override + public List visitAll_or_any_expression(EqlParser.All_or_any_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.ALL() != null) { + tokens.add(new JpaQueryParsingToken(ctx.ALL())); + } else if (ctx.ANY() != null) { + tokens.add(new JpaQueryParsingToken(ctx.ANY())); + } else if (ctx.SOME() != null) { + tokens.add(new JpaQueryParsingToken(ctx.SOME())); + } + + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.subquery())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + + return tokens; + } + + @Override + public List visitStringComparison(EqlParser.StringComparisonContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.string_expression(0))); + tokens.addAll(visit(ctx.comparison_operator())); + + if (ctx.string_expression(1) != null) { + tokens.addAll(visit(ctx.string_expression(1))); + } else { + tokens.addAll(visit(ctx.all_or_any_expression())); + } + + return tokens; + } + + @Override + public List visitBooleanComparison(EqlParser.BooleanComparisonContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.boolean_expression(0))); + tokens.add(new JpaQueryParsingToken(ctx.op)); + + if (ctx.boolean_expression(1) != null) { + tokens.addAll(visit(ctx.boolean_expression(1))); + } else { + tokens.addAll(visit(ctx.all_or_any_expression())); + } + + return tokens; + } + + @Override + public List visitDirectBooleanCheck(EqlParser.DirectBooleanCheckContext ctx) { + return visit(ctx.boolean_expression()); + } + + @Override + public List visitEnumComparison(EqlParser.EnumComparisonContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.enum_expression(0))); + tokens.add(new JpaQueryParsingToken(ctx.op)); + + if (ctx.enum_expression(1) != null) { + tokens.addAll(visit(ctx.enum_expression(1))); + } else { + tokens.addAll(visit(ctx.all_or_any_expression())); + } + + return tokens; + } + + @Override + public List visitDatetimeComparison(EqlParser.DatetimeComparisonContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.datetime_expression(0))); + tokens.addAll(visit(ctx.comparison_operator())); + + if (ctx.datetime_expression(1) != null) { + tokens.addAll(visit(ctx.datetime_expression(1))); + } else { + tokens.addAll(visit(ctx.all_or_any_expression())); + } + + return tokens; + } + + @Override + public List visitEntityComparison(EqlParser.EntityComparisonContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.entity_expression(0))); + tokens.add(new JpaQueryParsingToken(ctx.op)); + + if (ctx.entity_expression(1) != null) { + tokens.addAll(visit(ctx.entity_expression(1))); + } else { + tokens.addAll(visit(ctx.all_or_any_expression())); + } + + return tokens; + } + + @Override + public List visitArithmeticComparison(EqlParser.ArithmeticComparisonContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.arithmetic_expression(0))); + tokens.addAll(visit(ctx.comparison_operator())); + + if (ctx.arithmetic_expression(1) != null) { + tokens.addAll(visit(ctx.arithmetic_expression(1))); + } else { + tokens.addAll(visit(ctx.all_or_any_expression())); + } + + return tokens; + } + + @Override + public List visitEntityTypeComparison(EqlParser.EntityTypeComparisonContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.entity_type_expression(0))); + tokens.add(new JpaQueryParsingToken(ctx.op)); + tokens.addAll(visit(ctx.entity_type_expression(1))); + + return tokens; + } + + @Override + public List visitRegexpComparison(EqlParser.RegexpComparisonContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.string_expression())); + tokens.add(new JpaQueryParsingToken(ctx.REGEXP())); + tokens.addAll(visit(ctx.string_literal())); + + return tokens; + } + + @Override + public List visitComparison_operator(EqlParser.Comparison_operatorContext ctx) { + return List.of(new JpaQueryParsingToken(ctx.op)); + } + + @Override + public List visitArithmetic_expression(EqlParser.Arithmetic_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.arithmetic_expression() != null) { + + tokens.addAll(visit(ctx.arithmetic_expression())); + tokens.add(new JpaQueryParsingToken(ctx.op)); + tokens.addAll(visit(ctx.arithmetic_term())); + + } else { + tokens.addAll(visit(ctx.arithmetic_term())); + } + + return tokens; + } + + @Override + public List visitArithmetic_term(EqlParser.Arithmetic_termContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.arithmetic_term() != null) { + + tokens.addAll(visit(ctx.arithmetic_term())); + NOSPACE(tokens); + tokens.add(new JpaQueryParsingToken(ctx.op, false)); + tokens.addAll(visit(ctx.arithmetic_factor())); + } else { + tokens.addAll(visit(ctx.arithmetic_factor())); + } + + return tokens; + } + + @Override + public List visitArithmetic_factor(EqlParser.Arithmetic_factorContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.op != null) { + tokens.add(new JpaQueryParsingToken(ctx.op)); + } + tokens.addAll(visit(ctx.arithmetic_primary())); + + return tokens; + } + + @Override + public List visitArithmetic_primary(EqlParser.Arithmetic_primaryContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.state_valued_path_expression() != null) { + tokens.addAll(visit(ctx.state_valued_path_expression())); + } else if (ctx.numeric_literal() != null) { + tokens.addAll(visit(ctx.numeric_literal())); + } else if (ctx.arithmetic_expression() != null) { + + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.arithmetic_expression())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.input_parameter() != null) { + tokens.addAll(visit(ctx.input_parameter())); + } else if (ctx.functions_returning_numerics() != null) { + tokens.addAll(visit(ctx.functions_returning_numerics())); + } else if (ctx.aggregate_expression() != null) { + tokens.addAll(visit(ctx.aggregate_expression())); + } else if (ctx.case_expression() != null) { + tokens.addAll(visit(ctx.case_expression())); + } else if (ctx.cast_function() != null) { + tokens.addAll(visit(ctx.cast_function())); + } else if (ctx.function_invocation() != null) { + tokens.addAll(visit(ctx.function_invocation())); + } else if (ctx.subquery() != null) { + + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.subquery())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } + + return tokens; + } + + @Override + public List visitString_expression(EqlParser.String_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.state_valued_path_expression() != null) { + tokens.addAll(visit(ctx.state_valued_path_expression())); + } else if (ctx.string_literal() != null) { + tokens.addAll(visit(ctx.string_literal())); + } else if (ctx.input_parameter() != null) { + tokens.addAll(visit(ctx.input_parameter())); + } else if (ctx.functions_returning_strings() != null) { + tokens.addAll(visit(ctx.functions_returning_strings())); + } else if (ctx.aggregate_expression() != null) { + tokens.addAll(visit(ctx.aggregate_expression())); + } else if (ctx.case_expression() != null) { + tokens.addAll(visit(ctx.case_expression())); + } else if (ctx.function_invocation() != null) { + tokens.addAll(visit(ctx.function_invocation())); + } else if (ctx.subquery() != null) { + + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.subquery())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } + + return tokens; + } + + @Override + public List visitDatetime_expression(EqlParser.Datetime_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.state_valued_path_expression() != null) { + tokens.addAll(visit(ctx.state_valued_path_expression())); + } else if (ctx.input_parameter() != null) { + tokens.addAll(visit(ctx.input_parameter())); + } else if (ctx.input_parameter() != null) { + tokens.addAll(visit(ctx.input_parameter())); + } else if (ctx.functions_returning_datetime() != null) { + tokens.addAll(visit(ctx.functions_returning_datetime())); + } else if (ctx.aggregate_expression() != null) { + tokens.addAll(visit(ctx.aggregate_expression())); + } else if (ctx.case_expression() != null) { + tokens.addAll(visit(ctx.case_expression())); + } else if (ctx.function_invocation() != null) { + tokens.addAll(visit(ctx.function_invocation())); + } else if (ctx.date_time_timestamp_literal() != null) { + tokens.addAll(visit(ctx.date_time_timestamp_literal())); + } else if (ctx.subquery() != null) { + + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.subquery())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } + + return tokens; + } + + @Override + public List visitBoolean_expression(EqlParser.Boolean_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.state_valued_path_expression() != null) { + tokens.addAll(visit(ctx.state_valued_path_expression())); + } else if (ctx.boolean_literal() != null) { + tokens.addAll(visit(ctx.boolean_literal())); + } else if (ctx.input_parameter() != null) { + tokens.addAll(visit(ctx.input_parameter())); + } else if (ctx.case_expression() != null) { + tokens.addAll(visit(ctx.case_expression())); + } else if (ctx.function_invocation() != null) { + tokens.addAll(visit(ctx.function_invocation())); + } else if (ctx.subquery() != null) { + + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.subquery())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } + + return tokens; + } + + @Override + public List visitEnum_expression(EqlParser.Enum_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.state_valued_path_expression() != null) { + tokens.addAll(visit(ctx.state_valued_path_expression())); + } else if (ctx.enum_literal() != null) { + tokens.addAll(visit(ctx.enum_literal())); + } else if (ctx.input_parameter() != null) { + tokens.addAll(visit(ctx.input_parameter())); + } else if (ctx.case_expression() != null) { + tokens.addAll(visit(ctx.case_expression())); + } else if (ctx.subquery() != null) { + + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.subquery())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } + + return tokens; + } + + @Override + public List visitEntity_expression(EqlParser.Entity_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.single_valued_object_path_expression() != null) { + tokens.addAll(visit(ctx.single_valued_object_path_expression())); + } else if (ctx.simple_entity_expression() != null) { + tokens.addAll(visit(ctx.simple_entity_expression())); + } + + return tokens; + } + + @Override + public List visitSimple_entity_expression(EqlParser.Simple_entity_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.identification_variable() != null) { + tokens.addAll(visit(ctx.identification_variable())); + } else if (ctx.input_parameter() != null) { + tokens.addAll(visit(ctx.input_parameter())); + } + + return tokens; + } + + @Override + public List visitEntity_type_expression(EqlParser.Entity_type_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.type_discriminator() != null) { + tokens.addAll(visit(ctx.type_discriminator())); + } else if (ctx.entity_type_literal() != null) { + tokens.addAll(visit(ctx.entity_type_literal())); + } else if (ctx.input_parameter() != null) { + tokens.addAll(visit(ctx.input_parameter())); + } + + return tokens; + } + + @Override + public List visitType_discriminator(EqlParser.Type_discriminatorContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.TYPE(), false)); + tokens.add(TOKEN_OPEN_PAREN); + + if (ctx.general_identification_variable() != null) { + tokens.addAll(visit(ctx.general_identification_variable())); + } else if (ctx.single_valued_object_path_expression() != null) { + tokens.addAll(visit(ctx.single_valued_object_path_expression())); + } else if (ctx.input_parameter() != null) { + tokens.addAll(visit(ctx.input_parameter())); + } + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + + return tokens; + } + + @Override + public List visitFunctions_returning_numerics( + EqlParser.Functions_returning_numericsContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.LENGTH() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.LENGTH(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.string_expression(0))); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.LOCATE() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.LOCATE(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.string_expression(0))); + NOSPACE(tokens); + tokens.add(TOKEN_COMMA); + tokens.addAll(visit(ctx.string_expression(1))); + NOSPACE(tokens); + if (ctx.arithmetic_expression() != null) { + tokens.add(TOKEN_COMMA); + tokens.addAll(visit(ctx.arithmetic_expression(0))); + NOSPACE(tokens); + } + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.ABS() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.ABS(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.arithmetic_expression(0))); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.CEILING() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.CEILING(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.arithmetic_expression(0))); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.EXP() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.EXP(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.arithmetic_expression(0))); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.FLOOR() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.FLOOR(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.arithmetic_expression(0))); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.LN() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.LN(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.arithmetic_expression(0))); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.SIGN() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.SIGN(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.arithmetic_expression(0))); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.SQRT() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.SQRT(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.arithmetic_expression(0))); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.MOD() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.MOD(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.arithmetic_expression(0))); + tokens.add(new JpaQueryParsingToken("/")); + tokens.addAll(visit(ctx.arithmetic_expression(1))); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.POWER() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.POWER(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.arithmetic_expression(0))); + NOSPACE(tokens); + tokens.add(TOKEN_COMMA); + tokens.addAll(visit(ctx.arithmetic_expression(1))); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.ROUND() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.ROUND(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.arithmetic_expression(0))); + NOSPACE(tokens); + tokens.add(TOKEN_COMMA); + tokens.addAll(visit(ctx.arithmetic_expression(1))); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.SIZE() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.SIZE(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.collection_valued_path_expression())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.INDEX() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.INDEX(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.identification_variable())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.extract_datetime_field() != null) { + tokens.addAll(visit(ctx.extract_datetime_field())); + } + + return tokens; + } + + @Override + public List visitFunctions_returning_datetime( + EqlParser.Functions_returning_datetimeContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.CURRENT_DATE() != null) { + tokens.add(new JpaQueryParsingToken(ctx.CURRENT_DATE())); + } else if (ctx.CURRENT_TIME() != null) { + tokens.add(new JpaQueryParsingToken(ctx.CURRENT_TIME())); + } else if (ctx.CURRENT_TIMESTAMP() != null) { + tokens.add(new JpaQueryParsingToken(ctx.CURRENT_TIMESTAMP())); + } else if (ctx.LOCAL() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.LOCAL())); + + if (ctx.DATE() != null) { + tokens.add(new JpaQueryParsingToken(ctx.DATE())); + } else if (ctx.TIME() != null) { + tokens.add(new JpaQueryParsingToken(ctx.TIME())); + } else if (ctx.DATETIME() != null) { + tokens.add(new JpaQueryParsingToken(ctx.DATETIME())); + } + } else if (ctx.extract_datetime_part() != null) { + tokens.addAll(visit(ctx.extract_datetime_part())); + } + + return tokens; + } + + @Override + public List visitFunctions_returning_strings(EqlParser.Functions_returning_stringsContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.CONCAT() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.CONCAT(), false)); + tokens.add(TOKEN_OPEN_PAREN); + ctx.string_expression().forEach(stringExpressionContext -> { + tokens.addAll(visit(stringExpressionContext)); + NOSPACE(tokens); + tokens.add(TOKEN_COMMA); + }); + CLIP(tokens); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.SUBSTRING() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.SUBSTRING(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.string_expression(0))); + NOSPACE(tokens); + tokens.add(TOKEN_COMMA); + ctx.arithmetic_expression().forEach(arithmeticExpressionContext -> { + tokens.addAll(visit(arithmeticExpressionContext)); + NOSPACE(tokens); + tokens.add(TOKEN_COMMA); + }); + CLIP(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.TRIM() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.TRIM(), false)); + tokens.add(TOKEN_OPEN_PAREN); + if (ctx.trim_specification() != null) { + tokens.addAll(visit(ctx.trim_specification())); + } + if (ctx.trim_character() != null) { + tokens.addAll(visit(ctx.trim_character())); + } + if (ctx.FROM() != null) { + tokens.add(new JpaQueryParsingToken(ctx.FROM())); + } + tokens.addAll(visit(ctx.string_expression(0))); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.LOWER() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.LOWER(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.string_expression(0))); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else if (ctx.UPPER() != null) { + + tokens.add(new JpaQueryParsingToken(ctx.UPPER(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.string_expression(0))); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } + + return tokens; + } + + @Override + public List visitTrim_specification(EqlParser.Trim_specificationContext ctx) { + + if (ctx.LEADING() != null) { + return List.of(new JpaQueryParsingToken(ctx.LEADING())); + } else if (ctx.TRAILING() != null) { + return List.of(new JpaQueryParsingToken(ctx.TRAILING())); + } else { + return List.of(new JpaQueryParsingToken(ctx.BOTH())); + } + } + + @Override + public List visitCast_function(EqlParser.Cast_functionContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.CAST(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.single_valued_path_expression())); + tokens.addAll(visit(ctx.identification_variable())); + NOSPACE(tokens); + + if (ctx.numeric_literal() != null) { + + tokens.add(TOKEN_OPEN_PAREN); + ctx.numeric_literal().forEach(numericLiteralContext -> { + tokens.addAll(visit(numericLiteralContext)); + NOSPACE(tokens); + tokens.add(TOKEN_COMMA); + }); + CLIP(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + + return tokens; + } + + @Override + public List visitFunction_invocation(EqlParser.Function_invocationContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.FUNCTION() != null) { + tokens.add(new JpaQueryParsingToken(ctx.FUNCTION(), false)); + } else if (ctx.identification_variable() != null) { + + tokens.addAll(visit(ctx.identification_variable())); + NOSPACE(tokens); + } + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.function_name())); + NOSPACE(tokens); + ctx.function_arg().forEach(functionArgContext -> { + tokens.add(TOKEN_COMMA); + tokens.addAll(visit(functionArgContext)); + NOSPACE(tokens); + }); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + + return tokens; + } + + @Override + public List visitExtract_datetime_field(EqlParser.Extract_datetime_fieldContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.EXTRACT(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.datetime_field())); + tokens.add(new JpaQueryParsingToken(ctx.FROM())); + tokens.addAll(visit(ctx.datetime_expression())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + + return tokens; + } + + @Override + public List visitDatetime_field(EqlParser.Datetime_fieldContext ctx) { + return visit(ctx.identification_variable()); + } + + @Override + public List visitExtract_datetime_part(EqlParser.Extract_datetime_partContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.EXTRACT(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.datetime_part())); + tokens.add(new JpaQueryParsingToken(ctx.FROM())); + tokens.addAll(visit(ctx.datetime_expression())); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + + return tokens; + } + + @Override + public List visitDatetime_part(EqlParser.Datetime_partContext ctx) { + return visit(ctx.identification_variable()); + } + + @Override + public List visitFunction_arg(EqlParser.Function_argContext ctx) { + + if (ctx.literal() != null) { + return visit(ctx.literal()); + } else if (ctx.state_valued_path_expression() != null) { + return visit(ctx.state_valued_path_expression()); + } else if (ctx.input_parameter() != null) { + return visit(ctx.input_parameter()); + } else { + return visit(ctx.scalar_expression()); + } + } + + @Override + public List visitCase_expression(EqlParser.Case_expressionContext ctx) { + + if (ctx.general_case_expression() != null) { + return visit(ctx.general_case_expression()); + } else if (ctx.simple_case_expression() != null) { + return visit(ctx.simple_case_expression()); + } else if (ctx.coalesce_expression() != null) { + return visit(ctx.coalesce_expression()); + } else { + return visit(ctx.nullif_expression()); + } + } + + @Override + public List visitGeneral_case_expression(EqlParser.General_case_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.CASE())); + + ctx.when_clause().forEach(whenClauseContext -> { + tokens.addAll(visit(whenClauseContext)); + }); + + tokens.add(new JpaQueryParsingToken(ctx.ELSE())); + tokens.addAll(visit(ctx.scalar_expression())); + tokens.add(new JpaQueryParsingToken(ctx.END())); + + return tokens; + } + + @Override + public List visitWhen_clause(EqlParser.When_clauseContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.WHEN())); + tokens.addAll(visit(ctx.conditional_expression())); + tokens.add(new JpaQueryParsingToken(ctx.THEN())); + tokens.addAll(visit(ctx.scalar_expression())); + + return tokens; + } + + @Override + public List visitSimple_case_expression(EqlParser.Simple_case_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.CASE())); + tokens.addAll(visit(ctx.case_operand())); + + ctx.simple_when_clause().forEach(simpleWhenClauseContext -> { + tokens.addAll(visit(simpleWhenClauseContext)); + }); + + tokens.add(new JpaQueryParsingToken(ctx.ELSE())); + tokens.addAll(visit(ctx.scalar_expression())); + tokens.add(new JpaQueryParsingToken(ctx.END())); + + return tokens; + } + + @Override + public List visitCase_operand(EqlParser.Case_operandContext ctx) { + + if (ctx.state_valued_path_expression() != null) { + return visit(ctx.state_valued_path_expression()); + } else { + return visit(ctx.type_discriminator()); + } + } + + @Override + public List visitSimple_when_clause(EqlParser.Simple_when_clauseContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.WHEN())); + tokens.addAll(visit(ctx.scalar_expression(0))); + tokens.add(new JpaQueryParsingToken(ctx.THEN())); + tokens.addAll(visit(ctx.scalar_expression(1))); + + return tokens; + } + + @Override + public List visitCoalesce_expression(EqlParser.Coalesce_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.COALESCE(), false)); + tokens.add(TOKEN_OPEN_PAREN); + ctx.scalar_expression().forEach(scalarExpressionContext -> { + tokens.addAll(visit(scalarExpressionContext)); + NOSPACE(tokens); + tokens.add(TOKEN_COMMA); + }); + CLIP(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + + return tokens; + } + + @Override + public List visitNullif_expression(EqlParser.Nullif_expressionContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.NULLIF(), false)); + tokens.add(TOKEN_OPEN_PAREN); + tokens.addAll(visit(ctx.scalar_expression(0))); + NOSPACE(tokens); + tokens.add(TOKEN_COMMA); + tokens.addAll(visit(ctx.scalar_expression(1))); + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + + return tokens; + } + + @Override + public List visitTrim_character(EqlParser.Trim_characterContext ctx) { + + if (ctx.CHARACTER() != null) { + return List.of(new JpaQueryParsingToken(ctx.CHARACTER())); + } else if (ctx.character_valued_input_parameter() != null) { + return visit(ctx.character_valued_input_parameter()); + } else { + return List.of(); + } + } + + @Override + public List visitIdentification_variable(EqlParser.Identification_variableContext ctx) { + + if (ctx.IDENTIFICATION_VARIABLE() != null) { + return List.of(new JpaQueryParsingToken(ctx.IDENTIFICATION_VARIABLE())); + } else if (ctx.f != null) { + return List.of(new JpaQueryParsingToken(ctx.f)); + } else { + return List.of(); + } + } + + @Override + public List visitConstructor_name(EqlParser.Constructor_nameContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.state_field_path_expression())); + NOSPACE(tokens); + + return tokens; + } + + @Override + public List visitLiteral(EqlParser.LiteralContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.STRINGLITERAL() != null) { + tokens.add(new JpaQueryParsingToken(ctx.STRINGLITERAL())); + } else if (ctx.INTLITERAL() != null) { + tokens.add(new JpaQueryParsingToken(ctx.INTLITERAL())); + } else if (ctx.FLOATLITERAL() != null) { + tokens.add(new JpaQueryParsingToken(ctx.FLOATLITERAL())); + } else if (ctx.LONGLITERAL() != null) { + tokens.add(new JpaQueryParsingToken(ctx.LONGLITERAL())); + } else if (ctx.boolean_literal() != null) { + tokens.addAll(visit(ctx.boolean_literal())); + } else if (ctx.entity_type_literal() != null) { + tokens.addAll(visit(ctx.entity_type_literal())); + } + + return tokens; + } + + @Override + public List visitInput_parameter(EqlParser.Input_parameterContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.INTLITERAL() != null) { + + tokens.add(TOKEN_QUESTION_MARK); + tokens.add(new JpaQueryParsingToken(ctx.INTLITERAL())); + } else if (ctx.identification_variable() != null) { + + tokens.add(TOKEN_COLON); + tokens.addAll(visit(ctx.identification_variable())); + } + + return tokens; + } + + @Override + public List visitPattern_value(EqlParser.Pattern_valueContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.addAll(visit(ctx.string_expression())); + + return tokens; + } + + @Override + public List visitDate_time_timestamp_literal(EqlParser.Date_time_timestamp_literalContext ctx) { + + if (ctx.STRINGLITERAL() != null) { + return List.of(new JpaQueryParsingToken(ctx.STRINGLITERAL())); + } else if (ctx.DATELITERAL() != null) { + return List.of(new JpaQueryParsingToken(ctx.DATELITERAL())); + } else if (ctx.TIMELITERAL() != null) { + return List.of(new JpaQueryParsingToken(ctx.TIMELITERAL())); + } else if (ctx.TIMESTAMPLITERAL() != null) { + return List.of(new JpaQueryParsingToken(ctx.TIMESTAMPLITERAL())); + } else { + return List.of(); + } + } + + @Override + public List visitEntity_type_literal(EqlParser.Entity_type_literalContext ctx) { + return visit(ctx.identification_variable()); + } + + @Override + public List visitEscape_character(EqlParser.Escape_characterContext ctx) { + return List.of(new JpaQueryParsingToken(ctx.CHARACTER())); + } + + @Override + public List visitNumeric_literal(EqlParser.Numeric_literalContext ctx) { + + if (ctx.INTLITERAL() != null) { + return List.of(new JpaQueryParsingToken(ctx.INTLITERAL())); + } else if (ctx.FLOATLITERAL() != null) { + return List.of(new JpaQueryParsingToken(ctx.FLOATLITERAL())); + } else if (ctx.LONGLITERAL() != null) { + return List.of(new JpaQueryParsingToken(ctx.LONGLITERAL())); + } else { + return List.of(); + } + } + + @Override + public List visitBoolean_literal(EqlParser.Boolean_literalContext ctx) { + + if (ctx.TRUE() != null) { + return List.of(new JpaQueryParsingToken(ctx.TRUE())); + } else if (ctx.FALSE() != null) { + return List.of(new JpaQueryParsingToken(ctx.FALSE())); + } else { + return List.of(); + } + } + + @Override + public List visitEnum_literal(EqlParser.Enum_literalContext ctx) { + return visit(ctx.state_field_path_expression()); + } + + @Override + public List visitString_literal(EqlParser.String_literalContext ctx) { + + if (ctx.CHARACTER() != null) { + return List.of(new JpaQueryParsingToken(ctx.CHARACTER())); + } else if (ctx.STRINGLITERAL() != null) { + return List.of(new JpaQueryParsingToken(ctx.STRINGLITERAL())); + } else { + return List.of(); + } + } + + @Override + public List visitSingle_valued_embeddable_object_field( + EqlParser.Single_valued_embeddable_object_fieldContext ctx) { + return visit(ctx.identification_variable()); + } + + @Override + public List visitSubtype(EqlParser.SubtypeContext ctx) { + return visit(ctx.identification_variable()); + } + + @Override + public List visitCollection_valued_field(EqlParser.Collection_valued_fieldContext ctx) { + return visit(ctx.identification_variable()); + } + + @Override + public List visitSingle_valued_object_field(EqlParser.Single_valued_object_fieldContext ctx) { + return visit(ctx.identification_variable()); + } + + @Override + public List visitState_field(EqlParser.State_fieldContext ctx) { + return visit(ctx.identification_variable()); + } + + @Override + public List visitCollection_value_field(EqlParser.Collection_value_fieldContext ctx) { + return visit(ctx.identification_variable()); + } + + @Override + public List visitEntity_name(EqlParser.Entity_nameContext ctx) { + + List tokens = new ArrayList<>(); + + ctx.identification_variable().forEach(identificationVariableContext -> { + tokens.addAll(visit(identificationVariableContext)); + NOSPACE(tokens); + tokens.add(TOKEN_DOT); + }); + CLIP(tokens); + SPACE(tokens); + + return tokens; + } + + @Override + public List visitResult_variable(EqlParser.Result_variableContext ctx) { + return visit(ctx.identification_variable()); + } + + @Override + public List visitSuperquery_identification_variable( + EqlParser.Superquery_identification_variableContext ctx) { + return visit(ctx.identification_variable()); + } + + @Override + public List visitCollection_valued_input_parameter( + EqlParser.Collection_valued_input_parameterContext ctx) { + return visit(ctx.input_parameter()); + } + + @Override + public List visitSingle_valued_input_parameter( + EqlParser.Single_valued_input_parameterContext ctx) { + return visit(ctx.input_parameter()); + } + + @Override + public List visitFunction_name(EqlParser.Function_nameContext ctx) { + return visit(ctx.string_literal()); + } + + @Override + public List visitCharacter_valued_input_parameter( + EqlParser.Character_valued_input_parameterContext ctx) { + + if (ctx.CHARACTER() != null) { + return List.of(new JpaQueryParsingToken(ctx.CHARACTER())); + } else if (ctx.input_parameter() != null) { + return visit(ctx.input_parameter()); + } else { + return List.of(); + } + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryTransformer.java new file mode 100644 index 000000000..4abc0a660 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryTransformer.java @@ -0,0 +1,243 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import static org.springframework.data.jpa.repository.query.JpaQueryParsingToken.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.data.domain.Sort; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed EQL query. + * + * @author Greg Turnquist + * @since 3.2 + */ +class EqlQueryTransformer extends EqlQueryRenderer { + + // TODO: Separate input from result parameters, encapsulation... + private final Sort sort; + private final boolean countQuery; + + private final @Nullable String countProjection; + + private @Nullable String primaryFromAlias = null; + + private List projection = Collections.emptyList(); + private boolean projectionProcessed; + + private boolean hasConstructorExpression = false; + + private JpaQueryTransformerSupport transformerSupport; + + EqlQueryTransformer() { + this(Sort.unsorted(), false, null); + } + + EqlQueryTransformer(Sort sort) { + this(sort, false, null); + } + + EqlQueryTransformer(boolean countQuery, @Nullable String countProjection) { + this(Sort.unsorted(), countQuery, countProjection); + } + + private EqlQueryTransformer(Sort sort, boolean countQuery, @Nullable String countProjection) { + + Assert.notNull(sort, "Sort must not be null"); + + this.sort = sort; + this.countQuery = countQuery; + this.countProjection = countProjection; + this.transformerSupport = new JpaQueryTransformerSupport(); + } + + @Nullable + public String getAlias() { + return this.primaryFromAlias; + } + + public List getProjection() { + return this.projection; + } + + public boolean hasConstructorExpression() { + return this.hasConstructorExpression; + } + + @Override + public List visitSelect_statement(EqlParser.Select_statementContext ctx) { + + List tokens = newArrayList(); + + tokens.addAll(visit(ctx.select_clause())); + tokens.addAll(visit(ctx.from_clause())); + + if (ctx.where_clause() != null) { + tokens.addAll(visit(ctx.where_clause())); + } + + if (ctx.groupby_clause() != null) { + tokens.addAll(visit(ctx.groupby_clause())); + } + + if (ctx.having_clause() != null) { + tokens.addAll(visit(ctx.having_clause())); + } + + if (!countQuery) { + + if (ctx.orderby_clause() != null) { + tokens.addAll(visit(ctx.orderby_clause())); + } + + if (sort.isSorted()) { + + if (ctx.orderby_clause() != null) { + + NOSPACE(tokens); + tokens.add(TOKEN_COMMA); + } else { + + SPACE(tokens); + tokens.add(TOKEN_ORDER_BY); + } + + tokens.addAll(transformerSupport.generateOrderByArguments(primaryFromAlias, sort)); + } + } + + return tokens; + } + + @Override + public List visitSelect_clause(EqlParser.Select_clauseContext ctx) { + + List tokens = newArrayList(); + + tokens.add(new JpaQueryParsingToken(ctx.SELECT())); + + if (countQuery) { + tokens.add(TOKEN_COUNT_FUNC); + } + + if (ctx.DISTINCT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.DISTINCT())); + } + + List selectItemTokens = newArrayList(); + + ctx.select_item().forEach(selectItemContext -> { + selectItemTokens.addAll(visit(selectItemContext)); + NOSPACE(selectItemTokens); + selectItemTokens.add(TOKEN_COMMA); + }); + CLIP(selectItemTokens); + SPACE(selectItemTokens); + + if (countQuery) { + + if (countProjection != null) { + tokens.add(new JpaQueryParsingToken(countProjection)); + } else { + + if (ctx.DISTINCT() != null) { + + if (selectItemTokens.stream().anyMatch(jpqlToken -> jpqlToken.getToken().contains("new"))) { + // constructor + tokens.add(new JpaQueryParsingToken(() -> primaryFromAlias)); + } else { + // keep all the select items to distinct against + tokens.addAll(selectItemTokens); + } + } else { + tokens.add(new JpaQueryParsingToken(() -> primaryFromAlias)); + } + } + + NOSPACE(tokens); + tokens.add(TOKEN_CLOSE_PAREN); + } else { + tokens.addAll(selectItemTokens); + } + + if (!projectionProcessed) { + projection = selectItemTokens; + projectionProcessed = true; + } + + return tokens; + } + + @Override + public List visitSelect_item(EqlParser.Select_itemContext ctx) { + + List tokens = super.visitSelect_item(ctx); + + if (ctx.result_variable() != null) { + transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken()); + } + + return tokens; + } + + @Override + public List visitRange_variable_declaration(EqlParser.Range_variable_declarationContext ctx) { + + List tokens = newArrayList(); + + tokens.addAll(visit(ctx.entity_name())); + + if (ctx.AS() != null) { + tokens.add(new JpaQueryParsingToken(ctx.AS())); + } + + tokens.addAll(visit(ctx.identification_variable())); + + if (primaryFromAlias == null) { + primaryFromAlias = tokens.get(tokens.size() - 1).getToken(); + } + + return tokens; + } + + @Override + public List visitJoin(EqlParser.JoinContext ctx) { + + List tokens = super.visitJoin(ctx); + + transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken()); + + return tokens; + } + + @Override + public List visitConstructor_expression(EqlParser.Constructor_expressionContext ctx) { + + hasConstructorExpression = true; + + return super.visitConstructor_expression(ctx); + } + + private static ArrayList newArrayList() { + return new ArrayList<>(); + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java index b44ca445c..118b5af3d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java @@ -29,6 +29,7 @@ import org.springframework.util.Assert; * @since 3.1 * @see JpqlQueryParser * @see HqlQueryParser + * @see EqlQueryParser */ class JpaQueryEnhancer implements QueryEnhancer { @@ -73,6 +74,20 @@ class JpaQueryEnhancer implements QueryEnhancer { return new JpaQueryEnhancer(query, new HqlQueryParser(query.getQueryString())); } + /** + * Factory method to create a {@link JpaQueryParserSupport} for {@link DeclaredQuery} using EQL grammar. + * + * @param query must not be {@literal null}. + * @return a new {@link JpaQueryEnhancer} using EQL. + * @since 3.2 + */ + public static JpaQueryEnhancer forEql(DeclaredQuery query) { + + Assert.notNull(query, "DeclaredQuery must not be null!"); + + return new JpaQueryEnhancer(query, new EqlQueryParser(query.getQueryString())); + } + protected JpaQueryParserSupport getQueryParsingStrategy() { return queryParser; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryParsingToken.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryParsingToken.java index 00717de3d..4afe8bf3b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryParsingToken.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryParsingToken.java @@ -67,6 +67,12 @@ class JpaQueryParsingToken { public static final JpaQueryParsingToken TOKEN_MATERIALIZED = new JpaQueryParsingToken("materialized"); + public static final JpaQueryParsingToken TOKEN_NULLS = new JpaQueryParsingToken("NULLS"); + + public static final JpaQueryParsingToken TOKEN_FIRST = new JpaQueryParsingToken("FIRST"); + + public static final JpaQueryParsingToken TOKEN_LAST = new JpaQueryParsingToken("LAST"); + /** * The text value of the token. */ diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactory.java index 74aa77e61..6ef6492b8 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactory.java @@ -17,6 +17,7 @@ package org.springframework.data.jpa.repository.query; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.util.ClassUtils; /** @@ -34,18 +35,20 @@ public final class QueryEnhancerFactory { private static final boolean jSqlParserPresent = ClassUtils.isPresent("net.sf.jsqlparser.parser.JSqlParser", QueryEnhancerFactory.class.getClassLoader()); - private static final boolean hibernatePresent = ClassUtils.isPresent("org.hibernate.query.TypedParameterValue", - QueryEnhancerFactory.class.getClassLoader()); - static { if (jSqlParserPresent) { LOG.info("JSqlParser is in classpath; If applicable, JSqlParser will be used"); } - if (hibernatePresent) { + if (PersistenceProvider.ECLIPSELINK.isPresent()) { + LOG.info("EclipseLink is in classpath; If applicable, EQL parser will be used."); + } + + if (PersistenceProvider.HIBERNATE.isPresent()) { LOG.info("Hibernate is in classpath; If applicable, HQL parser will be used."); } + } private QueryEnhancerFactory() {} @@ -70,7 +73,13 @@ public final class QueryEnhancerFactory { return new DefaultQueryEnhancer(query); } - return hibernatePresent ? JpaQueryEnhancer.forHql(query) : JpaQueryEnhancer.forJpql(query); + if (PersistenceProvider.HIBERNATE.isPresent()) { + return JpaQueryEnhancer.forHql(query); + } else if (PersistenceProvider.ECLIPSELINK.isPresent()) { + return JpaQueryEnhancer.forEql(query); + } else { + return JpaQueryEnhancer.forJpql(query); + } } } 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 new file mode 100644 index 000000000..44451ee2a --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java @@ -0,0 +1,403 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.jpa.repository.query.JpaQueryParsingToken.*; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.junit.jupiter.api.Test; + +/** + * Tests built around examples of EQL found in the EclipseLink's docs at + * https://wiki.eclipse.org/EclipseLink/UserGuide/JPA/Basic_JPA_Development/Querying/JPQL
+ *
+ * IMPORTANT: Purely verifies the parser without any transformations. + * + * @author Greg Turnquist + */ +class EqlComplianceTests { + + /** + * Parse the query using {@link EqlParser} then run it through the query-preserving {@link EqlQueryRenderer}. + * + * @param query + */ + private static String parseWithoutChanges(String query) { + + EqlLexer lexer = new EqlLexer(CharStreams.fromString(query)); + EqlParser parser = new EqlParser(new CommonTokenStream(lexer)); + + parser.addErrorListener(new BadJpqlGrammarErrorListener(query)); + + EqlParser.StartContext parsedQuery = parser.start(); + + return render(new EqlQueryRenderer().visit(parsedQuery)); + } + + private void assertQuery(String query) { + + String slimmedDownQuery = reduceWhitespace(query); + assertThat(parseWithoutChanges(slimmedDownQuery)).isEqualTo(slimmedDownQuery); + } + + private String reduceWhitespace(String original) { + + return original // + .replaceAll("[ \\t\\n]{1,}", " ") // + .trim(); + } + + @Test + void selectQueries() { + + assertQuery("Select e FROM Employee e WHERE e.salary > 100000"); + assertQuery("Select e FROM Employee e WHERE e.id = :id"); + assertQuery("Select MAX(e.salary) FROM Employee e"); + assertQuery("Select e.firstName FROM Employee e"); + assertQuery("Select e.firstName, e.lastName FROM Employee e"); + } + + @Test + void selectClause() { + + assertQuery("SELECT COUNT(e) FROM Employee e"); + assertQuery("SELECT MAX(e.salary) FROM Employee e"); + assertQuery("SELECT NEW com.acme.reports.EmpReport(e.firstName, e.lastName, e.salary) FROM Employee e"); + } + + @Test + void fromClause() { + + assertQuery("SELECT e FROM Employee e"); + assertQuery("SELECT e, a FROM Employee e, MailingAddress a WHERE e.address = a.address"); + assertQuery("SELECT e FROM com.acme.Employee e"); + } + + @Test + void join() { + + assertQuery("SELECT e FROM Employee e JOIN e.address a WHERE a.city = :city"); + assertQuery("SELECT e FROM Employee e JOIN e.projects p JOIN e.projects p2 WHERE p.name = :p1 AND p2.name = :p2"); + } + + @Test + void joinFetch() { + + assertQuery("SELECT e FROM Employee e JOIN FETCH e.address"); + assertQuery("SELECT e FROM Employee e JOIN FETCH e.address a ORDER BY a.city"); + } + + @Test + void leftJoin() { + assertQuery("SELECT e FROM Employee e LEFT JOIN e.address a ORDER BY a.city"); + } + + @Test + void on() { + + assertQuery("SELECT e FROM Employee e LEFT JOIN e.address ON a.city = :city"); + assertQuery("SELECT e FROM Employee e LEFT JOIN MailingAddress a ON e.address = a.address"); + } + + @Test + void subselectsInFromClause() { + assertQuery( + "SELECT e, c.city FROM Employee e, (SELECT DISTINCT a.city FROM Address a) c WHERE e.address.city = c.city"); + } + + @Test + void orderByClause() { + + assertQuery("SELECT e FROM Employee e ORDER BY e.lastName ASC, e.firstName ASC"); // Typo in EQL document + assertQuery("SELECT e FROM Employee e ORDER BY UPPER(e.lastName)"); + assertQuery("SELECT e FROM Employee e LEFT JOIN e.manager m ORDER BY m.lastName NULLS FIRST"); + assertQuery("SELECT e FROM Employee e ORDER BY e.address"); + } + + @Test + void groupByClause() { + + assertQuery("SELECT AVG(e.salary), e.address.city FROM Employee e GROUP BY e.address.city"); + assertQuery("SELECT AVG(e.salary), e.address.city FROM Employee e GROUP BY e.address.city ORDER BY AVG(e.salary)"); + assertQuery("SELECT e, COUNT(p) FROM Employee e LEFT JOIN e.projects p GROUP BY e"); + } + + @Test + void havingClause() { + assertQuery( + "SELECT AVG(e.salary), e.address.city FROM Employee e GROUP BY e.address.city HAVING AVG(e.salary) > 100000"); + } + + @Test + void union() { + + assertQuery(""" + SELECT MAX(e.salary) FROM Employee e WHERE e.address.city = :city1 + UNION SELECT MAX(e.salary) FROM Employee e WHERE e.address.city = :city2 + """); + assertQuery(""" + SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode1 + INTERSECT SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode2 + """); + assertQuery(""" + SELECT e FROM Employee e + EXCEPT SELECT e FROM Employee e WHERE e.salary > e.manager.salary + """); + } + + @Test + void whereClause() { + // TBD + } + + @Test + void updateQueries() { + assertQuery("UPDATE Employee e SET e.salary = 60000 WHERE e.salary = 50000"); + } + + @Test + void deleteQueries() { + assertQuery("DELETE FROM Employee e WHERE e.department IS NULL"); + } + + @Test + void literals() { + + assertQuery("SELECT e FROM Employee e WHERE e.name = 'Bob'"); + assertQuery("SELECT e FROM Employee e WHERE e.id = 1234"); + assertQuery("SELECT e FROM Employee e WHERE e.id = 1234L"); + assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14F"); + assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14e32D"); + assertQuery("SELECT e FROM Employee e WHERE e.active = TRUE"); + assertQuery("SELECT e FROM Employee e WHERE e.startDate = {d'2012-01-03'}"); + assertQuery("SELECT e FROM Employee e WHERE e.startTime = {t'09:00:00'}"); + assertQuery("SELECT e FROM Employee e WHERE e.version = {ts'2012-01-03 09:00:00.000000001'}"); + assertQuery("SELECT e FROM Employee e WHERE e.gender = org.acme.Gender.MALE"); + assertQuery("UPDATE Employee e SET e.manager = NULL WHERE e.manager = :manager"); + } + + @Test + void functionsInSelect() { + + assertQuery("SELECT e.salary - 1000 FROM Employee e"); + assertQuery("SELECT e.salary + 1000 FROM Employee e"); + assertQuery("SELECT e.salary*2 FROM Employee e"); + assertQuery("SELECT e.salary*2.0 FROM Employee e"); + assertQuery("SELECT e.salary/2 FROM Employee e"); + assertQuery("SELECT e.salary/2.0 FROM Employee e"); + assertQuery("SELECT ABS(e.salary - e.manager.salary) FROM Employee e"); + assertQuery( + "select e from Employee e where case e.firstName when 'Bob' then 'Robert' when 'Jill' then 'Gillian' else '' end = 'Robert'"); + assertQuery( + "select case when e.firstName = 'Bob' then 'Robert' when e.firstName = 'Jill' then 'Gillian' else '' end from Employee e where e.firstName = 'Bob' or e.firstName = 'Jill'"); + assertQuery( + "select e from Employee e where case when e.firstName = 'Bob' then 'Robert' when e.firstName = 'Jill' then 'Gillian' else '' end = 'Robert'"); + assertQuery("SELECT COALESCE(e.salary, 0) FROM Employee e"); + assertQuery("SELECT CONCAT(e.firstName, ' ', e.lastName) FROM Employee e"); + assertQuery("SELECT e.name, CURRENT_DATE FROM Employee e"); + assertQuery("SELECT e.name, CURRENT_TIME FROM Employee e"); + assertQuery("SELECT e.name, CURRENT_TIMESTAMP FROM Employee e"); + assertQuery("SELECT LENGTH(e.lastName) FROM Employee e"); + assertQuery("SELECT LOWER(e.lastName) FROM Employee e"); + assertQuery("SELECT MOD(e.hoursWorked / 8) FROM Employee e"); + assertQuery("SELECT NULLIF(e.salary, 0) FROM Employee e"); + assertQuery("SELECT SQRT(o.RESULT) FROM Output o"); + assertQuery("SELECT SUBSTRING(e.lastName, 0, 2) FROM Employee e"); + assertQuery( + "SELECT TRIM(TRAILING FROM e.lastName), TRIM(e.lastName), TRIM(LEADING '-' FROM e.lastName) FROM Employee e"); + assertQuery("SELECT UPPER(e.lastName) FROM Employee e"); + assertQuery("SELECT CAST(e.salary NUMERIC(10, 2)) FROM Employee e"); + assertQuery("SELECT EXTRACT(YEAR FROM e.startDate) FROM Employee e"); + assertQuery("SELECT e FROM Employee e WHERE e.lastName REGEXP '^Dr.*'"); + assertQuery("SELECT e FROM Employee e WHERE e.lastName REGEXP '^Dr\\.*'"); + } + + @Test + void functionsInWhere() { + + assertQuery("SELECT e FROM Employee e WHERE e.salary - 1000 > 0"); + assertQuery("SELECT e FROM Employee e WHERE e.salary + 1000 > 0"); + assertQuery("SELECT e FROM Employee e WHERE e.salary*2 > 0"); + assertQuery("SELECT e FROM Employee e WHERE e.salary*2.0 > 0.0"); + assertQuery("SELECT e FROM Employee e WHERE e.salary/2 > 0"); + assertQuery("SELECT e FROM Employee e WHERE e.salary/2.0 > 0.0"); + assertQuery("SELECT e FROM Employee e WHERE ABS(e.salary - e.manager.salary) > 0"); + assertQuery("SELECT e FROM Employee e WHERE COALESCE(e.salary, 0) > 0"); + assertQuery("SELECT e FROM Employee e WHERE CONCAT(e.firstName, ' ', e.lastName) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE CURRENT_DATE > CURRENT_TIME"); + assertQuery("SELECT e FROM Employee e WHERE CURRENT_TIME > CURRENT_TIMESTAMP"); + assertQuery("SELECT e FROM Employee e WHERE LENGTH(e.lastName) > 0"); + assertQuery("SELECT e FROM Employee e WHERE LOWER(e.lastName) = 'bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE MOD(e.hoursWorked / 8) > 0"); + assertQuery("SELECT e FROM Employee e WHERE NULLIF(e.salary, 0) is null"); + assertQuery("SELECT e FROM Employee e WHERE SQRT(o.RESULT) > 0.0"); + assertQuery("SELECT e FROM Employee e WHERE SUBSTRING(e.lastName, 0, 2) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE TRIM(TRAILING FROM e.lastName) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE TRIM(e.lastName) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE TRIM(LEADING '-' FROM e.lastName) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE UPPER(e.lastName) = 'BILBO'"); + assertQuery("SELECT e FROM Employee e WHERE CAST(e.salary NUMERIC(10, 2)) > 0.0"); + assertQuery("SELECT e FROM Employee e WHERE EXTRACT(YEAR FROM e.startDate) = '2023'"); + } + + @Test + void functionsInOrderBy() { + + assertQuery("SELECT e FROM Employee e ORDER BY e.salary - 1000"); + assertQuery("SELECT e FROM Employee e ORDER BY e.salary + 1000"); + assertQuery("SELECT e FROM Employee e ORDER BY e.salary*2"); + assertQuery("SELECT e FROM Employee e ORDER BY e.salary*2.0"); + assertQuery("SELECT e FROM Employee e ORDER BY e.salary/2"); + assertQuery("SELECT e FROM Employee e ORDER BY e.salary/2.0"); + assertQuery("SELECT e FROM Employee e ORDER BY ABS(e.salary - e.manager.salary)"); + assertQuery("SELECT e FROM Employee e ORDER BY COALESCE(e.salary, 0)"); + assertQuery("SELECT e FROM Employee e ORDER BY CONCAT(e.firstName, ' ', e.lastName)"); + assertQuery("SELECT e FROM Employee e ORDER BY CURRENT_DATE"); + assertQuery("SELECT e FROM Employee e ORDER BY CURRENT_TIME"); + assertQuery("SELECT e FROM Employee e ORDER BY CURRENT_TIMESTAMP"); + assertQuery("SELECT e FROM Employee e ORDER BY LENGTH(e.lastName)"); + assertQuery("SELECT e FROM Employee e ORDER BY LOWER(e.lastName)"); + assertQuery("SELECT e FROM Employee e ORDER BY MOD(e.hoursWorked / 8)"); + assertQuery("SELECT e FROM Employee e ORDER BY NULLIF(e.salary, 0)"); + assertQuery("SELECT e FROM Employee e ORDER BY SQRT(o.RESULT)"); + assertQuery("SELECT e FROM Employee e ORDER BY SUBSTRING(e.lastName, 0, 2)"); + assertQuery("SELECT e FROM Employee e ORDER BY TRIM(TRAILING FROM e.lastName)"); + assertQuery("SELECT e FROM Employee e ORDER BY TRIM(e.lastName)"); + assertQuery("SELECT e FROM Employee e ORDER BY TRIM(LEADING '-' FROM e.lastName)"); + assertQuery("SELECT e FROM Employee e ORDER BY UPPER(e.lastName)"); + assertQuery("SELECT e FROM Employee e ORDER BY CAST(e.salary NUMERIC(10, 2))"); + assertQuery("SELECT e FROM Employee e ORDER BY EXTRACT(YEAR FROM e.startDate)"); + } + + @Test + void functionsInGroupBy() { + + assertQuery("SELECT e FROM Employee e GROUP BY e.salary - 1000"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary + 1000"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary*2"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary*2.0"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary/2"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary/2.0"); + assertQuery("SELECT e FROM Employee e GROUP BY ABS(e.salary - e.manager.salary)"); + assertQuery("SELECT e FROM Employee e GROUP BY COALESCE(e.salary, 0)"); + assertQuery("SELECT e FROM Employee e GROUP BY CONCAT(e.firstName, ' ', e.lastName)"); + assertQuery("SELECT e FROM Employee e GROUP BY CURRENT_DATE"); + assertQuery("SELECT e FROM Employee e GROUP BY CURRENT_TIME"); + assertQuery("SELECT e FROM Employee e GROUP BY CURRENT_TIMESTAMP"); + assertQuery("SELECT e FROM Employee e GROUP BY LENGTH(e.lastName)"); + assertQuery("SELECT e FROM Employee e GROUP BY LOWER(e.lastName)"); + assertQuery("SELECT e FROM Employee e GROUP BY MOD(e.hoursWorked / 8)"); + assertQuery("SELECT e FROM Employee e GROUP BY NULLIF(e.salary, 0)"); + assertQuery("SELECT e FROM Employee e GROUP BY SQRT(o.RESULT)"); + assertQuery("SELECT e FROM Employee e GROUP BY SUBSTRING(e.lastName, 0, 2)"); + assertQuery("SELECT e FROM Employee e GROUP BY TRIM(TRAILING FROM e.lastName)"); + assertQuery("SELECT e FROM Employee e GROUP BY TRIM(e.lastName)"); + assertQuery("SELECT e FROM Employee e GROUP BY TRIM(LEADING '-' FROM e.lastName)"); + assertQuery("SELECT e FROM Employee e GROUP BY UPPER(e.lastName)"); + assertQuery("SELECT e FROM Employee e GROUP BY CAST(e.salary NUMERIC(10, 2))"); + assertQuery("SELECT e FROM Employee e GROUP BY EXTRACT(YEAR FROM e.startDate)"); + } + + @Test + void functionsInHaving() { + + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING e.salary - 1000 > 0"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING e.salary + 1000 > 0"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING e.salary*2 > 0"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING e.salary*2.0 > 0.0"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING e.salary/2 > 0"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING e.salary/2.0 > 0.0"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING ABS(e.salary - e.manager.salary) > 0"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING COALESCE(e.salary, 0) > 0"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING CONCAT(e.firstName, ' ', e.lastName) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING CURRENT_DATE > CURRENT_TIME"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING CURRENT_TIME > CURRENT_TIMESTAMP"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING LENGTH(e.lastName) > 0"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING LOWER(e.lastName) = 'bilbo'"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING MOD(e.hoursWorked / 8) > 0"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING NULLIF(e.salary, 0) is null"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING SQRT(o.RESULT) > 0.0"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING SUBSTRING(e.lastName, 0, 2) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING TRIM(TRAILING FROM e.lastName) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING TRIM(e.lastName) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING TRIM(LEADING '-' FROM e.lastName) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING UPPER(e.lastName) = 'BILBO'"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING CAST(e.salary NUMERIC(10, 2)) > 0.0"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING EXTRACT(YEAR FROM e.startDate) = '2023'"); + } + + @Test + void specialOperators() { + + assertQuery("SELECT toDo FROM Employee e JOIN e.toDoList toDo WHERE INDEX(toDo) = 1"); + assertQuery("SELECT p FROM Employee e JOIN e.priorities p WHERE KEY(p) = 'high'"); + assertQuery("SELECT e FROM Employee e WHERE SIZE(e.managedEmployees) < 2"); + assertQuery("SELECT e FROM Employee e WHERE e.managedEmployees IS EMPTY"); + assertQuery("SELECT e FROM Employee e WHERE 'write code' MEMBER OF e.responsibilities"); + assertQuery("SELECT p FROM Project p WHERE TYPE(p) = LargeProject"); + + /** + * NOTE: The following query has been altered to properly align with EclipseLink test code despite NOT matching + * their ref docs. See https://github.com/eclipse-ee4j/eclipselink/issues/1949 for more details. + */ + assertQuery("SELECT e FROM Employee e JOIN TREAT(e.projects AS LargeProject) p WHERE p.budget > 1000000"); + + assertQuery("SELECT p FROM Phone p WHERE FUNCTION('TO_NUMBER', p.areaCode) > 613"); + } + + @Test + void eclipseLinkSpecialOperators() { + + assertQuery("SELECT p FROM Phone p WHERE FUNC('TO_NUMBER', e.areaCode) > 613"); + assertQuery("SELECT FUNC('YEAR', e.startDate) AS YEAR, COUNT(e) FROM Employee e GROUP BY YEAR"); + assertQuery( + "SELECT a FROM Asset a, Geography geo WHERE geo.id = :id AND a.id IN :id_list AND FUNC('ST_INTERSECTS', a.geometry, geo.geometry) = 'TRUE'"); + assertQuery( + "SELECT s FROM SimpleSpatial s WHERE FUNC('MDSYS.SDO_RELATE', s.jGeometry, :otherGeometry, :params) = 'TRUE' ORDER BY s.id ASC"); + assertQuery("SELECT e FROM Employee e WHERE OPERATOR('ExtractXml', e.resume, '@years-experience') > 10"); + } + + @Test + void sql() { + + assertQuery("SELECT p FROM Phone p WHERE SQL('CAST(? AS CHAR(3))', e.areaCode) = '613'"); + assertQuery("SELECT SQL('EXTRACT(YEAR FROM ?)', e.startDate) AS YEAR, COUNT(e) FROM Employee e GROUP BY YEAR"); + assertQuery("SELECT e FROM Employee e ORDER BY SQL('? NULLS FIRST', e.startDate)"); + assertQuery("SELECT e FROM Employee e WHERE e.startDate = SQL('(SELECT SYSDATE FROM DUAL)')"); + } + + @Test + void column() { + + assertQuery("SELECT e FROM Employee e WHERE COLUMN('MANAGER_ID', e) = :id"); + assertQuery("SELECT e FROM Employee e WHERE COLUMN('ROWID', e) = :id"); + } + + @Test + void table() { + assertQuery( + "SELECT e, a.LAST_UPDATE_USER FROM Employee e, TABLE('AUDIT') a WHERE a.TABLE = 'EMPLOYEE' AND a.ROWID = COLUMN('ROWID', e)"); + } + + @Test // GH-3175 + void coalesceFunctions() { + + assertQuery("SELECT b FROM Bundle b WHERE coalesce(b.deleted, false) AND b.latestImport = true"); + assertQuery("SELECT b FROM Bundle b WHERE NOT coalesce(b.deleted, false) AND b.latestImport = true"); + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java new file mode 100644 index 000000000..c0ff05995 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import static org.assertj.core.api.Assumptions.*; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * TCK Tests for {@link EqlQueryParser} mixed into {@link JpaQueryEnhancer}. + * + * @author Greg Turnquist + */ +public class EqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { + + @Override + QueryEnhancer createQueryEnhancer(DeclaredQuery query) { + + assumeThat(query.isNativeQuery()).isFalse(); + + return JpaQueryEnhancer.forEql(query); + } + + @Override + @ParameterizedTest // GH-2773 + @MethodSource("jpqlCountQueries") + void shouldDeriveJpqlCountQuery(String query, String expected) { + + assumeThat(query).as("EqlParser replaces the column name with alias name for count queries") // + .doesNotContain("SELECT name FROM table_name some_alias"); + + assumeThat(query).as("EqlParser does not support simple JPQL syntax") // + .doesNotStartWithIgnoringCase("FROM"); + + assumeThat(expected).as("EqlParser does turn 'select a.b' into 'select count(a.b)'") // + .doesNotContain("select count(a.b"); + + super.shouldDeriveJpqlCountQuery(query, expected); + } +} 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 new file mode 100644 index 000000000..72635392a --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java @@ -0,0 +1,995 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.jpa.repository.query.JpaQueryParsingToken.*; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +/** + * Tests built around examples of EQL found in the JPA spec + * https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc
+ *
+ * IMPORTANT: Purely verifies the parser without any transformations. + * + * @author Greg Turnquist + */ +class EqlQueryRendererTests { + + private static final String SPEC_FAULT = "Disabled due to spec fault> "; + + /** + * Parse the query using {@link EqlParser} then run it through the query-preserving {@link EqlQueryRenderer}. + * + * @param query + */ + private static String parseWithoutChanges(String query) { + + EqlLexer lexer = new EqlLexer(CharStreams.fromString(query)); + EqlParser parser = new EqlParser(new CommonTokenStream(lexer)); + + parser.addErrorListener(new BadJpqlGrammarErrorListener(query)); + + EqlParser.StartContext parsedQuery = parser.start(); + + return render(new EqlQueryRenderer().visit(parsedQuery)); + } + + private void assertQuery(String query) { + + String slimmedDownQuery = reduceWhitespace(query); + assertThat(parseWithoutChanges(slimmedDownQuery)).isEqualTo(slimmedDownQuery); + } + + private String reduceWhitespace(String original) { + + return original // + .replaceAll("[ \\t\\n]{1,}", " ") // + .trim(); + } + + /** + * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#example + */ + @Test + void joinExample1() { + + assertQuery(""" + SELECT DISTINCT o + FROM Order AS o JOIN o.lineItems AS l + WHERE l.shipped = FALSE + """); + } + + /** + * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#example + * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#identification-variables + */ + @Test + void joinExample2() { + + assertQuery(""" + SELECT DISTINCT o + FROM Order o JOIN o.lineItems l JOIN l.product p + WHERE p.productType = 'office_supplies' + """); + } + + /** + * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#range-variable-declarations + */ + @Test + void rangeVariableDeclarations() { + + assertQuery(""" + SELECT DISTINCT o1 + FROM Order o1, Order o2 + WHERE o1.quantity > o2.quantity AND + o2.customer.lastname = 'Smith' AND + o2.customer.firstname = 'John' + """); + } + + /** + * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#path-expressions + */ + @Test + void pathExpressionsExample1() { + + assertQuery(""" + SELECT i.name, VALUE(p) + FROM Item i JOIN i.photos p + WHERE KEY(p) LIKE '%egret' + """); + } + + /** + * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#path-expressions + */ + @Test + void pathExpressionsExample2() { + + assertQuery(""" + SELECT i.name, p + FROM Item i JOIN i.photos p + WHERE KEY(p) LIKE '%egret' + """); + } + + /** + * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#path-expressions + */ + @Test + void pathExpressionsExample3() { + + assertQuery(""" + SELECT p.vendor + FROM Employee e JOIN e.contactInfo.phones p + """); + } + + /** + * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#path-expressions + */ + @Test + void pathExpressionsExample4() { + + assertQuery(""" + SELECT p.vendor + FROM Employee e JOIN e.contactInfo c JOIN c.phones p + WHERE e.contactInfo.address.zipcode = '95054' + """); + } + + @Test + void pathExpressionSyntaxExample1() { + + assertQuery(""" + SELECT DISTINCT l.product + FROM Order AS o JOIN o.lineItems l + """); + } + + @Test + void joinsExample1() { + + assertQuery(""" + SELECT c FROM Customer c, Employee e WHERE c.hatsize = e.shoesize + """); + } + + @Test + void joinsExample2() { + + assertQuery(""" + SELECT c FROM Customer c JOIN c.orders o WHERE c.status = 1 + """); + } + + @Test + void joinsInnerExample() { + + assertQuery(""" + SELECT c FROM Customer c INNER JOIN c.orders o WHERE c.status = 1 + """); + } + + @Test + void joinsInExample() { + + assertQuery(""" + SELECT OBJECT(c) FROM Customer c, IN(c.orders) o WHERE c.status = 1 + """); + } + + @Test + void doubleJoinExample() { + + assertQuery(""" + SELECT p.vendor + FROM Employee e JOIN e.contactInfo c JOIN c.phones p + WHERE c.address.zipcode = '95054' + """); + } + + @Test + void leftJoinExample() { + + assertQuery(""" + SELECT s.name, COUNT(p) + FROM Suppliers s LEFT JOIN s.products p + GROUP BY s.name + """); + } + + @Test + void leftJoinOnExample() { + + assertQuery(""" + SELECT s.name, COUNT(p) + FROM Suppliers s LEFT JOIN s.products p + ON p.status = 'inStock' + GROUP BY s.name + """); + } + + @Test + void leftJoinWhereExample() { + + assertQuery(""" + SELECT s.name, COUNT(p) + FROM Suppliers s LEFT JOIN s.products p + WHERE p.status = 'inStock' + GROUP BY s.name + """); + } + + @Test + void leftJoinFetchExample() { + + assertQuery(""" + SELECT d + FROM Department d LEFT JOIN FETCH d.employees + WHERE d.deptno = 1 + """); + } + + @Test + void collectionMemberExample() { + + assertQuery(""" + SELECT DISTINCT o + FROM Order o JOIN o.lineItems l + WHERE l.product.productType = 'office_supplies' + """); + } + + @Test + void collectionMemberInExample() { + + assertQuery(""" + SELECT DISTINCT o + FROM Order o, IN(o.lineItems) l + WHERE l.product.productType = 'office_supplies' + """); + } + + @Test + void fromClauseExample() { + + assertQuery(""" + SELECT o + FROM Order AS o JOIN o.lineItems l JOIN l.product p + """); + } + + @Test + void fromClauseDowncastingExample1() { + + assertQuery(""" + SELECT b.name, b.ISBN + FROM Order o JOIN TREAT(o.product AS Book) b + """); + } + + @Test + void fromClauseDowncastingExample2() { + + assertQuery(""" + SELECT e FROM Employee e JOIN TREAT(e.projects AS LargeProject) lp + WHERE lp.budget > 1000 + """); + } + + /** + * @see #fromClauseDowncastingExample3fixed() + */ + @Test + @Disabled(SPEC_FAULT + "Use double-quotes when it should be using single-quotes for a string literal") + void fromClauseDowncastingExample3_SPEC_BUG() { + + assertQuery(""" + SELECT e FROM Employee e JOIN e.projects p + WHERE TREAT(p AS LargeProject).budget > 1000 + OR TREAT(p AS SmallProject).name LIKE 'Persist%' + OR p.description LIKE "cost overrun" + """); + } + + @Test + void fromClauseDowncastingExample3fixed() { + + assertQuery(""" + SELECT e FROM Employee e JOIN e.projects p + WHERE TREAT(p AS LargeProject).budget > 1000 + OR TREAT(p AS SmallProject).name LIKE 'Persist%' + OR p.description LIKE 'cost overrun' + """); + } + + @Test + void fromClauseDowncastingExample4() { + + assertQuery(""" + SELECT e FROM Employee e + WHERE TREAT(e AS Exempt).vacationDays > 10 + OR TREAT(e AS Contractor).hours > 100 + """); + } + + @Test + void pathExpressionsNamedParametersExample() { + + assertQuery(""" + SELECT c + FROM Customer c + WHERE c.status = :stat + """); + } + + @Test + void betweenExpressionsExample() { + + assertQuery(""" + SELECT t + FROM CreditCard c JOIN c.transactionHistory t + WHERE c.holder.name = 'John Doe' AND INDEX(t) BETWEEN 0 AND 9 + """); + } + + @Test + void isEmptyExample() { + + assertQuery(""" + SELECT o + FROM Order o + WHERE o.lineItems IS EMPTY + """); + } + + @Test + void memberOfExample() { + + assertQuery(""" + SELECT p + FROM Person p + WHERE 'Joe' MEMBER OF p.nicknames + """); + } + + @Test + void existsSubSelectExample1() { + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE EXISTS (SELECT spouseEmp + FROM Employee spouseEmp + WHERE spouseEmp = emp.spouse) + """); + } + + @Test + void allExample() { + + assertQuery(""" + SELECT emp + FROM Employee emp + WHERE emp.salary > ALL (SELECT m.salary + FROM Manager m + WHERE m.department = emp.department) + """); + } + + @Test + void existsSubSelectExample2() { + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE EXISTS (SELECT spouseEmp + FROM Employee spouseEmp + WHERE spouseEmp = emp.spouse) + """); + } + + @Test + void subselectNumericComparisonExample1() { + + assertQuery(""" + SELECT c + FROM Customer c + WHERE (SELECT AVG(o.price) FROM c.orders o) > 100 + """); + } + + @Test + void subselectNumericComparisonExample2() { + + assertQuery(""" + SELECT goodCustomer + FROM Customer goodCustomer + WHERE goodCustomer.balanceOwed < (SELECT AVG(c.balanceOwed)/2.0 FROM Customer c) + """); + } + + @Test + void indexExample() { + + assertQuery(""" + SELECT w.name + FROM Course c JOIN c.studentWaitlist w + WHERE c.name = 'Calculus' + AND INDEX(w) = 0 + """); + } + + /** + * @see #functionInvocationExampleWithCorrection() + */ + @Test + @Disabled(SPEC_FAULT + "FUNCTION calls needs a comparator") + void functionInvocationExample_SPEC_BUG() { + + assertQuery(""" + SELECT c + FROM Customer c + WHERE FUNCTION('hasGoodCredit', c.balance, c.creditLimit) + """); + } + + @Test + void functionInvocationExampleWithCorrection() { + + assertQuery(""" + SELECT c + FROM Customer c + WHERE FUNCTION('hasGoodCredit', c.balance, c.creditLimit) = TRUE + """); + } + + @Test + void updateCaseExample1() { + + assertQuery(""" + UPDATE Employee e + SET e.salary = + CASE WHEN e.rating = 1 THEN e.salary*1.1 + WHEN e.rating = 2 THEN e.salary*1.05 + ELSE e.salary*1.01 + END + """); + } + + @Test + void updateCaseExample2() { + + assertQuery(""" + UPDATE Employee e + SET e.salary = + CASE e.rating WHEN 1 THEN e.salary*1.1 + WHEN 2 THEN e.salary*1.05 + ELSE e.salary*1.01 + END + """); + } + + @Test + void selectCaseExample1() { + + assertQuery(""" + SELECT e.name, + CASE TYPE(e) WHEN Exempt THEN 'Exempt' + WHEN Contractor THEN 'Contractor' + WHEN Intern THEN 'Intern' + ELSE 'NonExempt' + END + FROM Employee e + WHERE e.dept.name = 'Engineering' + """); + } + + @Test + void selectCaseExample2() { + + assertQuery(""" + SELECT e.name, + f.name, + CONCAT(CASE WHEN f.annualMiles > 50000 THEN 'Platinum ' + WHEN f.annualMiles > 25000 THEN 'Gold ' + ELSE '' + END, + 'Frequent Flyer') + FROM Employee e JOIN e.frequentFlierPlan f + """); + } + + @Test + void inClauseWithTypeLiteralsShouldWork() { + + assertQuery(""" + SELECT e + FROM Employee e + WHERE TYPE(e) IN (Exempt, Contractor) + """); + } + + @Test + void inClauseWithParametersShouldWork() { + + assertQuery(""" + SELECT e + FROM Employee e + WHERE TYPE(e) IN (:empType1, :empType2) + """); + } + + @Test + void inClauseWithSingleParameterShouldWork() { + + assertQuery(""" + SELECT e + FROM Employee e + WHERE TYPE(e) IN :empTypes + """); + } + + @Test + void notEqualsForTypeShouldWork() { + + assertQuery(""" + SELECT TYPE(e) + FROM Employee e + WHERE TYPE(e) <> Exempt + """); + } + + @Test + void havingWithInClauseShouldWork() { + + assertQuery(""" + SELECT c.status, AVG(c.filledOrderCount), COUNT(c) + FROM Customer c + GROUP BY c.status + HAVING c.status IN (1, 2) + """); + } + + @Test + void havingClauseWithComparisonShouldWork() { + + assertQuery(""" + SELECT c.country, COUNT(c) + FROM Customer c + GROUP BY c.country + HAVING COUNT(c) > 30 + """); + } + + @Test + void havingClauseWithAnotherComparisonShouldWork() { + + assertQuery(""" + SELECT c, COUNT(o) + FROM Customer c JOIN c.orders o + GROUP BY c + HAVING COUNT(o) >= 5 + """); + } + + @Test + void whereClauseWithComparisonShouldWork() { + + assertQuery(""" + SELECT c.id, c.status + FROM Customer c JOIN c.orders o + WHERE o.count > 100 + """); + } + + @Test + void keyValueFunctionsShouldWork() { + + assertQuery(""" + SELECT v.location.street, KEY(i).title, VALUE(i) + FROM VideoStore v JOIN v.videoInventory i + WHERE v.location.zipcode = '94301' AND VALUE(i) > 0 + """); + } + + @Test + void fromClauseWithAsShouldWork() { + + assertQuery(""" + SELECT o.lineItems FROM Order AS o + """); + } + + @Test + void countFunctionWithAsClauseShouldWork() { + + assertQuery(""" + SELECT c, COUNT(l) AS itemCount + FROM Customer c JOIN c.Orders o JOIN o.lineItems l + WHERE c.address.state = 'CA' + GROUP BY c + ORDER BY itemCount + """); + } + + @Test + void objectConstructionShouldWork() { + + assertQuery(""" + SELECT NEW com.acme.example.CustomerDetails(c.id, c.status, o.count) + FROM Customer c JOIN c.orders o + WHERE o.count > 100 + """); + } + + @Test + void selectWithAsClauseShouldWork() { + + assertQuery(""" + SELECT e.address AS addr + FROM Employee e + """); + } + + @Test + void averageFunctionShouldWork() { + + assertQuery(""" + SELECT AVG(o.quantity) FROM Order o + """); + } + + @Test + void sumFunctionShouldWork() { + + assertQuery(""" + SELECT SUM(l.price) + FROM Order o JOIN o.lineItems l JOIN o.customer c + WHERE c.lastname = 'Smith' AND c.firstname = 'John' + """); + } + + @Test + void countFunctionShouldWork() { + + assertQuery(""" + SELECT COUNT(o) FROM Order o + """); + } + + @Test + void countFunctionOnSubElementShouldWork() { + + assertQuery(""" + SELECT COUNT(l.price) + FROM Order o JOIN o.lineItems l JOIN o.customer c + WHERE c.lastname = 'Smith' AND c.firstname = 'John' + """); + } + + @Test + void equivalentCountFunctionShouldAlsoWork() { + + assertQuery(""" + SELECT COUNT(l) + FROM Order o JOIN o.lineItems l JOIN o.customer c + WHERE c.lastname = 'Smith' AND c.firstname = 'John' AND l.price IS NOT NULL + """); + } + + @Test + void orderByBasedOnSelectClauseShouldWork() { + + assertQuery(""" + SELECT o + FROM Customer c JOIN c.orders o JOIN c.address a + WHERE a.state = 'CA' + ORDER BY o.quantity DESC, o.totalcost + """); + } + + @Test + void orderByThatMatchesSelectClauseShouldWork() { + + assertQuery(""" + SELECT o.quantity, a.zipcode + FROM Customer c JOIN c.orders o JOIN c.address a + WHERE a.state = 'CA' + ORDER BY o.quantity, a.zipcode + """); + } + + @Test + void orderByThatMatchesAllSelectAliasesShouldWork() { + + assertQuery(""" + SELECT o.quantity, o.cost*1.08 AS taxedCost, a.zipcode + FROM Customer c JOIN c.orders o JOIN c.address a + WHERE a.state = 'CA' AND a.county = 'Santa Clara' + ORDER BY o.quantity, taxedCost, a.zipcode + """); + } + + @Test + void orderByThatMatchesSelectFunctionAliasShouldWork() { + + assertQuery(""" + SELECT AVG(o.quantity) as q, a.zipcode + FROM Customer c JOIN c.orders o JOIN c.address a + WHERE a.state = 'CA' + GROUP BY a.zipcode + ORDER BY q DESC + """); + } + + /** + * NOTE: This query is specifically dubbed illegal in the spec. However, it's not due to a grammar failure but instead + * for semantic reasons. Our parser does NOT check if the ORDER BY clause matches the SELECT or not. Hence, this is + * left to the JPA provider. + */ + @Test + void orderByClauseThatIsNotReflectedInTheSelectClause() { + + assertQuery(""" + SELECT p.product_name + FROM Order o JOIN o.lineItems l JOIN l.product p JOIN o.customer c + WHERE c.lastname = 'Smith' AND c.firstname = 'John' + ORDER BY p.price + """); + } + + /** + * NOTE: This query is specifically dubbed illegal in the spec. It may actually be failing for a different reason. + */ + @Test + void orderByClauseThatIsNotReflectedInTheSelectClauseButAlsoHasAnInClauseInTheFromClause() { + + assertThatExceptionOfType(BadJpqlGrammarException.class).isThrownBy(() -> { + assertQuery(""" + SELECT p.product_name + FROM Order o, IN(o.lineItems) l JOIN o.customer c + WHERE c.lastname = 'Smith' AND c.firstname = 'John' + ORDER BY o.quantity + """); + }); + } + + @Test + void simpleDeleteShouldWork() { + + assertQuery(""" + DELETE + FROM Customer c + WHERE c.status = 'inactive' + """); + } + + @Test + void deleteWithMoreComplexCriteriaShouldWork() { + + assertQuery(""" + DELETE + FROM Customer c + WHERE c.status = 'inactive' + AND c.orders IS EMPTY + """); + } + + @Test + void simpleUpdateShouldWork() { + + assertQuery(""" + UPDATE Customer c + SET c.status = 'outstanding' + WHERE c.balance < 10000 + """); + } + + @Test + void moreComplexUpdateShouldWork() { + + assertQuery(""" + UPDATE Employee e + SET e.address.building = 22 + WHERE e.address.building = 14 + AND e.address.city = 'Santa Clara' + AND e.project = 'Jakarta EE' + """); + } + + @Test + void simpleSelectShouldWork() { + + assertQuery(""" + SELECT o + FROM Order o + """); + } + + @Test + void selectWithWhereClauseShouldWork() { + + assertQuery(""" + SELECT o + FROM Order o + WHERE o.shippingAddress.state = 'CA' + """); + } + + @Test + void selectWithDistinctSubElementShouldWork() { + + assertQuery(""" + SELECT DISTINCT o.shippingAddress.state + FROM Order o + """); + } + + @Test + void selectWithSimpleDistinctShouldWork() { + + assertQuery(""" + SELECT DISTINCT o + FROM Order o JOIN o.lineItems l + """); + } + + @Test + void selectWithIsNotEmptyCriteriaShouldWork() { + + assertQuery(""" + SELECT o + FROM Order o + WHERE o.lineItems IS NOT EMPTY + """); + } + + @Test + void selectWithIsEmptyCriteriaShouldWork() { + + assertQuery(""" + SELECT o + FROM Order o + WHERE o.lineItems IS EMPTY + """); + } + + @Test + void findAllPendingOrders() { + + assertQuery(""" + SELECT DISTINCT o + FROM Order o JOIN o.lineItems l + WHERE l.shipped = FALSE + """); + } + + @Test + void findAllOrdersWhereShippingAddressDoesNotMatchBillingAddress() { + + assertQuery(""" + SELECT o + FROM Order o + WHERE + NOT (o.shippingAddress.state = o.billingAddress.state AND + o.shippingAddress.city = o.billingAddress.city AND + o.shippingAddress.street = o.billingAddress.street) + """); + } + + @Test + void simplerVersionOfShippingAddressNotMatchingBillingAddress() { + + assertQuery(""" + SELECT o + FROM Order o + WHERE o.shippingAddress <> o.billingAddress + """); + } + + @Test + void findOrdersThatHaveProductNamedByAParameter() { + + assertQuery(""" + SELECT DISTINCT o + FROM Order o JOIN o.lineItems l + WHERE l.product.name = ?1 + """); + } + + @Test // GH-2982 + void floorShouldBeValidEntityName() { + + assertQuery(""" + SELECT f + FROM Floor f + WHERE f.name = :name + """); + + assertQuery(""" + SELECT r + FROM Room r + JOIN r.floor f + WHERE f.name = :name + """); + } + + @Test // GH-2994 + void queryWithSignShouldWork() { + assertQuery("select t.sign from TestEntity t"); + } + + @Test // GH-3028 + void queryWithValueShouldWork() { + assertQuery("select t.value from TestEntity t"); + } + + @Test // GH-3062, GH-3056 + void typeShouldBeAValidParameter() { + + assertQuery("select e from Employee e where e.type = :_type"); + assertQuery("select te from TestEntity te where te.type = :type"); + } + + @Test // GH-3061 + void alternateNotEqualsOperatorShouldWork() { + assertQuery("select e from Employee e where e.firstName != :name"); + } + + @Test // GH-3092 + void dateAndFromShouldBeValidNames() { + assertQuery("SELECT e FROM Entity e WHERE e.embeddedId.date BETWEEN :from AND :to"); + } + + @Test // GH-3092 + void timeShouldBeAValidParameterName() { + assertQuery(""" + UPDATE Lock L + SET L.isLocked = TRUE, L.forceUnlockTime = :forceUnlockTime + WHERE L.isLocked = FALSE OR L.forceUnlockTime < :time + """); + } + + @Test // GH-3128 + void newShouldBeLegalAsPartOfAStateFieldPathExpression() { + + assertQuery(""" + SELECT j + FROM AgentUpdateTask j + WHERE j.creationTimestamp < :date + AND (j.status = com.ca.apm.acc.configserver.core.domain.jobs.AgentUpdateTaskStatus.NEW + OR + j.status = com.ca.apm.acc.configserver.core.domain.jobs.AgentUpdateTaskStatus.STARTED + OR + j.status = com.ca.apm.acc.configserver.core.domain.jobs.AgentUpdateTaskStatus.QUEUED) + ORDER BY j.id + """); + } + + @Test // GH-3143 + void powerShouldBeLegalInAQuery() { + assertQuery("select e.power.id from MyEntity e"); + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java new file mode 100644 index 000000000..5ec8f08cb --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java @@ -0,0 +1,783 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import static org.assertj.core.api.Assertions.*; + +import java.util.stream.Stream; + +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.JpaSort; +import org.springframework.lang.Nullable; + +/** + * Verify that EQL queries are properly transformed through the {@link JpaQueryEnhancer} and the {@link EqlQueryParser}. + * + * @author Greg Turnquist + */ +class EqlQueryTransformerTests { + + private static final String QUERY = "select u from User u"; + private static final String SIMPLE_QUERY = "select u from User u"; + private static final String COUNT_QUERY = "select count(u) from User u"; + private static final String QUERY_WITH_AS = "select u from User as u where u.username = ?1"; + + @Test + void applyingSortShouldIntroduceOrderByCriteriaWhereNoneExists() { + + // given + var original = "SELECT e FROM Employee e where e.name = :name"; + var sort = Sort.by("first_name", "last_name"); + + // when + var results = createQueryFor(original, sort); + + // then + assertThat(original).doesNotContainIgnoringCase("order by"); + assertThat(results).contains("order by e.first_name asc, e.last_name asc"); + } + + @Test + void applyingSortShouldCreateAdditionalOrderByCriteria() { + + // given + var original = "SELECT e FROM Employee e where e.name = :name ORDER BY e.role, e.hire_date"; + var sort = Sort.by("first_name", "last_name"); + + // when + var results = createQueryFor(original, sort); + + // then + assertThat(results).contains("ORDER BY e.role, e.hire_date, e.first_name asc, e.last_name asc"); + } + + @Test + void applyCountToSimpleQuery() { + + // given + var original = "SELECT e FROM Employee e where e.name = :name"; + + // when + var results = createCountQueryFor(original); + + // then + assertThat(results).isEqualTo("SELECT count(e) FROM Employee e where e.name = :name"); + } + + @Test + void applyCountToMoreComplexQuery() { + + // given + var original = "SELECT e FROM Employee e where e.name = :name ORDER BY e.modified_date"; + + // when + var results = createCountQueryFor(original); + + // then + assertThat(results).isEqualTo("SELECT count(e) FROM Employee e where e.name = :name"); + } + + @Test + void applyCountToAlreadySorteQuery() { + + // given + var original = "SELECT e FROM Employee e where e.name = :name ORDER BY e.modified_date"; + + // when + var results = createCountQueryFor(original); + + // then + assertThat(results).isEqualTo("SELECT count(e) FROM Employee e where e.name = :name"); + } + + @Test + void multipleAliasesShouldBeGathered() { + + // given + var original = "select e from Employee e join e.manager m"; + + // when + var results = createQueryFor(original, Sort.unsorted()); + + // then + assertThat(results).isEqualTo("select e from Employee e join e.manager m"); + } + + @Test + void createsCountQueryCorrectly() { + assertCountQuery(QUERY, COUNT_QUERY); + } + + @Test + void createsCountQueriesCorrectlyForCapitalLetterEQL() { + + assertCountQuery("select u FROM User u WHERE u.foo.bar = ?1", "select count(u) FROM User u WHERE u.foo.bar = ?1"); + assertCountQuery("SELECT u FROM User u where u.foo.bar = ?1", "SELECT count(u) FROM User u where u.foo.bar = ?1"); + } + + @Test + void createsCountQueryForDistinctQueries() { + + assertCountQuery("select distinct u from User u where u.foo = ?1", + "select count(distinct u) from User u where u.foo = ?1"); + } + + @Test + void createsCountQueryForConstructorQueries() { + + assertCountQuery("select distinct new com.example.User(u.name) from User u where u.foo = ?1", + "select count(distinct u) from User u where u.foo = ?1"); + } + + @Test + void createsCountQueryForJoins() { + + assertCountQuery("select distinct new com.User(u.name) from User u left outer join u.roles r WHERE r = ?1", + "select count(distinct u) from User u left outer join u.roles r WHERE r = ?1"); + } + + @Test + void createsCountQueryForQueriesWithSubSelects() { + + assertCountQuery("select u from User u left outer join u.roles r where r in (select r from Role r)", + "select count(u) from User u left outer join u.roles r where r in (select r from Role r)"); + } + + @Test + void createsCountQueryForAliasesCorrectly() { + assertCountQuery("select u from User as u", "select count(u) from User as u"); + } + + @Test + void allowsShortJpaSyntax() { + assertCountQuery(SIMPLE_QUERY, COUNT_QUERY); + } + + @Test // GH-2260 + void detectsAliasCorrectly() { + + assertThat(alias(QUERY)).isEqualTo("u"); + assertThat(alias(SIMPLE_QUERY)).isEqualTo("u"); + assertThat(alias(COUNT_QUERY)).isEqualTo("u"); + assertThat(alias(QUERY_WITH_AS)).isEqualTo("u"); + assertThat(alias("SELECT u FROM USER U")).isEqualTo("U"); + assertThat(alias("select u from User u")).isEqualTo("u"); + assertThat(alias("select new com.acme.UserDetails(u.id, u.name) from User u")).isEqualTo("u"); + assertThat(alias("select u from T05User u")).isEqualTo("u"); + assertThat(alias("select u from User u where not exists (select m from User m where m = u.manager) ")) + .isEqualTo("u"); + assertThat(alias("select u from User u where not exists (select u2 from User u2)")).isEqualTo("u"); + assertThat(alias( + "select u from User u where not exists (select u2 from User u2 where not exists (select u3 from User u3))")) + .isEqualTo("u"); + } + + @Test // GH-2557 + void applySortingAccountsForNewlinesInSubselect() { + + Sort sort = Sort.by(Sort.Order.desc("age")); + + assertThat(newParser(""" + select u + from user u + where exists (select u2 + from user u2 + ) + """).applySorting(sort)).isEqualToIgnoringWhitespace(""" + select u + from user u + where exists (select u2 + from user u2 + ) + order by u.age desc"""); + } + + @Test // GH-2563 + void aliasDetectionProperlyHandlesNewlinesInSubselects() { + + assertThat(alias(""" + SELECT o + FROM Order o + WHERE EXISTS( SELECT 1 + FROM Vehicle vehicle + WHERE vehicle.vehicleOrderId = o.id + AND LOWER(COALESCE(vehicle.make, '')) LIKE :query) + """)).isEqualTo("o"); + } + + @Test // DATAJPA-252, GH-664, GH-1066, GH-2960 + void doesNotPrefixOrderReferenceIfOuterJoinAliasDetected() { + + String query = "select p from Person p left join p.address address"; + Sort sort = Sort.by("address.city"); + assertThat(createQueryFor(query, sort)).endsWith("order by address.city asc"); + } + + @Test // DATAJPA-252 + void extendsExistingOrderByClausesCorrectly() { + + String query = "select p from Person p order by p.lastname asc"; + Sort sort = Sort.by("firstname"); + assertThat(createQueryFor(query, sort)).endsWith("order by p.lastname asc, p.firstname asc"); + } + + @Test // DATAJPA-296 + void appliesIgnoreCaseOrderingCorrectly() { + + String query = "select p from Person p"; + Sort sort = Sort.by(Sort.Order.by("firstname").ignoreCase()); + + assertThat(createQueryFor(query, sort)).endsWith("order by lower(p.firstname) asc"); + } + + @Test // DATAJPA-296 + void appendsIgnoreCaseOrderingCorrectly() { + + String query = "select p from Person p order by p.lastname asc"; + Sort sort = Sort.by(Sort.Order.by("firstname").ignoreCase()); + + assertThat(createQueryFor(query, sort)) + .isEqualTo("select p from Person p order by p.lastname asc, lower(p.firstname) asc"); + } + + @Test // DATAJPA-342 + void usesReturnedVariableInCountProjectionIfSet() { + + assertCountQuery("select distinct m.genre from Media m where m.user = ?1 order by m.genre asc", + "select count(distinct m.genre) from Media m where m.user = ?1"); + } + + @Test // DATAJPA-343 + void projectsCountQueriesForQueriesWithSubselects() { + + // given + var original = "select o from Foo o where cb.id in (select b from Bar b)"; + + // when + var results = createQueryFor(original, Sort.by("first_name", "last_name")); + + // then + assertThat(results).isEqualTo( + "select o from Foo o where cb.id in (select b from Bar b) order by o.first_name asc, o.last_name asc"); + + assertCountQuery("select o from Foo o where cb.id in (select b from Bar b)", + "select count(o) from Foo o where cb.id in (select b from Bar b)"); + } + + @Test // DATAJPA-148 + void doesNotPrefixSortsIfFunction() { + + Sort sort = Sort.by("sum(foo)"); + + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) + .isThrownBy(() -> createQueryFor("select p from Person p", sort)); + } + + @Test // DATAJPA-377 + void removesOrderByInGeneratedCountQueryFromOriginalQueryIfPresent() { + + assertCountQuery("select distinct m.genre from Media m where m.user = ?1 OrDer By m.genre ASC", + "select count(distinct m.genre) from Media m where m.user = ?1"); + } + + @Test // DATAJPA-375 + void findsExistingOrderByIndependentOfCase() { + + Sort sort = Sort.by("lastname"); + String query = createQueryFor("select p from Person p ORDER BY p.firstname", sort); + assertThat(query).endsWith("ORDER BY p.firstname, p.lastname asc"); + } + + @Test // DATAJPA-409 + void createsCountQueryForNestedReferenceCorrectly() { + assertCountQuery("select a.b from A a", "select count(a) from A a"); + } + + @Test // DATAJPA-420 + void createsCountQueryForScalarSelects() { + assertCountQuery("select p.lastname,p.firstname from Person p", "select count(p) from Person p"); + } + + @Test // DATAJPA-456 + void createCountQueryFromTheGivenCountProjection() { + + assertThat(createCountQueryFor("select p.lastname,p.firstname from Person p", "p.lastname")) + .isEqualTo("select count(p.lastname) from Person p"); + } + + @Test // DATAJPA-736 + void supportsNonAsciiCharactersInEntityNames() { + assertThat(createCountQueryFor("select u from Usèr u")).isEqualTo("select count(u) from Usèr u"); + } + + @Test // DATAJPA-798 + void detectsAliasInQueryContainingLineBreaks() { + assertThat(alias("select \n u \n from \n User \nu")).isEqualTo("u"); + } + + @Test // DATAJPA-938 + void detectsConstructorExpressionInDistinctQuery() { + assertThat(hasConstructorExpression("select distinct new com.example.Foo(b.name) from Bar b")).isTrue(); + } + + @Test // DATAJPA-938 + void detectsComplexConstructorExpression() { + + // + assertThat(hasConstructorExpression( + """ + select new foo.bar.Foo(ip.id, ip.name, sum(lp.amount)) + from Bar lp join lp.investmentProduct ip + where (lp.toDate is null and lp.fromDate <= :now and lp.fromDate is not null) and lp.accountId = :accountId group by ip.id, ip.name, lp.accountId + order by ip.name ASC""")) + .isTrue(); + } + + @Test // DATAJPA-938 + void detectsConstructorExpressionWithLineBreaks() { + assertThat(hasConstructorExpression("select new foo.bar.FooBar(\na.id) from DtoA a ")).isTrue(); + } + + @Test // DATAJPA-965, DATAJPA-970 + void doesNotAllowWhitespaceInSort() { + + Sort sort = Sort.by("case when foo then bar"); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) + .isThrownBy(() -> createQueryFor("select p from Person p", sort)); + } + + @Test // DATAJPA-965, DATAJPA-970 + void doesNotPrefixUnsafeJpaSortFunctionCalls() { + + JpaSort sort = JpaSort.unsafe("sum(foo)"); + assertThat(createQueryFor("select p from Person p", sort)).endsWith("order by sum(foo) asc"); + } + + @Test // DATAJPA-965, DATAJPA-970, GH-2863 + void doesNotPrefixMultipleAliasedFunctionCalls() { + + String query = "SELECT AVG(m.price) AS avgPrice, SUM(m.stocks) AS sumStocks FROM Magazine m"; + Sort sort = Sort.by("avgPrice", "sumStocks"); + + assertThat(createQueryFor(query, sort)).endsWith("order by avgPrice asc, sumStocks asc"); + } + + @Test // DATAJPA-965, DATAJPA-970, GH-2863 + void doesNotPrefixSingleAliasedFunctionCalls() { + + String query = "SELECT AVG(m.price) AS avgPrice FROM Magazine m"; + Sort sort = Sort.by("avgPrice"); + + assertThat(createQueryFor(query, sort)).endsWith("order by avgPrice asc"); + } + + @Test // DATAJPA-965, DATAJPA-970, GH-2863 + void prefixesSingleNonAliasedFunctionCallRelatedSortProperty() { + + String query = "SELECT AVG(m.price) AS avgPrice FROM Magazine m"; + Sort sort = Sort.by("someOtherProperty"); + + assertThat(createQueryFor(query, sort)).endsWith("order by m.someOtherProperty asc"); + } + + @Test // DATAJPA-965, DATAJPA-970, GH-2863 + void prefixesNonAliasedFunctionCallRelatedSortPropertyWhenSelectClauseContainsAliasedFunctionForDifferentProperty() { + + String query = "SELECT m.name, AVG(m.price) AS avgPrice FROM Magazine m"; + Sort sort = Sort.by("name", "avgPrice"); + + assertThat(createQueryFor(query, sort)).endsWith("order by m.name asc, avgPrice asc"); + } + + @Test // DATAJPA-965, DATAJPA-970, GH-2863 + void doesNotPrefixAliasedFunctionCallNameWithMultipleNumericParameters() { + + String query = "SELECT SUBSTRING(m.name, 2, 5) AS trimmedName FROM Magazine m"; + Sort sort = Sort.by("trimmedName"); + + assertThat(createQueryFor(query, sort)).endsWith("order by trimmedName asc"); + } + + @Test // DATAJPA-965, DATAJPA-970, GH-2863 + void doesNotPrefixAliasedFunctionCallNameWithMultipleStringParameters() { + + String query = "SELECT CONCAT(m.name, 'foo') AS extendedName FROM Magazine m"; + Sort sort = Sort.by("extendedName"); + + assertThat(createQueryFor(query, sort)).endsWith("order by extendedName asc"); + } + + @Test // DATAJPA-965, DATAJPA-970, GH-2863 + void doesNotPrefixAliasedFunctionCallNameWithUnderscores() { + + String query = "SELECT AVG(m.price) AS avg_price FROM Magazine m"; + Sort sort = Sort.by("avg_price"); + + assertThat(createQueryFor(query, sort)).endsWith("order by avg_price asc"); + } + + @Test // DATAJPA-965, DATAJPA-970, GH-2863 + void doesNotPrefixAliasedFunctionCallNameWithDots() { + + String query = "SELECT AVG(m.price) AS m.avg FROM Magazine m"; + Sort sort = Sort.by("m.avg"); + + assertThatIllegalArgumentException().isThrownBy(() -> createQueryFor(query, sort)); + } + + @Test // DATAJPA-965, DATAJPA-970, GH-2863 + void doesNotPrefixAliasedFunctionCallNameWhenQueryStringContainsMultipleWhiteSpaces() { + + String query = "SELECT AVG( m.price ) AS avgPrice FROM Magazine m"; + Sort sort = Sort.by("avgPrice"); + + assertThat(createQueryFor(query, sort)).endsWith("order by avgPrice asc"); + } + + @Test // DATAJPA-1506 + void detectsAliasWithGroupAndOrderBy() { + + assertThat(alias("select * from User group by name")).isNull(); + assertThat(alias("select * from User order by name")).isNull(); + assertThat(alias("select u from User u group by name")).isEqualTo("u"); + assertThat(alias("select u from User u order by name")).isEqualTo("u"); + } + + @Test // DATAJPA-1500 + void createCountQuerySupportsWhitespaceCharacters() { + + assertThat(createCountQueryFor(""" + select user from User user + where user.age = 18 + order by user.name + """)).isEqualToIgnoringWhitespace(""" + select count(user) from User user + where user.age = 18 + """); + } + + @Test + void createCountQuerySupportsLineBreaksInSelectClause() { + + assertThat(createCountQueryFor(""" + select user.age, + user.name + from User user + where user.age = 18 + order + by + user.name + """)).isEqualToIgnoringWhitespace(""" + select count(user) from User user + where user.age = 18 + """); + } + + @Test // DATAJPA-1061 + void appliesSortCorrectlyForFieldAliases() { + + String query = "SELECT m.price, lower(m.title) AS title, a.name as authorName FROM Magazine m INNER JOIN m.author a"; + Sort sort = Sort.by("authorName"); + + String fullQuery = createQueryFor(query, sort); + + assertThat(fullQuery).endsWith("order by authorName asc"); + } + + @Test // GH-2280 + void appliesOrderingCorrectlyForFieldAliasWithIgnoreCase() { + + String query = "SELECT customer.id as id, customer.name as name FROM CustomerEntity customer"; + Sort sort = Sort.by(Sort.Order.by("name").ignoreCase()); + + String fullQuery = createQueryFor(query, sort); + + assertThat(fullQuery).isEqualTo( + "SELECT customer.id as id, customer.name as name FROM CustomerEntity customer order by lower(name) asc"); + } + + @Test // DATAJPA-1061 + void appliesSortCorrectlyForFunctionAliases() { + + String query = "SELECT m.price, lower(m.title) AS title, a.name as authorName FROM Magazine m INNER JOIN m.author a"; + Sort sort = Sort.by("title"); + + String fullQuery = createQueryFor(query, sort); + + assertThat(fullQuery).endsWith("order by title asc"); + } + + @Test // DATAJPA-1061 + void appliesSortCorrectlyForSimpleField() { + + String query = "SELECT m.price, lower(m.title) AS title, a.name as authorName FROM Magazine m INNER JOIN m.author a"; + Sort sort = Sort.by("price"); + + String fullQuery = createQueryFor(query, sort); + + assertThat(fullQuery).endsWith("order by m.price asc"); + } + + @Test + void createCountQuerySupportsLineBreakRightAfterDistinct() { + + assertThat(createCountQueryFor(""" + select + distinct + user.age, + user.name + from + User + user""")).isEqualTo(createCountQueryFor(""" + select + distinct user.age, + user.name + from + User + user""")); + } + + @Test + void detectsAliasWithGroupAndOrderByWithLineBreaks() { + + assertThat(alias("select * from User group\nby name")).isNull(); + assertThat(alias("select * from User order\nby name")).isNull(); + assertThat(alias("select u from User u group\nby name")).isEqualTo("u"); + assertThat(alias("select u from User u order\nby name")).isEqualTo("u"); + assertThat(alias("select u from User\nu\norder \n by name")).isEqualTo("u"); + } + + @Test // DATAJPA-1679 + void findProjectionClauseWithDistinct() { + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(projection("select a,b,c from Entity x")).isEqualTo("a, b, c"); + softly.assertThat(projection("select a, b, c from Entity x")).isEqualTo("a, b, c"); + softly.assertThat(projection("select distinct a, b, c from Entity x")).isEqualTo("a, b, c"); + softly.assertThat(projection("select DISTINCT a, b, c from Entity x")).isEqualTo("a, b, c"); + }); + } + + @Test // DATAJPA-1696 + void findProjectionClauseWithSubselect() { + + // This is not a required behavior, in fact the opposite is, + // but it documents a current limitation. + // to fix this without breaking findProjectionClauseWithIncludedFrom we need a more sophisticated parser. + assertThat(projection("select * from (select x from y)")).isNotEqualTo("*"); + } + + @Test // DATAJPA-1696 + void findProjectionClauseWithIncludedFrom() { + assertThat(projection("select x, frommage, y from Element t")).isEqualTo("x, frommage, y"); + } + + @Test // GH-2341 + void countProjectionDistrinctQueryIncludesNewLineAfterFromAndBeforeJoin() { + + String originalQuery = "SELECT DISTINCT entity1\nFROM Entity1 entity1\nLEFT JOIN entity1.entity2 entity2 ON entity1.key = entity2.key"; + assertCountQuery(originalQuery, + "SELECT count(DISTINCT entity1) FROM Entity1 entity1 LEFT JOIN entity1.entity2 entity2 ON entity1.key = entity2.key"); + } + + @Test // GH-2341 + void countProjectionDistinctQueryIncludesNewLineAfterEntity() { + + String originalQuery = "SELECT DISTINCT entity1\nFROM Entity1 entity1 LEFT JOIN entity1.entity2 entity2 ON entity1.key = entity2.key"; + assertCountQuery(originalQuery, + "SELECT count(DISTINCT entity1) FROM Entity1 entity1 LEFT JOIN entity1.entity2 entity2 ON entity1.key = entity2.key"); + } + + @Test // GH-2341 + void countProjectionDistinctQueryIncludesNewLineAfterEntityAndBeforeWhere() { + + String originalQuery = "SELECT DISTINCT entity1\nFROM Entity1 entity1 LEFT JOIN entity1.entity2 entity2 ON entity1.key = entity2.key\nwhere entity1.id = 1799"; + assertCountQuery(originalQuery, + "SELECT count(DISTINCT entity1) FROM Entity1 entity1 LEFT JOIN entity1.entity2 entity2 ON entity1.key = entity2.key where entity1.id = 1799"); + } + + @Test // GH-2393 + void createCountQueryStartsWithWhitespace() { + + assertThat(createCountQueryFor(" \nselect u from User u where u.age > :age")) + .isEqualTo("select count(u) from User u where u.age > :age"); + + assertThat(createCountQueryFor(" \nselect u from User u where u.age > :age")) + .isEqualTo("select count(u) from User u where u.age > :age"); + } + + @Test // GH-2260 + void applySortingAccountsForNativeWindowFunction() { + + Sort sort = Sort.by(Sort.Order.desc("age")); + + // order by absent + assertThat(createQueryFor("select u from user u", sort)).isEqualTo("select u from user u order by u.age desc"); + + // order by present + assertThat(createQueryFor("select u from user u order by u.lastname", sort)) + .isEqualTo("select u from user u order by u.lastname, u.age desc"); + } + + @Test // GH-2511 + void countQueryUsesCorrectVariable() { + + assertThat(createCountQueryFor("SELECT e FROM User e WHERE created_at > $1")) + .isEqualTo("SELECT count(e) FROM User e WHERE created_at > $1"); + + assertThat( + createCountQueryFor("SELECT t FROM mytable t WHERE nr = :number AND kon = :kon AND datum >= '2019-01-01'")) + .isEqualTo("SELECT count(t) FROM mytable t WHERE nr = :number AND kon = :kon AND datum >= '2019-01-01'"); + + assertThat(createCountQueryFor("select s FROM users_statuses s WHERE (user_created_at BETWEEN $1 AND $2)")) + .isEqualTo("select count(s) FROM users_statuses s WHERE (user_created_at BETWEEN $1 AND $2)"); + + assertThat( + createCountQueryFor("SELECT us FROM users_statuses us WHERE (user_created_at BETWEEN :fromDate AND :toDate)")) + .isEqualTo("SELECT count(us) FROM users_statuses us WHERE (user_created_at BETWEEN :fromDate AND :toDate)"); + } + + @Test // GH-2496, GH-2522, GH-2537, GH-2045 + void orderByShouldWorkWithSubSelectStatements() { + + Sort sort = Sort.by(Sort.Order.desc("age")); + + assertThat(createQueryFor(""" + select r + From DataRecord r + where + ( + r.adusrId = :userId + or EXISTS( select 1 FROM DataRecordDvsRight dr WHERE dr.adusrId = :userId AND dr.dataRecord = r ) + )""", sort)).endsWith("order by r.age desc"); + + assertThat(createQueryFor(""" + select distinct u + from FooBar u + where u.role = 'redacted' + and ( + not exists ( + select g from FooBarGroup g + where g in :excludedGroups + ) + )""", sort)).endsWith("order by u.age desc"); + + assertThat(createQueryFor(""" + SELECT i + FROM Item i + WHERE i.id IN ( + SELECT max(i2.id) FROM Item i2 + WHERE i2.field.id = :fieldId + GROUP BY i2.field.id, i2.version)""", sort)).endsWith("order by i.age desc"); + } + + @Test // GH-2074 + void queryParserPicksCorrectAliasAmidstMultipleAliases() { + assertThat(alias("select u from User as u left join u.roles as r")).isEqualTo("u"); + } + + @ParameterizedTest + @MethodSource("queriesWithReservedWordsAsIdentifiers") // GH-2864 + void usingReservedWordAsRelationshipNameShouldWork(String relationshipName, String joinAlias) { + + EqlQueryParser.parseQuery(String.format(""" + select u + from UserAccountEntity u + join u.lossInspectorLimitConfiguration lil + join u.companyTeam ct + where exists ( + select iu + from UserAccountEntity iu + join iu.roles u2r + join u2r.role r + join r.rights r2r + join r2r.%s %s + where + %s.code = :rightCode + and iu = u + ) + and ct.id = :teamId + """, relationshipName, joinAlias, joinAlias)); + } + + @Test // GH-664, GH-1066, GH-2960 + void sortingRecognizesJoinAliases() { + + String query = "select p from Customer c join c.productOrder p where p.delayed = true"; + + assertThat(createQueryFor(query, Sort.by(Sort.Order.desc("lastName")))).isEqualToIgnoringWhitespace(""" + select p from Customer c + join c.productOrder p + where p.delayed = true + order by c.lastName desc + """); + + assertThat(createQueryFor(query, Sort.by(Sort.Order.desc("p.lineItems")))).isEqualToIgnoringWhitespace(""" + select p from Customer c + join c.productOrder p + where p.delayed = true + order by p.lineItems desc + """); + } + + static Stream queriesWithReservedWordsAsIdentifiers() { + + return Stream.of( // + Arguments.of("right", "rt"), // + Arguments.of("left", "lt"), // + Arguments.of("outer", "ou"), // + Arguments.of("full", "full"), // + Arguments.of("inner", "inr")); + } + + private void assertCountQuery(String originalQuery, String countQuery) { + assertThat(createCountQueryFor(originalQuery)).isEqualTo(countQuery); + } + + private String createQueryFor(String query, Sort sort) { + return newParser(query).applySorting(sort); + } + + private String createCountQueryFor(String query) { + return createCountQueryFor(query, null); + } + + private String createCountQueryFor(String original, @Nullable String countProjection) { + return newParser(original).createCountQueryFor(countProjection); + } + + private String alias(String query) { + return newParser(query).detectAlias(); + } + + private boolean hasConstructorExpression(String query) { + return newParser(query).hasConstructorExpression(); + } + + private String projection(String query) { + return newParser(query).getProjection(); + } + + private QueryEnhancer newParser(String query) { + return JpaQueryEnhancer.forEql(DeclaredQuery.of(query, false)); + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlSpecificationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlSpecificationTests.java new file mode 100644 index 000000000..81f2d565f --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlSpecificationTests.java @@ -0,0 +1,887 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +/** + * Tests built around examples of EQL found in the JPA spec + * https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc
+ *
+ * IMPORTANT: Purely verifies the parser without any transformations. + * + * @author Greg Turnquist + */ +class EqlSpecificationTests { + + private static final String SPEC_FAULT = "Disabled due to spec fault> "; + + /** + * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#example + */ + @Test + void joinExample1() { + + EqlQueryParser.parseQuery(""" + SELECT DISTINCT o + FROM Order AS o JOIN o.lineItems AS l + WHERE l.shipped = FALSE + """); + } + + /** + * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#example + * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#identification-variables + */ + @Test + void joinExample2() { + + EqlQueryParser.parseQuery(""" + SELECT DISTINCT o + FROM Order o JOIN o.lineItems l JOIN l.product p + WHERE p.productType = 'office_supplies' + """); + } + + /** + * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#range-variable-declarations + */ + @Test + void rangeVariableDeclarations() { + + EqlQueryParser.parseQuery(""" + SELECT DISTINCT o1 + FROM Order o1, Order o2 + WHERE o1.quantity > o2.quantity AND + o2.customer.lastname = 'Smith' AND + o2.customer.firstname= 'John' + """); + } + + /** + * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#path-expressions + */ + @Test + void pathExpressionsExample1() { + + EqlQueryParser.parseQuery(""" + SELECT i.name, VALUE(p) + FROM Item i JOIN i.photos p + WHERE KEY(p) LIKE '%egret' + """); + } + + /** + * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#path-expressions + */ + @Test + void pathExpressionsExample2() { + + EqlQueryParser.parseQuery(""" + SELECT i.name, p + FROM Item i JOIN i.photos p + WHERE KEY(p) LIKE '%egret' + """); + } + + /** + * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#path-expressions + */ + @Test + void pathExpressionsExample3() { + + EqlQueryParser.parseQuery(""" + SELECT p.vendor + FROM Employee e JOIN e.contactInfo.phones p + """); + } + + /** + * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#path-expressions + */ + @Test + void pathExpressionsExample4() { + + EqlQueryParser.parseQuery(""" + SELECT p.vendor + FROM Employee e JOIN e.contactInfo c JOIN c.phones p + WHERE e.contactInfo.address.zipcode = '95054' + """); + } + + @Test + void pathExpressionSyntaxExample1() { + + EqlQueryParser.parseQuery(""" + SELECT DISTINCT l.product + FROM Order AS o JOIN o.lineItems l + """); + } + + @Test + void joinsExample1() { + + EqlQueryParser.parseQuery(""" + SELECT c FROM Customer c, Employee e WHERE c.hatsize = e.shoesize + """); + } + + @Test + void joinsExample2() { + + EqlQueryParser.parseQuery(""" + SELECT c FROM Customer c JOIN c.orders o WHERE c.status = 1 + """); + } + + @Test + void joinsInnerExample() { + + EqlQueryParser.parseQuery(""" + SELECT c FROM Customer c INNER JOIN c.orders o WHERE c.status = 1 + """); + } + + @Test + void joinsInExample() { + + EqlQueryParser.parseQuery(""" + SELECT OBJECT(c) FROM Customer c, IN(c.orders) o WHERE c.status = 1 + """); + } + + @Test + void doubleJoinExample() { + + EqlQueryParser.parseQuery(""" + SELECT p.vendor + FROM Employee e JOIN e.contactInfo c JOIN c.phones p + WHERE c.address.zipcode = '95054' + """); + } + + @Test + void leftJoinExample() { + + EqlQueryParser.parseQuery(""" + SELECT s.name, COUNT(p) + FROM Suppliers s LEFT JOIN s.products p + GROUP BY s.name + """); + } + + @Test + void leftJoinOnExample() { + + EqlQueryParser.parseQuery(""" + SELECT s.name, COUNT(p) + FROM Suppliers s LEFT JOIN s.products p + ON p.status = 'inStock' + GROUP BY s.name + """); + } + + @Test + void leftJoinWhereExample() { + + EqlQueryParser.parseQuery(""" + SELECT s.name, COUNT(p) + FROM Suppliers s LEFT JOIN s.products p + WHERE p.status = 'inStock' + GROUP BY s.name + """); + } + + @Test + void leftJoinFetchExample() { + + EqlQueryParser.parseQuery(""" + SELECT d + FROM Department d LEFT JOIN FETCH d.employees + WHERE d.deptno = 1 + """); + } + + @Test + void collectionMemberExample() { + + EqlQueryParser.parseQuery(""" + SELECT DISTINCT o + FROM Order o JOIN o.lineItems l + WHERE l.product.productType = 'office_supplies' + """); + } + + @Test + void collectionMemberInExample() { + + EqlQueryParser.parseQuery(""" + SELECT DISTINCT o + FROM Order o, IN(o.lineItems) l + WHERE l.product.productType = 'office_supplies' + """); + } + + @Test + void fromClauseExample() { + + EqlQueryParser.parseQuery(""" + SELECT o + FROM Order AS o JOIN o.lineItems l JOIN l.product p + """); + } + + @Test + void fromClauseDowncastingExample1() { + + EqlQueryParser.parseQuery(""" + SELECT b.name, b.ISBN + FROM Order o JOIN TREAT(o.product AS Book) b + """); + } + + @Test + void fromClauseDowncastingExample2() { + + EqlQueryParser.parseQuery(""" + SELECT e FROM Employee e JOIN TREAT(e.projects AS LargeProject) lp + WHERE lp.budget > 1000 + """); + } + + /** + * @see #fromClauseDowncastingExample3fixed() + */ + @Test + @Disabled(SPEC_FAULT + "Use double-quotes when it should be using single-quotes for a string literal") + void fromClauseDowncastingExample3_SPEC_BUG() { + + EqlQueryParser.parseQuery(""" + SELECT e FROM Employee e JOIN e.projects p + WHERE TREAT(p AS LargeProject).budget > 1000 + OR TREAT(p AS SmallProject).name LIKE 'Persist%' + OR p.description LIKE "cost overrun" + """); + } + + @Test + void fromClauseDowncastingExample3fixed() { + + EqlQueryParser.parseQuery(""" + SELECT e FROM Employee e JOIN e.projects p + WHERE TREAT(p AS LargeProject).budget > 1000 + OR TREAT(p AS SmallProject).name LIKE 'Persist%' + OR p.description LIKE 'cost overrun' + """); + } + + @Test + void fromClauseDowncastingExample4() { + + EqlQueryParser.parseQuery(""" + SELECT e FROM Employee e + WHERE TREAT(e AS Exempt).vacationDays > 10 + OR TREAT(e AS Contractor).hours > 100 + """); + } + + @Test + void pathExpressionsNamedParametersExample() { + + EqlQueryParser.parseQuery(""" + SELECT c + FROM Customer c + WHERE c.status = :stat + """); + } + + @Test + void betweenExpressionsExample() { + + EqlQueryParser.parseQuery(""" + SELECT t + FROM CreditCard c JOIN c.transactionHistory t + WHERE c.holder.name = 'John Doe' AND INDEX(t) BETWEEN 0 AND 9 + """); + } + + @Test + void isEmptyExample() { + + EqlQueryParser.parseQuery(""" + SELECT o + FROM Order o + WHERE o.lineItems IS EMPTY + """); + } + + @Test + void memberOfExample() { + + EqlQueryParser.parseQuery(""" + SELECT p + FROM Person p + WHERE 'Joe' MEMBER OF p.nicknames + """); + } + + @Test + void existsSubSelectExample1() { + + EqlQueryParser.parseQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE EXISTS ( + SELECT spouseEmp + FROM Employee spouseEmp + WHERE spouseEmp = emp.spouse) + """); + } + + @Test + void allExample() { + + EqlQueryParser.parseQuery(""" + SELECT emp + FROM Employee emp + WHERE emp.salary > ALL ( + SELECT m.salary + FROM Manager m + WHERE m.department = emp.department) + """); + } + + @Test + void existsSubSelectExample2() { + + EqlQueryParser.parseQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE EXISTS ( + SELECT spouseEmp + FROM Employee spouseEmp + WHERE spouseEmp = emp.spouse) + """); + } + + @Test + void subselectNumericComparisonExample1() { + + EqlQueryParser.parseQuery(""" + SELECT c + FROM Customer c + WHERE (SELECT AVG(o.price) FROM c.orders o) > 100 + """); + } + + @Test + void subselectNumericComparisonExample2() { + + EqlQueryParser.parseQuery(""" + SELECT goodCustomer + FROM Customer goodCustomer + WHERE goodCustomer.balanceOwed < ( + SELECT AVG(c.balanceOwed)/2.0 FROM Customer c) + """); + } + + @Test + void indexExample() { + + EqlQueryParser.parseQuery(""" + SELECT w.name + FROM Course c JOIN c.studentWaitlist w + WHERE c.name = 'Calculus' + AND INDEX(w) = 0 + """); + } + + /** + * @see #functionInvocationExampleWithCorrection() + */ + @Test + @Disabled(SPEC_FAULT + "FUNCTION calls needs a comparator") + void functionInvocationExample_SPEC_BUG() { + + EqlQueryParser.parseQuery(""" + SELECT c + FROM Customer c + WHERE FUNCTION('hasGoodCredit', c.balance, c.creditLimit) + """); + } + + @Test + void functionInvocationExampleWithCorrection() { + + EqlQueryParser.parseQuery(""" + SELECT c + FROM Customer c + WHERE FUNCTION('hasGoodCredit', c.balance, c.creditLimit) = TRUE + """); + } + + @Test + void updateCaseExample1() { + + EqlQueryParser.parseQuery(""" + UPDATE Employee e + SET e.salary = + CASE WHEN e.rating = 1 THEN e.salary * 1.1 + WHEN e.rating = 2 THEN e.salary * 1.05 + ELSE e.salary * 1.01 + END + """); + } + + @Test + void updateCaseExample2() { + + EqlQueryParser.parseQuery(""" + UPDATE Employee e + SET e.salary = + CASE e.rating WHEN 1 THEN e.salary * 1.1 + WHEN 2 THEN e.salary * 1.05 + ELSE e.salary * 1.01 + END + """); + } + + @Test + void selectCaseExample1() { + + EqlQueryParser.parseQuery(""" + SELECT e.name, + CASE TYPE(e) WHEN Exempt THEN 'Exempt' + WHEN Contractor THEN 'Contractor' + WHEN Intern THEN 'Intern' + ELSE 'NonExempt' + END + FROM Employee e + WHERE e.dept.name = 'Engineering' + """); + } + + @Test + void selectCaseExample2() { + + EqlQueryParser.parseQuery(""" + SELECT e.name, + f.name, + CONCAT(CASE WHEN f.annualMiles > 50000 THEN 'Platinum ' + WHEN f.annualMiles > 25000 THEN 'Gold ' + ELSE '' + END, + 'Frequent Flyer') + FROM Employee e JOIN e.frequentFlierPlan f + """); + } + + @Test + void theRest() { + + EqlQueryParser.parseQuery(""" + SELECT e + FROM Employee e + WHERE TYPE(e) IN (Exempt, Contractor) + """); + } + + @Test + void theRest2() { + + EqlQueryParser.parseQuery(""" + SELECT e + FROM Employee e + WHERE TYPE(e) IN (:empType1, :empType2) + """); + } + + @Test + void theRest3() { + + EqlQueryParser.parseQuery(""" + SELECT e + FROM Employee e + WHERE TYPE(e) IN :empTypes + """); + } + + @Test + void theRest4() { + + EqlQueryParser.parseQuery(""" + SELECT TYPE(e) + FROM Employee e + WHERE TYPE(e) <> Exempt + """); + } + + @Test + void theRest5() { + + EqlQueryParser.parseQuery(""" + SELECT c.status, AVG(c.filledOrderCount), COUNT(c) + FROM Customer c + GROUP BY c.status + HAVING c.status IN (1, 2) + """); + } + + @Test + void theRest6() { + + EqlQueryParser.parseQuery(""" + SELECT c.country, COUNT(c) + FROM Customer c + GROUP BY c.country + HAVING COUNT(c) > 30 + """); + } + + @Test + void theRest7() { + + EqlQueryParser.parseQuery(""" + SELECT c, COUNT(o) + FROM Customer c JOIN c.orders o + GROUP BY c + HAVING COUNT(o) >= 5 + """); + } + + @Test + void theRest8() { + + EqlQueryParser.parseQuery(""" + SELECT c.id, c.status + FROM Customer c JOIN c.orders o + WHERE o.count > 100 + """); + } + + @Test + void theRest9() { + + EqlQueryParser.parseQuery(""" + SELECT v.location.street, KEY(i).title, VALUE(i) + FROM VideoStore v JOIN v.videoInventory i + WHERE v.location.zipcode = '94301' AND VALUE(i) > 0 + """); + } + + @Test + void theRest10() { + + EqlQueryParser.parseQuery(""" + SELECT o.lineItems FROM Order AS o + """); + } + + @Test + void theRest11() { + + EqlQueryParser.parseQuery(""" + SELECT c, COUNT(l) AS itemCount + FROM Customer c JOIN c.Orders o JOIN o.lineItems l + WHERE c.address.state = 'CA' + GROUP BY c + ORDER BY itemCount + """); + } + + @Test + void theRest12() { + + EqlQueryParser.parseQuery(""" + SELECT NEW com.acme.example.CustomerDetails(c.id, c.status, o.count) + FROM Customer c JOIN c.orders o + WHERE o.count > 100 + """); + } + + @Test + void theRest13() { + + EqlQueryParser.parseQuery(""" + SELECT e.address AS addr + FROM Employee e + """); + } + + @Test + void theRest14() { + + EqlQueryParser.parseQuery(""" + SELECT AVG(o.quantity) FROM Order o + """); + } + + @Test + void theRest15() { + + EqlQueryParser.parseQuery(""" + SELECT SUM(l.price) + FROM Order o JOIN o.lineItems l JOIN o.customer c + WHERE c.lastname = 'Smith' AND c.firstname = 'John' + """); + } + + @Test + void theRest16() { + + EqlQueryParser.parseQuery(""" + SELECT COUNT(o) FROM Order o + """); + } + + @Test + void theRest17() { + + EqlQueryParser.parseQuery(""" + SELECT COUNT(l.price) + FROM Order o JOIN o.lineItems l JOIN o.customer c + WHERE c.lastname = 'Smith' AND c.firstname = 'John' + """); + } + + @Test + void theRest18() { + + EqlQueryParser.parseQuery(""" + SELECT COUNT(l) + FROM Order o JOIN o.lineItems l JOIN o.customer c + WHERE c.lastname = 'Smith' AND c.firstname = 'John' AND l.price IS NOT NULL + """); + } + + @Test + void theRest19() { + + EqlQueryParser.parseQuery(""" + SELECT o + FROM Customer c JOIN c.orders o JOIN c.address a + WHERE a.state = 'CA' + ORDER BY o.quantity DESC, o.totalcost + """); + } + + @Test + void theRest20() { + + EqlQueryParser.parseQuery(""" + SELECT o.quantity, a.zipcode + FROM Customer c JOIN c.orders o JOIN c.address a + WHERE a.state = 'CA' + ORDER BY o.quantity, a.zipcode + """); + } + + @Test + void theRest21() { + + EqlQueryParser.parseQuery(""" + SELECT o.quantity, o.cost*1.08 AS taxedCost, a.zipcode + FROM Customer c JOIN c.orders o JOIN c.address a + WHERE a.state = 'CA' AND a.county = 'Santa Clara' + ORDER BY o.quantity, taxedCost, a.zipcode + """); + } + + @Test + void theRest22() { + + EqlQueryParser.parseQuery(""" + SELECT AVG(o.quantity) as q, a.zipcode + FROM Customer c JOIN c.orders o JOIN c.address a + WHERE a.state = 'CA' + GROUP BY a.zipcode + ORDER BY q DESC + """); + } + + @Test + void theRest23() { + + EqlQueryParser.parseQuery(""" + SELECT p.product_name + FROM Order o JOIN o.lineItems l JOIN l.product p JOIN o.customer c + WHERE c.lastname = 'Smith' AND c.firstname = 'John' + ORDER BY p.price + """); + } + + /** + * This query is specifically dubbed illegal in the spec. It may actually be failing for a different reason. + */ + @Test + void theRest24() { + + assertThatExceptionOfType(BadJpqlGrammarException.class).isThrownBy(() -> { + EqlQueryParser.parseQuery(""" + SELECT p.product_name + FROM Order o, IN(o.lineItems) l JOIN o.customer c + WHERE c.lastname = 'Smith' AND c.firstname = 'John' + ORDER BY o.quantity + """); + }); + } + + @Test + void theRest25() { + + EqlQueryParser.parseQuery(""" + DELETE + FROM Customer c + WHERE c.status = 'inactive' + """); + } + + @Test + void theRest26() { + + EqlQueryParser.parseQuery(""" + DELETE + FROM Customer c + WHERE c.status = 'inactive' + AND c.orders IS EMPTY + """); + } + + @Test + void theRest27() { + + EqlQueryParser.parseQuery(""" + UPDATE Customer c + SET c.status = 'outstanding' + WHERE c.balance < 10000 + """); + } + + @Test + void theRest28() { + + EqlQueryParser.parseQuery(""" + UPDATE Employee e + SET e.address.building = 22 + WHERE e.address.building = 14 + AND e.address.city = 'Santa Clara' + AND e.project = 'Jakarta EE' + """); + } + + @Test + void theRest29() { + + EqlQueryParser.parseQuery(""" + SELECT o + FROM Order o + """); + } + + @Test + void theRest30() { + + EqlQueryParser.parseQuery(""" + SELECT o + FROM Order o + WHERE o.shippingAddress.state = 'CA' + """); + } + + @Test + void theRest31() { + + EqlQueryParser.parseQuery(""" + SELECT DISTINCT o.shippingAddress.state + FROM Order o + """); + } + + @Test + void theRest32() { + + EqlQueryParser.parseQuery(""" + SELECT DISTINCT o + FROM Order o JOIN o.lineItems l + """); + } + + @Test + void theRest33() { + + EqlQueryParser.parseQuery(""" + SELECT o + FROM Order o + WHERE o.lineItems IS NOT EMPTY + """); + } + + @Test + void theRest34() { + + EqlQueryParser.parseQuery(""" + SELECT o + FROM Order o + WHERE o.lineItems IS EMPTY + """); + } + + @Test + void theRest35() { + + EqlQueryParser.parseQuery(""" + SELECT DISTINCT o + FROM Order o JOIN o.lineItems l + WHERE l.shipped = FALSE + """); + } + + @Test + void theRest36() { + + EqlQueryParser.parseQuery(""" + SELECT o + FROM Order o + WHERE + NOT (o.shippingAddress.state = o.billingAddress.state AND + o.shippingAddress.city = o.billingAddress.city AND + o.shippingAddress.street = o.billingAddress.street) + """); + } + + @Test + void theRest37() { + + EqlQueryParser.parseQuery(""" + SELECT o + FROM Order o + WHERE o.shippingAddress <> o.billingAddress + """); + } + + @Test + void theRest38() { + + EqlQueryParser.parseQuery(""" + SELECT DISTINCT o + FROM Order o JOIN o.lineItems l + WHERE l.product.name = ?1 + """); + } +} 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 403f7ffae..de8b922d3 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 @@ -523,7 +523,7 @@ class JpqlQueryRendererTests { } @Test - void theRest() { + void inClauseWithTypeLiteralsShouldWork() { assertQuery(""" SELECT e @@ -533,7 +533,7 @@ class JpqlQueryRendererTests { } @Test - void theRest2() { + void inClauseWithParametersShouldWork() { assertQuery(""" SELECT e @@ -543,7 +543,7 @@ class JpqlQueryRendererTests { } @Test - void theRest3() { + void inClauseWithSingleParameterShouldWork() { assertQuery(""" SELECT e @@ -553,7 +553,7 @@ class JpqlQueryRendererTests { } @Test - void theRest4() { + void notEqualsForTypeShouldWork() { assertQuery(""" SELECT TYPE(e) @@ -563,7 +563,7 @@ class JpqlQueryRendererTests { } @Test - void theRest5() { + void havingWithInClauseShouldWork() { assertQuery(""" SELECT c.status, AVG(c.filledOrderCount), COUNT(c) @@ -574,7 +574,7 @@ class JpqlQueryRendererTests { } @Test - void theRest6() { + void havingClauseWithComparisonShouldWork() { assertQuery(""" SELECT c.country, COUNT(c) @@ -585,7 +585,7 @@ class JpqlQueryRendererTests { } @Test - void theRest7() { + void havingClauseWithAnotherComparisonShouldWork() { assertQuery(""" SELECT c, COUNT(o) @@ -596,7 +596,7 @@ class JpqlQueryRendererTests { } @Test - void theRest8() { + void whereClauseWithComparisonShouldWork() { assertQuery(""" SELECT c.id, c.status @@ -606,7 +606,7 @@ class JpqlQueryRendererTests { } @Test - void theRest9() { + void keyValueFunctionsShouldWork() { assertQuery(""" SELECT v.location.street, KEY(i).title, VALUE(i) @@ -616,7 +616,7 @@ class JpqlQueryRendererTests { } @Test - void theRest10() { + void fromClauseWithAsShouldWork() { assertQuery(""" SELECT o.lineItems FROM Order AS o @@ -624,7 +624,7 @@ class JpqlQueryRendererTests { } @Test - void theRest11() { + void countFunctionWithAsClauseShouldWork() { assertQuery(""" SELECT c, COUNT(l) AS itemCount @@ -636,7 +636,7 @@ class JpqlQueryRendererTests { } @Test - void theRest12() { + void objectConstructionShouldWork() { assertQuery(""" SELECT NEW com.acme.example.CustomerDetails(c.id, c.status, o.count) @@ -646,7 +646,7 @@ class JpqlQueryRendererTests { } @Test - void theRest13() { + void selectWithAsClauseShouldWork() { assertQuery(""" SELECT e.address AS addr @@ -655,7 +655,7 @@ class JpqlQueryRendererTests { } @Test - void theRest14() { + void averageFunctionShouldWork() { assertQuery(""" SELECT AVG(o.quantity) FROM Order o @@ -663,7 +663,7 @@ class JpqlQueryRendererTests { } @Test - void theRest15() { + void sumFunctionShouldWork() { assertQuery(""" SELECT SUM(l.price) @@ -673,7 +673,7 @@ class JpqlQueryRendererTests { } @Test - void theRest16() { + void countFunctionShouldWork() { assertQuery(""" SELECT COUNT(o) FROM Order o @@ -681,7 +681,7 @@ class JpqlQueryRendererTests { } @Test - void theRest17() { + void countFunctionOnSubElementShouldWork() { assertQuery(""" SELECT COUNT(l.price) @@ -691,7 +691,7 @@ class JpqlQueryRendererTests { } @Test - void theRest18() { + void equivalentCountFunctionShouldAlsoWork() { assertQuery(""" SELECT COUNT(l) @@ -701,7 +701,7 @@ class JpqlQueryRendererTests { } @Test - void theRest19() { + void orderByBasedOnSelectClauseShouldWork() { assertQuery(""" SELECT o @@ -712,7 +712,7 @@ class JpqlQueryRendererTests { } @Test - void theRest20() { + void orderByThatMatchesSelectClauseShouldWork() { assertQuery(""" SELECT o.quantity, a.zipcode @@ -723,7 +723,7 @@ class JpqlQueryRendererTests { } @Test - void theRest21() { + void orderByThatMatchesAllSelectAliasesShouldWork() { assertQuery(""" SELECT o.quantity, o.cost*1.08 AS taxedCost, a.zipcode @@ -734,7 +734,7 @@ class JpqlQueryRendererTests { } @Test - void theRest22() { + void orderByThatMatchesSelectFunctionAliasShouldWork() { assertQuery(""" SELECT AVG(o.quantity) as q, a.zipcode @@ -745,8 +745,13 @@ class JpqlQueryRendererTests { """); } + /** + * NOTE: This query is specifically dubbed illegal in the spec. However, it's not due to a grammar failure but instead + * for semantic reasons. Our parser does NOT check if the ORDER BY matches the SELECT or not. Hence, this is left to + * the JPA provider. + */ @Test - void theRest23() { + void orderByClauseThatIsNotReflectedInTheSelectClause() { assertQuery(""" SELECT p.product_name @@ -757,10 +762,10 @@ class JpqlQueryRendererTests { } /** - * This query is specifically dubbed illegal in the spec. It may actually be failing for a different reason. + * NOTE: This query is specifically dubbed illegal in the spec. It may actually be failing for a different reason. */ @Test - void theRest24() { + void orderByClauseThatIsNotReflectedInTheSelectClauseButAlsoHasAnInClauseInTheFromClause() { assertThatExceptionOfType(BadJpqlGrammarException.class).isThrownBy(() -> { assertQuery(""" @@ -773,7 +778,7 @@ class JpqlQueryRendererTests { } @Test - void theRest25() { + void simpleDeleteShouldWork() { assertQuery(""" DELETE @@ -783,7 +788,7 @@ class JpqlQueryRendererTests { } @Test - void theRest26() { + void deleteWithMoreComplexCriteriaShouldWork() { assertQuery(""" DELETE @@ -794,7 +799,7 @@ class JpqlQueryRendererTests { } @Test - void theRest27() { + void simpleUpdateShouldWork() { assertQuery(""" UPDATE Customer c @@ -804,7 +809,7 @@ class JpqlQueryRendererTests { } @Test - void theRest28() { + void moreComplexUpdateShouldWork() { assertQuery(""" UPDATE Employee e @@ -816,7 +821,7 @@ class JpqlQueryRendererTests { } @Test - void theRest29() { + void simpleSelectShouldWork() { assertQuery(""" SELECT o @@ -825,7 +830,7 @@ class JpqlQueryRendererTests { } @Test - void theRest30() { + void selectWithWhereClauseShouldWork() { assertQuery(""" SELECT o @@ -835,7 +840,7 @@ class JpqlQueryRendererTests { } @Test - void theRest31() { + void selectWithDistinctSubElementShouldWork() { assertQuery(""" SELECT DISTINCT o.shippingAddress.state @@ -844,7 +849,7 @@ class JpqlQueryRendererTests { } @Test - void theRest32() { + void selectWithSimpleDistinctShouldWork() { assertQuery(""" SELECT DISTINCT o @@ -853,7 +858,7 @@ class JpqlQueryRendererTests { } @Test - void theRest33() { + void selectWithIsNotEmptyCriteriaShouldWork() { assertQuery(""" SELECT o @@ -863,7 +868,7 @@ class JpqlQueryRendererTests { } @Test - void theRest34() { + void selectWithIsEmptyCriteriaShouldWork() { assertQuery(""" SELECT o @@ -873,7 +878,7 @@ class JpqlQueryRendererTests { } @Test - void theRest35() { + void findAllPendingOrders() { assertQuery(""" SELECT DISTINCT o @@ -883,7 +888,7 @@ class JpqlQueryRendererTests { } @Test - void theRest36() { + void findAllOrdersWhereShippingAddressDoesNotMatchBillingAddress() { assertQuery(""" SELECT o @@ -896,7 +901,7 @@ class JpqlQueryRendererTests { } @Test - void theRest37() { + void simplerVersionOfShippingAddressNotMatchingBillingAddress() { assertQuery(""" SELECT o @@ -906,7 +911,7 @@ class JpqlQueryRendererTests { } @Test - void theRest38() { + void findOrdersThatHaveProductNamedByAParameter() { assertQuery(""" SELECT DISTINCT o