Implement EQL parser.

Implement support for EclipseLink Query Language (EQL), handling the various extensions it offers.

Closes #3170
Original pull request: #3176
This commit is contained in:
Greg L. Turnquist
2023-09-21 15:12:32 -05:00
committed by Mark Paluch
parent ae12c1cb05
commit dde9e07c24
15 changed files with 7056 additions and 45 deletions

View File

@@ -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 '}';

View File

@@ -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<String> entityManagerClassNames;
private final Iterable<String> 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.
*

View File

@@ -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();
}

View File

@@ -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<JpaQueryParsingToken> 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<JpaQueryParsingToken> 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<JpaQueryParsingToken> 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();
}
}

View File

@@ -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<JpaQueryParsingToken> 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<JpaQueryParsingToken> getProjection() {
return this.projection;
}
public boolean hasConstructorExpression() {
return this.hasConstructorExpression;
}
@Override
public List<JpaQueryParsingToken> visitSelect_statement(EqlParser.Select_statementContext ctx) {
List<JpaQueryParsingToken> 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<JpaQueryParsingToken> visitSelect_clause(EqlParser.Select_clauseContext ctx) {
List<JpaQueryParsingToken> 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<JpaQueryParsingToken> 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<JpaQueryParsingToken> visitSelect_item(EqlParser.Select_itemContext ctx) {
List<JpaQueryParsingToken> tokens = super.visitSelect_item(ctx);
if (ctx.result_variable() != null) {
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
}
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitRange_variable_declaration(EqlParser.Range_variable_declarationContext ctx) {
List<JpaQueryParsingToken> 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<JpaQueryParsingToken> visitJoin(EqlParser.JoinContext ctx) {
List<JpaQueryParsingToken> tokens = super.visitJoin(ctx);
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
return tokens;
}
@Override
public List<JpaQueryParsingToken> visitConstructor_expression(EqlParser.Constructor_expressionContext ctx) {
hasConstructorExpression = true;
return super.visitConstructor_expression(ctx);
}
private static <T> ArrayList<T> newArrayList() {
return new ArrayList<>();
}
}

View File

@@ -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;
}

View File

@@ -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.
*/

View File

@@ -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);
}
}
}

View File

@@ -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<br/>
* <br/>
* 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");
}
}

View File

@@ -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);
}
}

View File

@@ -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<br/>
* <br/>
* 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");
}
}

View File

@@ -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<Arguments> 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));
}
}

View File

@@ -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<br/>
* <br/>
* 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
""");
}
}

View File

@@ -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