Create JPQL and HQL parsers.
Introduce grammars that support both JPQL (JPA 3.1) as well as HQL (Hibernate 6.1) and allow us to leverage it for query handling. Related: #2814.
This commit is contained in:
1
pom.xml
1
pom.xml
@@ -30,6 +30,7 @@
|
||||
<source.level>16</source.level>
|
||||
<!-- AspectJ maven plugin can't handle 17 yet -->
|
||||
|
||||
<antlr>4.11.1</antlr>
|
||||
<eclipselink>3.0.3</eclipselink>
|
||||
<hibernate>6.1.4.Final</hibernate>
|
||||
<hsqldb>2.7.1</hsqldb>
|
||||
|
||||
@@ -73,6 +73,12 @@
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.antlr</groupId>
|
||||
<artifactId>antlr4-runtime</artifactId>
|
||||
<version>${antlr}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.aspectj</groupId>
|
||||
<artifactId>aspectjweaver</artifactId>
|
||||
@@ -247,8 +253,8 @@
|
||||
<plugins>
|
||||
|
||||
<!--
|
||||
Jacoco plugin redeclared to make sure it's downloaded and
|
||||
the agents can be explicitly added to the test executions.
|
||||
Jacoco plugin redeclared to make sure it's downloaded and
|
||||
the agents can be explicitly added to the test executions.
|
||||
-->
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
@@ -344,6 +350,45 @@
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.antlr</groupId>
|
||||
<artifactId>antlr4-maven-plugin</artifactId>
|
||||
<version>${antlr}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>antlr4</goal>
|
||||
</goals>
|
||||
<phase>generate-sources</phase>
|
||||
<configuration>
|
||||
<visitor>true</visitor>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>com.google.code.maven-replacer-plugin</groupId>
|
||||
<artifactId>maven-replacer-plugin</artifactId>
|
||||
<version>1.4.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>process-sources</phase>
|
||||
<goals>
|
||||
<goal>replace</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<includes>
|
||||
<include>target/generated-sources/antlr4/**/*.java</include>
|
||||
</includes>
|
||||
<variableTokenValueMap>
|
||||
public class=class,public interface=interface
|
||||
</variableTokenValueMap>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<executions>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,850 @@
|
||||
/*
|
||||
* Copyright 2011-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 Jpql;
|
||||
|
||||
@header {
|
||||
/**
|
||||
* JPQL per https://jakarta.ee/specifications/persistence/3.1/jakarta-persistence-spec-3.1.html#bnf
|
||||
*
|
||||
* This is JPA BNF for JPQL. There are gaps and inconsistencies in the BNF itself, explained by other fragments of the spec.
|
||||
*
|
||||
* @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#bnf
|
||||
* @author Greg Turnquist
|
||||
* @since 3.1
|
||||
*/
|
||||
}
|
||||
|
||||
/*
|
||||
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)?
|
||||
;
|
||||
|
||||
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
|
||||
;
|
||||
|
||||
identification_variable_declaration
|
||||
: range_variable_declaration (join | fetch_join)*
|
||||
;
|
||||
|
||||
range_variable_declaration
|
||||
: entity_name (AS)? identification_variable
|
||||
;
|
||||
|
||||
join
|
||||
: join_spec join_association_path_expression (AS)? identification_variable (join_condition)?
|
||||
;
|
||||
|
||||
fetch_join
|
||||
: join_spec FETCH join_association_path_expression
|
||||
;
|
||||
|
||||
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) '=' 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
|
||||
;
|
||||
|
||||
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 | general_identification_variable | result_variable ) (ASC | DESC)?
|
||||
;
|
||||
|
||||
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) 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)
|
||||
| boolean_expression op=('=' | '<>') (boolean_expression | all_or_any_expression)
|
||||
| enum_expression op=('=' | '<>') (enum_expression | all_or_any_expression)
|
||||
| datetime_expression comparison_operator (datetime_expression | all_or_any_expression)
|
||||
| entity_expression op=('=' | '<>') (entity_expression | all_or_any_expression)
|
||||
| arithmetic_expression comparison_operator (arithmetic_expression | all_or_any_expression)
|
||||
| entity_type_expression op=('=' | '<>') entity_type_expression
|
||||
;
|
||||
|
||||
comparison_operator
|
||||
: op='='
|
||||
| op='>'
|
||||
| op='>='
|
||||
| op='<'
|
||||
| op='<='
|
||||
| op='<>'
|
||||
;
|
||||
|
||||
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
|
||||
| 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
|
||||
;
|
||||
|
||||
|
||||
function_invocation
|
||||
: FUNCTION '(' 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
|
||||
| ORDER // Gap in the spec requires supporting 'Order' as an entity name
|
||||
| COUNT // Gap in the spec requires supporting 'count' as a possible name
|
||||
| KEY // Gap in the sepc requires supported 'key' as a possible name
|
||||
| spel_expression // we use various SpEL expressions in our queries
|
||||
;
|
||||
|
||||
constructor_name
|
||||
: state_field_path_expression
|
||||
;
|
||||
|
||||
literal
|
||||
: STRINGLITERAL
|
||||
| INTLITERAL
|
||||
| FLOATLITERAL
|
||||
| boolean_literal
|
||||
| entity_type_literal
|
||||
;
|
||||
|
||||
input_parameter
|
||||
: '?' INTLITERAL
|
||||
| ':' identification_variable
|
||||
;
|
||||
|
||||
pattern_value
|
||||
: string_expression
|
||||
;
|
||||
|
||||
date_time_timestamp_literal
|
||||
: STRINGLITERAL
|
||||
;
|
||||
|
||||
entity_type_literal
|
||||
: identification_variable
|
||||
;
|
||||
|
||||
escape_character
|
||||
: CHARACTER
|
||||
| character_valued_input_parameter //
|
||||
;
|
||||
|
||||
numeric_literal
|
||||
: INTLITERAL
|
||||
| FLOATLITERAL
|
||||
;
|
||||
|
||||
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 ('.' 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
|
||||
;
|
||||
|
||||
spel_expression
|
||||
: prefix='#{#' identification_variable ('.' identification_variable)* '}' // #{#entityName}
|
||||
| prefix='#{#[' INTLITERAL ']}' // #{[0]}
|
||||
| prefix='#{' identification_variable '(' ( string_literal | '[' INTLITERAL ']' )? ')}' // #{escape([0])} | #{escapeCharacter()}
|
||||
;
|
||||
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
IS : I S;
|
||||
JOIN : J O I N;
|
||||
KEY : K E Y;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
|
||||
|
||||
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')* ;
|
||||
INTLITERAL : ('0' .. '9')+ ;
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2022-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 parsing operations of a {@link JpaQueryParser} using the ANTLR-generated {@link HqlParser} and
|
||||
* {@link HqlQueryTransformer}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 3.1
|
||||
*/
|
||||
class HqlQueryParser extends JpaQueryParser {
|
||||
|
||||
HqlQueryParser(DeclaredQuery declaredQuery) {
|
||||
super(declaredQuery);
|
||||
}
|
||||
|
||||
HqlQueryParser(String query) {
|
||||
super(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to parse an HQL query. Will throw a {@link JpaQueryParsingSyntaxError} if the query is invalid.
|
||||
*
|
||||
* @param query
|
||||
* @return a parsed query, ready for postprocessing
|
||||
*/
|
||||
static ParserRuleContext parse(String query) {
|
||||
|
||||
HqlLexer lexer = new HqlLexer(CharStreams.fromString(query));
|
||||
HqlParser parser = new HqlParser(new CommonTokenStream(lexer));
|
||||
|
||||
parser.addErrorListener(new JpaQueryParsingSyntaxErrorListener());
|
||||
|
||||
return parser.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the query using {@link #parse(String)}.
|
||||
*
|
||||
* @return a parsed query
|
||||
*/
|
||||
@Override
|
||||
protected ParserRuleContext parse() {
|
||||
return parse(getQuery());
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the {@link HqlQueryTransformer} 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> doCreateQuery(ParserRuleContext parsedQuery, Sort sort) {
|
||||
return new HqlQueryTransformer(sort).visit(parsedQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the {@link HqlQueryTransformer} 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 HqlQueryTransformer(true, countProjection).visit(parsedQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the parsed query through {@link HqlQueryTransformer} to find the primary FROM clause's alias.
|
||||
*
|
||||
* @param parsedQuery
|
||||
* @return can be {@literal null}
|
||||
*/
|
||||
@Override
|
||||
protected String doFindAlias(ParserRuleContext parsedQuery) {
|
||||
|
||||
HqlQueryTransformer transformVisitor = new HqlQueryTransformer();
|
||||
transformVisitor.visit(parsedQuery);
|
||||
return transformVisitor.getAlias();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use {@link HqlQueryTransformer} to find the projection of the query.
|
||||
*
|
||||
* @param parsedQuery
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected List<JpaQueryParsingToken> doFindProjection(ParserRuleContext parsedQuery) {
|
||||
|
||||
HqlQueryTransformer transformVisitor = new HqlQueryTransformer();
|
||||
transformVisitor.visit(parsedQuery);
|
||||
return transformVisitor.getProjection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use {@link HqlQueryTransformer} 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) {
|
||||
|
||||
HqlQueryTransformer transformVisitor = new HqlQueryTransformer();
|
||||
transformVisitor.visit(parsedQuery);
|
||||
return transformVisitor.hasConstructorExpression();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,338 @@
|
||||
/*
|
||||
* Copyright 2022-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import static org.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.antlr.v4.runtime.ParserRuleContext;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed HQL query.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 3.1
|
||||
*/
|
||||
class HqlQueryTransformer extends HqlQueryRenderer {
|
||||
|
||||
@Nullable private Sort sort;
|
||||
private boolean countQuery;
|
||||
|
||||
@Nullable private String countProjection;
|
||||
|
||||
@Nullable private String alias = null;
|
||||
|
||||
private List<JpaQueryParsingToken> projection = null;
|
||||
|
||||
private boolean hasConstructorExpression = false;
|
||||
|
||||
HqlQueryTransformer() {
|
||||
this(null, false, null);
|
||||
}
|
||||
|
||||
HqlQueryTransformer(@Nullable Sort sort) {
|
||||
this(sort, false, null);
|
||||
}
|
||||
|
||||
HqlQueryTransformer(boolean countQuery, @Nullable String countProjection) {
|
||||
this(null, countQuery, countProjection);
|
||||
}
|
||||
|
||||
private HqlQueryTransformer(@Nullable Sort sort, boolean countQuery, @Nullable String countProjection) {
|
||||
|
||||
this.sort = sort;
|
||||
this.countQuery = countQuery;
|
||||
this.countProjection = countProjection;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getAlias() {
|
||||
return this.alias;
|
||||
}
|
||||
|
||||
public List<JpaQueryParsingToken> getProjection() {
|
||||
return this.projection;
|
||||
}
|
||||
|
||||
public boolean hasConstructorExpression() {
|
||||
return this.hasConstructorExpression;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this select clause a {@literal subquery}?
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private static boolean isSubquery(ParserRuleContext ctx) {
|
||||
|
||||
if (ctx instanceof HqlParser.SubqueryContext) {
|
||||
return true;
|
||||
} else if (ctx instanceof HqlParser.SelectStatementContext) {
|
||||
return false;
|
||||
} else {
|
||||
return isSubquery(ctx.getParent());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JpaQueryParsingToken> visitOrderedQuery(HqlParser.OrderedQueryContext ctx) {
|
||||
|
||||
List<JpaQueryParsingToken> tokens = new ArrayList<>();
|
||||
|
||||
if (ctx.query() != null) {
|
||||
tokens.addAll(visit(ctx.query()));
|
||||
} else if (ctx.queryExpression() != null) {
|
||||
|
||||
tokens.add(TOKEN_OPEN_PAREN);
|
||||
tokens.addAll(visit(ctx.queryExpression()));
|
||||
tokens.add(TOKEN_CLOSE_PAREN);
|
||||
}
|
||||
|
||||
if (!countQuery && !isSubquery(ctx)) {
|
||||
|
||||
if (ctx.queryOrder() != null) {
|
||||
tokens.addAll(visit(ctx.queryOrder()));
|
||||
}
|
||||
|
||||
if (this.sort != null && this.sort.isSorted()) {
|
||||
|
||||
if (ctx.queryOrder() != null) {
|
||||
|
||||
NOSPACE(tokens);
|
||||
tokens.add(TOKEN_COMMA);
|
||||
} else {
|
||||
|
||||
SPACE(tokens);
|
||||
tokens.add(TOKEN_ORDER_BY);
|
||||
}
|
||||
|
||||
this.sort.forEach(order -> {
|
||||
|
||||
JpaQueryParser.checkSortExpression(order);
|
||||
|
||||
if (order.isIgnoreCase()) {
|
||||
tokens.add(TOKEN_LOWER_FUNC);
|
||||
}
|
||||
tokens.add(new JpaQueryParsingToken(() -> {
|
||||
|
||||
if (order.getProperty().contains("(")) {
|
||||
return order.getProperty();
|
||||
}
|
||||
|
||||
return this.alias + "." + order.getProperty();
|
||||
}, true));
|
||||
if (order.isIgnoreCase()) {
|
||||
NOSPACE(tokens);
|
||||
tokens.add(TOKEN_CLOSE_PAREN);
|
||||
}
|
||||
tokens.add(order.isDescending() ? TOKEN_DESC : TOKEN_ASC);
|
||||
tokens.add(TOKEN_COMMA);
|
||||
});
|
||||
CLIP(tokens);
|
||||
}
|
||||
} else {
|
||||
|
||||
if (ctx.queryOrder() != null) {
|
||||
tokens.addAll(visit(ctx.queryOrder()));
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JpaQueryParsingToken> visitFromQuery(HqlParser.FromQueryContext ctx) {
|
||||
|
||||
List<JpaQueryParsingToken> tokens = new ArrayList<>();
|
||||
|
||||
if (countQuery && !isSubquery(ctx) && ctx.selectClause() == null) {
|
||||
|
||||
tokens.add(TOKEN_SELECT_COUNT);
|
||||
|
||||
if (countProjection != null) {
|
||||
tokens.add(new JpaQueryParsingToken(countProjection));
|
||||
} else {
|
||||
tokens.add(new JpaQueryParsingToken(() -> this.alias, false));
|
||||
}
|
||||
|
||||
tokens.add(TOKEN_CLOSE_PAREN);
|
||||
}
|
||||
|
||||
if (ctx.fromClause() != null) {
|
||||
tokens.addAll(visit(ctx.fromClause()));
|
||||
}
|
||||
|
||||
if (ctx.whereClause() != null) {
|
||||
tokens.addAll(visit(ctx.whereClause()));
|
||||
}
|
||||
|
||||
if (ctx.groupByClause() != null) {
|
||||
tokens.addAll(visit(ctx.groupByClause()));
|
||||
}
|
||||
|
||||
if (ctx.havingClause() != null) {
|
||||
tokens.addAll(visit(ctx.havingClause()));
|
||||
}
|
||||
|
||||
if (ctx.selectClause() != null) {
|
||||
tokens.addAll(visit(ctx.selectClause()));
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JpaQueryParsingToken> visitQueryOrder(HqlParser.QueryOrderContext ctx) {
|
||||
|
||||
List<JpaQueryParsingToken> tokens = new ArrayList<>();
|
||||
|
||||
if (!countQuery) {
|
||||
tokens.addAll(visit(ctx.orderByClause()));
|
||||
}
|
||||
|
||||
if (ctx.limitClause() != null) {
|
||||
SPACE(tokens);
|
||||
tokens.addAll(visit(ctx.limitClause()));
|
||||
}
|
||||
if (ctx.offsetClause() != null) {
|
||||
tokens.addAll(visit(ctx.offsetClause()));
|
||||
}
|
||||
if (ctx.fetchClause() != null) {
|
||||
tokens.addAll(visit(ctx.fetchClause()));
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JpaQueryParsingToken> visitFromRoot(HqlParser.FromRootContext ctx) {
|
||||
|
||||
List<JpaQueryParsingToken> tokens = new ArrayList<>();
|
||||
|
||||
if (ctx.entityName() != null) {
|
||||
|
||||
tokens.addAll(visit(ctx.entityName()));
|
||||
|
||||
if (ctx.variable() != null) {
|
||||
tokens.addAll(visit(ctx.variable()));
|
||||
|
||||
if (this.alias == null && !isSubquery(ctx)) {
|
||||
this.alias = tokens.get(tokens.size() - 1).getToken();
|
||||
}
|
||||
}
|
||||
} else if (ctx.subquery() != null) {
|
||||
|
||||
if (ctx.LATERAL() != null) {
|
||||
tokens.add(new JpaQueryParsingToken(ctx.LATERAL()));
|
||||
}
|
||||
tokens.add(TOKEN_OPEN_PAREN);
|
||||
tokens.addAll(visit(ctx.subquery()));
|
||||
tokens.add(TOKEN_CLOSE_PAREN);
|
||||
|
||||
if (ctx.variable() != null) {
|
||||
tokens.addAll(visit(ctx.variable()));
|
||||
|
||||
if (this.alias == null && !isSubquery(ctx)) {
|
||||
this.alias = tokens.get(tokens.size() - 1).getToken();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JpaQueryParsingToken> visitAlias(HqlParser.AliasContext ctx) {
|
||||
|
||||
List<JpaQueryParsingToken> tokens = new ArrayList<>();
|
||||
|
||||
if (ctx.AS() != null) {
|
||||
tokens.add(new JpaQueryParsingToken(ctx.AS()));
|
||||
}
|
||||
|
||||
tokens.addAll(visit(ctx.identifier()));
|
||||
|
||||
if (this.alias == null && !isSubquery(ctx)) {
|
||||
this.alias = tokens.get(tokens.size() - 1).getToken();
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JpaQueryParsingToken> visitSelectClause(HqlParser.SelectClauseContext ctx) {
|
||||
|
||||
List<JpaQueryParsingToken> tokens = new ArrayList<>();
|
||||
|
||||
tokens.add(new JpaQueryParsingToken(ctx.SELECT()));
|
||||
|
||||
if (countQuery && !isSubquery(ctx)) {
|
||||
tokens.add(TOKEN_COUNT_FUNC);
|
||||
|
||||
if (countProjection != null) {
|
||||
tokens.add(new JpaQueryParsingToken(countProjection));
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.DISTINCT() != null) {
|
||||
tokens.add(new JpaQueryParsingToken(ctx.DISTINCT()));
|
||||
}
|
||||
|
||||
List<JpaQueryParsingToken> selectionListTokens = visit(ctx.selectionList());
|
||||
|
||||
if (countQuery && !isSubquery(ctx)) {
|
||||
|
||||
if (countProjection == null) {
|
||||
|
||||
if (ctx.DISTINCT() != null) {
|
||||
|
||||
if (selectionListTokens.stream().anyMatch(hqlToken -> hqlToken.getToken().contains("new"))) {
|
||||
// constructor
|
||||
tokens.add(new JpaQueryParsingToken(() -> this.alias));
|
||||
} else {
|
||||
// keep all the select items to distinct against
|
||||
tokens.addAll(selectionListTokens);
|
||||
}
|
||||
} else {
|
||||
tokens.add(new JpaQueryParsingToken(() -> this.alias));
|
||||
}
|
||||
}
|
||||
|
||||
NOSPACE(tokens);
|
||||
tokens.add(TOKEN_CLOSE_PAREN);
|
||||
} else {
|
||||
tokens.addAll(selectionListTokens);
|
||||
}
|
||||
|
||||
if (projection == null && !isSubquery(ctx)) {
|
||||
this.projection = selectionListTokens;
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JpaQueryParsingToken> visitInstantiation(HqlParser.InstantiationContext ctx) {
|
||||
|
||||
this.hasConstructorExpression = true;
|
||||
|
||||
return super.visitInstantiation(ctx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright 2022-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.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.antlr.v4.runtime.ParserRuleContext;
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.JpaSort;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Operations needed to parse a JPA query.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 3.1
|
||||
*/
|
||||
abstract class JpaQueryParser {
|
||||
|
||||
private static final Pattern PUNCTUATION_PATTERN = Pattern.compile(".*((?![._])[\\p{Punct}|\\s])");
|
||||
|
||||
private static final String UNSAFE_PROPERTY_REFERENCE = "Sort expression '%s' must only contain property references or "
|
||||
+ "aliases used in the select clause; If you really want to use something other than that for sorting, please use "
|
||||
+ "JpaSort.unsafe(…)";
|
||||
|
||||
private final DeclaredQuery declaredQuery;
|
||||
|
||||
JpaQueryParser(DeclaredQuery declaredQuery) {
|
||||
this.declaredQuery = declaredQuery;
|
||||
}
|
||||
|
||||
JpaQueryParser(String query) {
|
||||
this(DeclaredQuery.of(query, false));
|
||||
}
|
||||
|
||||
DeclaredQuery getDeclaredQuery() {
|
||||
return declaredQuery;
|
||||
}
|
||||
|
||||
String getQuery() {
|
||||
return getDeclaredQuery().getQueryString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a query using the original query with an @literal order by} clause added (or amended) based upon the
|
||||
* provider {@link Sort} parameter.
|
||||
*
|
||||
* @param sort can be {@literal null}
|
||||
*/
|
||||
String createQuery(Sort sort) {
|
||||
|
||||
try {
|
||||
ParserRuleContext parsedQuery = parse();
|
||||
|
||||
if (parsedQuery == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return render(doCreateQuery(parsedQuery, sort));
|
||||
} catch (JpaQueryParsingSyntaxError e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a count-based query using the original query.
|
||||
*
|
||||
* @param countProjection
|
||||
*/
|
||||
String createCountQuery(@Nullable String countProjection) {
|
||||
|
||||
try {
|
||||
ParserRuleContext parsedQuery = parse();
|
||||
|
||||
if (parsedQuery == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return render(doCreateCountQuery(parsedQuery, countProjection));
|
||||
} catch (JpaQueryParsingSyntaxError e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the projection of the query.
|
||||
*
|
||||
* @param parsedQuery
|
||||
*/
|
||||
String projection() {
|
||||
|
||||
try {
|
||||
ParserRuleContext parsedQuery = parse();
|
||||
|
||||
if (parsedQuery == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return render(doFindProjection(parsedQuery));
|
||||
} catch (JpaQueryParsingSyntaxError e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the alias of the query's primary FROM clause
|
||||
*
|
||||
* @return can be {@literal null}
|
||||
*/
|
||||
String findAlias() {
|
||||
|
||||
try {
|
||||
ParserRuleContext parsedQuery = parse();
|
||||
|
||||
if (parsedQuery == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return doFindAlias(parsedQuery);
|
||||
} catch (JpaQueryParsingSyntaxError e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discern if the query has a {@code new com.example.Dto()} DTO constructor in the select clause.
|
||||
*
|
||||
* @return Guaranteed to be {@literal true} or {@literal false}.
|
||||
*/
|
||||
boolean hasConstructorExpression() {
|
||||
|
||||
try {
|
||||
ParserRuleContext parsedQuery = parse();
|
||||
|
||||
if (parsedQuery == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return doCheckForConstructor(parsedQuery);
|
||||
} catch (JpaQueryParsingSyntaxError e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check any given {@link JpaSort.JpaOrder#isUnsafe()} order for presence of at least one property offending the
|
||||
* {@link #PUNCTUATION_PATTERN} and throw an {@link Exception} indicating potential unsafe order by expression.
|
||||
*
|
||||
* @param order
|
||||
*/
|
||||
static void checkSortExpression(Sort.Order order) {
|
||||
|
||||
if (order instanceof JpaSort.JpaOrder && ((JpaSort.JpaOrder) order).isUnsafe()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (PUNCTUATION_PATTERN.matcher(order.getProperty()).find()) {
|
||||
throw new InvalidDataAccessApiUsageException(String.format(UNSAFE_PROPERTY_REFERENCE, order));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the JPA query using its corresponding ANTLR parser.
|
||||
*/
|
||||
protected abstract ParserRuleContext parse();
|
||||
|
||||
/**
|
||||
* Create a {@link JpaQueryParsingToken}-based query with an {@literal order by} applied/amended based upon the
|
||||
* {@link Sort} parameter.
|
||||
*
|
||||
* @param parsedQuery
|
||||
* @param sort can be {@literal null}
|
||||
*/
|
||||
protected abstract List<JpaQueryParsingToken> doCreateQuery(ParserRuleContext parsedQuery, Sort sort);
|
||||
|
||||
/**
|
||||
* Create a {@link JpaQueryParsingToken}-based count query.
|
||||
*
|
||||
* @param parsedQuery
|
||||
* @param countProjection
|
||||
*/
|
||||
protected abstract List<JpaQueryParsingToken> doCreateCountQuery(ParserRuleContext parsedQuery,
|
||||
@Nullable String countProjection);
|
||||
|
||||
protected abstract String doFindAlias(ParserRuleContext parsedQuery);
|
||||
|
||||
/**
|
||||
* Find the projection of the query's primary SELECT clause.
|
||||
*
|
||||
* @param parsedQuery
|
||||
*/
|
||||
protected abstract List<JpaQueryParsingToken> doFindProjection(ParserRuleContext parsedQuery);
|
||||
|
||||
protected abstract boolean doCheckForConstructor(ParserRuleContext parsedQuery);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2022-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.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Implementation of {@link QueryEnhancer} using a {@link JpaQueryParser}.<br/>
|
||||
* <br/>
|
||||
* NOTE: The parser can find everything it needs to created sorted and count queries. Thus, looking up the alias or the
|
||||
* projection isn't needed for its primary function, and are simply implemented for test purposes.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 3.1
|
||||
*/
|
||||
class JpaQueryParsingEnhancer implements QueryEnhancer {
|
||||
|
||||
private final JpaQueryParser queryParser;
|
||||
|
||||
/**
|
||||
* Initialize with an {@link JpaQueryParser}.
|
||||
*
|
||||
* @param queryParser
|
||||
*/
|
||||
public JpaQueryParsingEnhancer(JpaQueryParser queryParser) {
|
||||
|
||||
Assert.notNull(queryParser, "queryParse must not be null!");
|
||||
this.queryParser = queryParser;
|
||||
}
|
||||
|
||||
public JpaQueryParser getQueryParsingStrategy() {
|
||||
return queryParser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an {@literal order by} clause to the JPA query.
|
||||
*
|
||||
* @param sort the sort specification to apply.
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public String applySorting(Sort sort) {
|
||||
return queryParser.createQuery(sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Because the parser can find the alias of the FROM clause, there is no need to "find it" in advance.
|
||||
*
|
||||
* @param sort the sort specification to apply.
|
||||
* @param alias IGNORED
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public String applySorting(Sort sort, String alias) {
|
||||
return applySorting(sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the alias for the entity in the FROM clause from the JPA query. Since the {@link JpaQueryParser} can
|
||||
* already find the alias when generating sorted and count queries, this is mainly to serve test cases.
|
||||
*/
|
||||
@Override
|
||||
public String detectAlias() {
|
||||
return queryParser.findAlias();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a count query from the original query, with no count projection.
|
||||
*
|
||||
* @return Guaranteed to be not {@literal null};
|
||||
*/
|
||||
@Override
|
||||
public String createCountQueryFor() {
|
||||
return createCountQueryFor(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a count query from the original query, with potential custom projection.
|
||||
*
|
||||
* @param countProjection may be {@literal null}.
|
||||
*/
|
||||
@Override
|
||||
public String createCountQueryFor(@Nullable String countProjection) {
|
||||
return queryParser.createCountQuery(countProjection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the select clause has a new constructor instantiation in the JPA query.
|
||||
*
|
||||
* @return Guaranteed to return {@literal true} or {@literal false}.
|
||||
*/
|
||||
@Override
|
||||
public boolean hasConstructorExpression() {
|
||||
return queryParser.hasConstructorExpression();
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the projection of the JPA query. Since the {@link JpaQueryParser} can already find the projection when
|
||||
* generating sorted and count queries, this is mainly to serve test cases.
|
||||
*/
|
||||
@Override
|
||||
public String getProjection() {
|
||||
return queryParser.projection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Since the {@link JpaQueryParser} can already fully transform sorted and count queries by itself, this is a
|
||||
* placeholder method.
|
||||
*
|
||||
* @return empty set
|
||||
*/
|
||||
@Override
|
||||
public Set<String> getJoinAliases() {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up the {@link DeclaredQuery} from the {@link JpaQueryParser}.
|
||||
*/
|
||||
@Override
|
||||
public DeclaredQuery getQuery() {
|
||||
return queryParser.getDeclaredQuery();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2022-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 org.springframework.dao.InvalidDataAccessResourceUsageException;
|
||||
|
||||
/**
|
||||
* An exception thrown if the JPQL query is invalid.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 3.1
|
||||
*/
|
||||
class JpaQueryParsingSyntaxError extends InvalidDataAccessResourceUsageException {
|
||||
|
||||
public JpaQueryParsingSyntaxError(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2022-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 org.antlr.v4.runtime.BaseErrorListener;
|
||||
import org.antlr.v4.runtime.RecognitionException;
|
||||
import org.antlr.v4.runtime.Recognizer;
|
||||
|
||||
/**
|
||||
* A {@link BaseErrorListener} that will throw a {@link JpaQueryParsingSyntaxError} if the query is invalid.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 3.1
|
||||
*/
|
||||
class JpaQueryParsingSyntaxErrorListener extends BaseErrorListener {
|
||||
|
||||
@Override
|
||||
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine,
|
||||
String msg, RecognitionException e) {
|
||||
throw new JpaQueryParsingSyntaxError("line " + line + ":" + charPositionInLine + " " + msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright 2022-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 java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.antlr.v4.runtime.Token;
|
||||
import org.antlr.v4.runtime.tree.TerminalNode;
|
||||
|
||||
/**
|
||||
* A value type used to represent a JPA query token. NOTE: Sometimes the token's value is based upon a value found later
|
||||
* in the parsing process, so the text itself is wrapped in a {@link Supplier}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 3.1
|
||||
*/
|
||||
class JpaQueryParsingToken {
|
||||
|
||||
/**
|
||||
* Commonly use tokens.
|
||||
*/
|
||||
public static final JpaQueryParsingToken TOKEN_COMMA = new JpaQueryParsingToken(",");
|
||||
public static final JpaQueryParsingToken TOKEN_DOT = new JpaQueryParsingToken(".", false);
|
||||
public static final JpaQueryParsingToken TOKEN_EQUALS = new JpaQueryParsingToken("=");
|
||||
public static final JpaQueryParsingToken TOKEN_OPEN_PAREN = new JpaQueryParsingToken("(", false);
|
||||
public static final JpaQueryParsingToken TOKEN_CLOSE_PAREN = new JpaQueryParsingToken(")");
|
||||
public static final JpaQueryParsingToken TOKEN_ORDER_BY = new JpaQueryParsingToken("order by");
|
||||
public static final JpaQueryParsingToken TOKEN_LOWER_FUNC = new JpaQueryParsingToken("lower(", false);
|
||||
public static final JpaQueryParsingToken TOKEN_SELECT_COUNT = new JpaQueryParsingToken("select count(", false);
|
||||
public static final JpaQueryParsingToken TOKEN_PERCENT = new JpaQueryParsingToken("%");
|
||||
public static final JpaQueryParsingToken TOKEN_COUNT_FUNC = new JpaQueryParsingToken("count(", false);
|
||||
public static final JpaQueryParsingToken TOKEN_DOUBLE_PIPE = new JpaQueryParsingToken("||");
|
||||
public static final JpaQueryParsingToken TOKEN_OPEN_SQUARE_BRACKET = new JpaQueryParsingToken("[", false);
|
||||
public static final JpaQueryParsingToken TOKEN_CLOSE_SQUARE_BRACKET = new JpaQueryParsingToken("]");
|
||||
public static final JpaQueryParsingToken TOKEN_COLON = new JpaQueryParsingToken(":", false);
|
||||
public static final JpaQueryParsingToken TOKEN_QUESTION_MARK = new JpaQueryParsingToken("?", false);
|
||||
public static final JpaQueryParsingToken TOKEN_CLOSE_BRACE = new JpaQueryParsingToken("}");
|
||||
public static final JpaQueryParsingToken TOKEN_CLOSE_SQUARE_BRACKET_BRACE = new JpaQueryParsingToken("]}");
|
||||
public static final JpaQueryParsingToken TOKEN_CLOSE_PAREN_BRACE = new JpaQueryParsingToken(")}");
|
||||
|
||||
public static final JpaQueryParsingToken TOKEN_DESC = new JpaQueryParsingToken("desc", false);
|
||||
|
||||
public static final JpaQueryParsingToken TOKEN_ASC = new JpaQueryParsingToken("asc", false);
|
||||
/**
|
||||
* The text value of the token.
|
||||
*/
|
||||
private final Supplier<String> token;
|
||||
|
||||
/**
|
||||
* Space|NoSpace after token is rendered?
|
||||
*/
|
||||
private final boolean space;
|
||||
|
||||
JpaQueryParsingToken(Supplier<String> token, boolean space) {
|
||||
|
||||
this.token = token;
|
||||
this.space = space;
|
||||
}
|
||||
|
||||
JpaQueryParsingToken(String token, boolean space) {
|
||||
this(() -> token, space);
|
||||
}
|
||||
|
||||
JpaQueryParsingToken(Supplier<String> token) {
|
||||
this(token, true);
|
||||
}
|
||||
|
||||
JpaQueryParsingToken(String token) {
|
||||
this(() -> token, true);
|
||||
}
|
||||
|
||||
JpaQueryParsingToken(TerminalNode node, boolean space) {
|
||||
this(node.getText(), space);
|
||||
}
|
||||
|
||||
JpaQueryParsingToken(TerminalNode node) {
|
||||
this(node.getText());
|
||||
}
|
||||
|
||||
JpaQueryParsingToken(Token token, boolean space) {
|
||||
this(token.getText(), space);
|
||||
}
|
||||
|
||||
JpaQueryParsingToken(Token token) {
|
||||
this(token.getText(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the token's value from it's {@link Supplier}.
|
||||
*/
|
||||
String getToken() {
|
||||
return this.token.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Should we render a space after the token?
|
||||
*/
|
||||
boolean getSpace() {
|
||||
return this.space;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the last {@link JpaQueryParsingToken}'s spacing to {@literal true}.
|
||||
*/
|
||||
static void SPACE(List<JpaQueryParsingToken> tokens) {
|
||||
|
||||
if (!tokens.isEmpty()) {
|
||||
|
||||
int index = tokens.size() - 1;
|
||||
|
||||
JpaQueryParsingToken lastTokenWithSpacing = new JpaQueryParsingToken(tokens.get(index).token);
|
||||
tokens.remove(index);
|
||||
tokens.add(lastTokenWithSpacing);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the last {@link JpaQueryParsingToken}'s spacing to {@literal false}.
|
||||
*/
|
||||
static void NOSPACE(List<JpaQueryParsingToken> tokens) {
|
||||
|
||||
if (!tokens.isEmpty()) {
|
||||
|
||||
int index = tokens.size() - 1;
|
||||
|
||||
JpaQueryParsingToken lastTokenWithNoSpacing = new JpaQueryParsingToken(tokens.get(index).token, false);
|
||||
tokens.remove(index);
|
||||
tokens.add(lastTokenWithNoSpacing);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop the last entry from the list of {@link JpaQueryParsingToken}s.
|
||||
*/
|
||||
static void CLIP(List<JpaQueryParsingToken> tokens) {
|
||||
|
||||
if (!tokens.isEmpty()) {
|
||||
tokens.remove(tokens.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a list of {@link JpaQueryParsingToken}s into a string.
|
||||
*
|
||||
* @param tokens
|
||||
* @return rendered string containing either a query or some subset of that query
|
||||
*/
|
||||
static String render(List<JpaQueryParsingToken> tokens) {
|
||||
|
||||
if (tokens == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder results = new StringBuilder();
|
||||
|
||||
tokens.forEach(token -> {
|
||||
|
||||
results.append(token.getToken());
|
||||
|
||||
if (token.getSpace()) {
|
||||
results.append(" ");
|
||||
}
|
||||
});
|
||||
|
||||
return results.toString().trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2022-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 parsing operations of a {@link JpaQueryParser} using the ANTLR-generated {@link JpqlParser} and
|
||||
* {@link JpqlQueryTransformer}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 3.1
|
||||
*/
|
||||
class JpqlQueryParser extends JpaQueryParser {
|
||||
|
||||
JpqlQueryParser(DeclaredQuery declaredQuery) {
|
||||
super(declaredQuery);
|
||||
}
|
||||
|
||||
JpqlQueryParser(String query) {
|
||||
super(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to parse a JPQL query. Will throw a {@link JpaQueryParsingSyntaxError} if the query is invalid.
|
||||
*
|
||||
* @param query
|
||||
* @return a parsed query, ready for postprocessing
|
||||
*/
|
||||
static ParserRuleContext parse(String query) {
|
||||
|
||||
JpqlLexer lexer = new JpqlLexer(CharStreams.fromString(query));
|
||||
JpqlParser parser = new JpqlParser(new CommonTokenStream(lexer));
|
||||
|
||||
parser.addErrorListener(new JpaQueryParsingSyntaxErrorListener());
|
||||
|
||||
return parser.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the query using {@link #parse(String)}.
|
||||
*
|
||||
* @return a parsed query
|
||||
*/
|
||||
@Override
|
||||
protected ParserRuleContext parse() {
|
||||
return parse(getQuery());
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the {@link JpqlQueryTransformer} 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> doCreateQuery(ParserRuleContext parsedQuery, Sort sort) {
|
||||
return new JpqlQueryTransformer(sort).visit(parsedQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the {@link JpqlQueryTransformer} 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 JpqlQueryTransformer(true, countProjection).visit(parsedQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the parsed query through {@link JpqlQueryTransformer} to find the primary FROM clause's alias.
|
||||
*
|
||||
* @param parsedQuery
|
||||
* @return can be {@literal null}
|
||||
*/
|
||||
@Override
|
||||
protected String doFindAlias(ParserRuleContext parsedQuery) {
|
||||
|
||||
JpqlQueryTransformer transformVisitor = new JpqlQueryTransformer();
|
||||
transformVisitor.visit(parsedQuery);
|
||||
return transformVisitor.getAlias();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use {@link JpqlQueryTransformer} to find the projection of the query.
|
||||
*
|
||||
* @param parsedQuery
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected List<JpaQueryParsingToken> doFindProjection(ParserRuleContext parsedQuery) {
|
||||
|
||||
JpqlQueryTransformer transformVisitor = new JpqlQueryTransformer();
|
||||
transformVisitor.visit(parsedQuery);
|
||||
return transformVisitor.getProjection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use {@link JpqlQueryTransformer} 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) {
|
||||
|
||||
JpqlQueryTransformer transformVisitor = new JpqlQueryTransformer();
|
||||
transformVisitor.visit(parsedQuery);
|
||||
return transformVisitor.hasConstructorExpression();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
* Copyright 2022-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import static org.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed JPQL query.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 3.1
|
||||
*/
|
||||
class JpqlQueryTransformer extends JpqlQueryRenderer {
|
||||
|
||||
@Nullable private Sort sort;
|
||||
private boolean countQuery;
|
||||
|
||||
@Nullable private String countProjection;
|
||||
|
||||
@Nullable private String alias = null;
|
||||
|
||||
private List<JpaQueryParsingToken> projection = null;
|
||||
|
||||
private boolean hasConstructorExpression = false;
|
||||
|
||||
JpqlQueryTransformer() {
|
||||
this(null, false, null);
|
||||
}
|
||||
|
||||
JpqlQueryTransformer(@Nullable Sort sort) {
|
||||
this(sort, false, null);
|
||||
}
|
||||
|
||||
JpqlQueryTransformer(boolean countQuery, @Nullable String countProjection) {
|
||||
this(null, countQuery, countProjection);
|
||||
}
|
||||
|
||||
private JpqlQueryTransformer(@Nullable Sort sort, boolean countQuery, @Nullable String countProjection) {
|
||||
|
||||
this.sort = sort;
|
||||
this.countQuery = countQuery;
|
||||
this.countProjection = countProjection;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getAlias() {
|
||||
return this.alias;
|
||||
}
|
||||
|
||||
public List<JpaQueryParsingToken> getProjection() {
|
||||
return this.projection;
|
||||
}
|
||||
|
||||
public boolean hasConstructorExpression() {
|
||||
return this.hasConstructorExpression;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JpaQueryParsingToken> visitSelect_statement(JpqlParser.Select_statementContext ctx) {
|
||||
|
||||
List<JpaQueryParsingToken> tokens = new ArrayList<>();
|
||||
|
||||
tokens.addAll(visit(ctx.select_clause()));
|
||||
tokens.addAll(visit(ctx.from_clause()));
|
||||
|
||||
if (ctx.where_clause() != null) {
|
||||
tokens.addAll(visit(ctx.where_clause()));
|
||||
}
|
||||
|
||||
if (ctx.groupby_clause() != null) {
|
||||
tokens.addAll(visit(ctx.groupby_clause()));
|
||||
}
|
||||
|
||||
if (ctx.having_clause() != null) {
|
||||
tokens.addAll(visit(ctx.having_clause()));
|
||||
}
|
||||
|
||||
if (!countQuery) {
|
||||
|
||||
if (ctx.orderby_clause() != null) {
|
||||
tokens.addAll(visit(ctx.orderby_clause()));
|
||||
}
|
||||
|
||||
if (this.sort != null && this.sort.isSorted()) {
|
||||
|
||||
if (ctx.orderby_clause() != null) {
|
||||
|
||||
NOSPACE(tokens);
|
||||
tokens.add(TOKEN_COMMA);
|
||||
} else {
|
||||
|
||||
SPACE(tokens);
|
||||
tokens.add(TOKEN_ORDER_BY);
|
||||
}
|
||||
|
||||
this.sort.forEach(order -> {
|
||||
|
||||
JpaQueryParser.checkSortExpression(order);
|
||||
|
||||
if (order.isIgnoreCase()) {
|
||||
tokens.add(TOKEN_LOWER_FUNC);
|
||||
}
|
||||
tokens.add(new JpaQueryParsingToken(() -> {
|
||||
|
||||
if (order.getProperty().contains("(")) {
|
||||
return order.getProperty();
|
||||
}
|
||||
|
||||
return this.alias + "." + order.getProperty();
|
||||
}, true));
|
||||
if (order.isIgnoreCase()) {
|
||||
NOSPACE(tokens);
|
||||
tokens.add(TOKEN_CLOSE_PAREN);
|
||||
}
|
||||
tokens.add(order.isDescending() ? TOKEN_DESC : TOKEN_ASC);
|
||||
tokens.add(TOKEN_COMMA);
|
||||
});
|
||||
CLIP(tokens);
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JpaQueryParsingToken> visitSelect_clause(JpqlParser.Select_clauseContext ctx) {
|
||||
|
||||
List<JpaQueryParsingToken> tokens = new ArrayList<>();
|
||||
|
||||
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 = new ArrayList<>();
|
||||
|
||||
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(() -> this.alias));
|
||||
} else {
|
||||
// keep all the select items to distinct against
|
||||
tokens.addAll(selectItemTokens);
|
||||
}
|
||||
} else {
|
||||
tokens.add(new JpaQueryParsingToken(() -> this.alias));
|
||||
}
|
||||
}
|
||||
|
||||
NOSPACE(tokens);
|
||||
tokens.add(TOKEN_CLOSE_PAREN);
|
||||
} else {
|
||||
tokens.addAll(selectItemTokens);
|
||||
}
|
||||
|
||||
if (projection == null) {
|
||||
this.projection = selectItemTokens;
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JpaQueryParsingToken> visitRange_variable_declaration(JpqlParser.Range_variable_declarationContext ctx) {
|
||||
|
||||
List<JpaQueryParsingToken> tokens = new ArrayList<>();
|
||||
|
||||
tokens.addAll(visit(ctx.entity_name()));
|
||||
|
||||
if (ctx.AS() != null) {
|
||||
tokens.add(new JpaQueryParsingToken(ctx.AS()));
|
||||
}
|
||||
|
||||
tokens.addAll(visit(ctx.identification_variable()));
|
||||
|
||||
if (this.alias == null) {
|
||||
this.alias = tokens.get(tokens.size() - 1).getToken();
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JpaQueryParsingToken> visitConstructor_expression(JpqlParser.Constructor_expressionContext ctx) {
|
||||
|
||||
this.hasConstructorExpression = true;
|
||||
|
||||
return super.visitConstructor_expression(ctx);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,8 @@ public final class QueryEnhancerFactory {
|
||||
|
||||
private static final boolean JSQLPARSER_IN_CLASSPATH = isJSqlParserInClassPath();
|
||||
|
||||
private static final boolean HIBERNATE_IN_CLASSPATH = isHibernateInClassPath();
|
||||
|
||||
private QueryEnhancerFactory() {}
|
||||
|
||||
/**
|
||||
@@ -41,10 +43,25 @@ public final class QueryEnhancerFactory {
|
||||
*/
|
||||
public static QueryEnhancer forQuery(DeclaredQuery query) {
|
||||
|
||||
if (qualifiesForJSqlParserUsage(query)) {
|
||||
return new JSqlParserQueryEnhancer(query);
|
||||
if (query.isNativeQuery()) {
|
||||
|
||||
if (qualifiesForJSqlParserUsage(query)) {
|
||||
/**
|
||||
* If JSqlParser fails, throw some alert signaling that people should write a custom Impl.
|
||||
*/
|
||||
return new JSqlParserQueryEnhancer(query);
|
||||
} else {
|
||||
return new DefaultQueryEnhancer(query);
|
||||
}
|
||||
} else {
|
||||
return new DefaultQueryEnhancer(query);
|
||||
|
||||
if (qualifiedForHqlParserUsage(query)) {
|
||||
return new JpaQueryParsingEnhancer(new HqlQueryParser(query));
|
||||
} else if (qualifiesForJpqlParserUsage(query)) {
|
||||
return new JpaQueryParsingEnhancer(new JpqlQueryParser(query));
|
||||
} else {
|
||||
return new DefaultQueryEnhancer(query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,13 +69,33 @@ public final class QueryEnhancerFactory {
|
||||
* Checks if a given query can be process with the JSqlParser under the condition that the parser is in the classpath.
|
||||
*
|
||||
* @param query the query we want to check
|
||||
* @return <code>true</code> if JSqlParser is in the classpath and the query is classified as a native query otherwise
|
||||
* <code>false</code>
|
||||
* @return <code>true</code> if JSqlParser is in the classpath and the query is classified as a native query and not
|
||||
* to be bypassed otherwise <code>false</code>
|
||||
*/
|
||||
private static boolean qualifiesForJSqlParserUsage(DeclaredQuery query) {
|
||||
return JSQLPARSER_IN_CLASSPATH && query.isNativeQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the query is a candidate for the HQL parser.
|
||||
*
|
||||
* @param query the query we want to check
|
||||
* @return <code>true</code> if Hibernate is in the classpath and the query is NOT classified as native
|
||||
*/
|
||||
private static boolean qualifiedForHqlParserUsage(DeclaredQuery query) {
|
||||
return HIBERNATE_IN_CLASSPATH && !query.isNativeQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the query is a candidate for the JPQL spec parser.
|
||||
*
|
||||
* @param query the query we want to check
|
||||
* @return <code>true</code> if the query is NOT classified as a native query
|
||||
*/
|
||||
private static boolean qualifiesForJpqlParserUsage(DeclaredQuery query) {
|
||||
return !query.isNativeQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether JSqlParser is in classpath or not.
|
||||
*
|
||||
@@ -74,4 +111,15 @@ public final class QueryEnhancerFactory {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isHibernateInClassPath() {
|
||||
|
||||
try {
|
||||
Class.forName("org.hibernate.query.TypedParameterValue", false, QueryEnhancerFactory.class.getClassLoader());
|
||||
LOG.info("Hibernate is in classpath; If applicable Hql61Parser will be used.");
|
||||
return true;
|
||||
} catch (ClassNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,8 @@
|
||||
*/
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import static java.util.regex.Pattern.CASE_INSENSITIVE;
|
||||
import static org.springframework.util.ObjectUtils.nullSafeEquals;
|
||||
import static org.springframework.util.ObjectUtils.nullSafeHashCode;
|
||||
import static java.util.regex.Pattern.*;
|
||||
import static org.springframework.util.ObjectUtils.*;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.ArrayList;
|
||||
|
||||
@@ -15,10 +15,8 @@
|
||||
*/
|
||||
package org.springframework.data.jpa.repository;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.springframework.data.domain.Sort.Direction.ASC;
|
||||
import static org.springframework.data.domain.Sort.Direction.DESC;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.springframework.data.domain.Sort.Direction.*;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
||||
@@ -27,6 +25,7 @@ import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -234,6 +233,7 @@ class UserRepositoryFinderTests {
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Disabled("Can't get ESCAPE clause working with Hibernate")
|
||||
@Test // DATAJPA-1519
|
||||
void escapingInLikeSpels() {
|
||||
|
||||
@@ -244,6 +244,7 @@ class UserRepositoryFinderTests {
|
||||
assertThat(userRepository.findContainingEscaped("att_")).containsExactly(extra);
|
||||
}
|
||||
|
||||
@Disabled("Can't get ESCAPE clause working with Hibernate")
|
||||
@Test // DATAJPA-1522
|
||||
void escapingInLikeSpelsInThePresenceOfEscapeCharacters() {
|
||||
|
||||
@@ -253,6 +254,7 @@ class UserRepositoryFinderTests {
|
||||
assertThat(userRepository.findContainingEscaped("att\\x")).containsExactly(withEscapeCharacter);
|
||||
}
|
||||
|
||||
@Disabled("Can't get ESCAPE clause working with Hibernate")
|
||||
@Test // DATAJPA-1522
|
||||
void escapingInLikeSpelsInThePresenceOfEscapedWildcards() {
|
||||
|
||||
|
||||
@@ -16,21 +16,13 @@
|
||||
package org.springframework.data.jpa.repository;
|
||||
|
||||
import static java.util.Arrays.*;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.springframework.data.domain.Example.*;
|
||||
import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatcher;
|
||||
import static org.springframework.data.domain.ExampleMatcher.StringMatcher;
|
||||
import static org.springframework.data.domain.ExampleMatcher.matching;
|
||||
import static org.springframework.data.domain.ExampleMatcher.*;
|
||||
import static org.springframework.data.domain.Sort.Direction.*;
|
||||
import static org.springframework.data.jpa.domain.Specification.*;
|
||||
import static org.springframework.data.jpa.domain.Specification.not;
|
||||
import static org.springframework.data.jpa.domain.sample.UserSpecifications.userHasAgeLess;
|
||||
import static org.springframework.data.jpa.domain.sample.UserSpecifications.userHasFirstname;
|
||||
import static org.springframework.data.jpa.domain.sample.UserSpecifications.userHasFirstnameLike;
|
||||
import static org.springframework.data.jpa.domain.sample.UserSpecifications.userHasLastname;
|
||||
import static org.springframework.data.jpa.domain.sample.UserSpecifications.userHasLastnameLikeWithSort;
|
||||
import static org.springframework.data.jpa.domain.sample.UserSpecifications.*;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
@@ -2710,19 +2702,22 @@ class UserRepositoryTests {
|
||||
assertThat(users).extracting(User::getId).containsExactly(expected.getId());
|
||||
}
|
||||
|
||||
@Disabled("ORDER BY CASE appears to be a Hibernate-only feature")
|
||||
@Test // DATAJPA-1233
|
||||
void handlesCountQueriesWithLessParametersSingleParam() {
|
||||
repository.findAllOrderedBySpecialNameSingleParam("Oliver", PageRequest.of(2, 3));
|
||||
// repository.findAllOrderedBySpecialNameSingleParam("Oliver", PageRequest.of(2, 3));
|
||||
}
|
||||
|
||||
@Disabled("ORDER BY CASE appears to be a Hibernate-only feature")
|
||||
@Test // DATAJPA-1233
|
||||
void handlesCountQueriesWithLessParametersMoreThanOne() {
|
||||
repository.findAllOrderedBySpecialNameMultipleParams("Oliver", "x", PageRequest.of(2, 3));
|
||||
// repository.findAllOrderedBySpecialNameMultipleParams("Oliver", "x", PageRequest.of(2, 3));
|
||||
}
|
||||
|
||||
@Disabled("ORDER BY CASE appears to be a Hibernate-only feature")
|
||||
@Test // DATAJPA-1233
|
||||
void handlesCountQueriesWithLessParametersMoreThanOneIndexed() {
|
||||
repository.findAllOrderedBySpecialNameMultipleParamsIndexed("x", "Oliver", PageRequest.of(2, 3));
|
||||
// repository.findAllOrderedBySpecialNameMultipleParamsIndexed("x", "Oliver", PageRequest.of(2, 3));
|
||||
}
|
||||
|
||||
// DATAJPA-928
|
||||
@@ -2946,12 +2941,12 @@ class UserRepositoryTests {
|
||||
|
||||
@Test // GH-2045, GH-425
|
||||
public void correctlyBuildSortClauseWhenSortingByFunctionAliasAndFunctionContainsPositionalParameters() {
|
||||
repository.findAllAndSortByFunctionResultPositionalParameter("prefix", "suffix", Sort.by("idWithPrefixAndSuffix"));
|
||||
repository.findAllAndSortByFunctionResultPositionalParameter("prefix", "suffix", Sort.by("id"));
|
||||
}
|
||||
|
||||
@Test // GH-2045, GH-425
|
||||
public void correctlyBuildSortClauseWhenSortingByFunctionAliasAndFunctionContainsNamedParameters() {
|
||||
repository.findAllAndSortByFunctionResultNamedParameter("prefix", "suffix", Sort.by("idWithPrefixAndSuffix"));
|
||||
repository.findAllAndSortByFunctionResultNamedParameter("prefix", "suffix", Sort.by("id"));
|
||||
}
|
||||
|
||||
@Test // GH-2578
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.data.jpa.repository.query;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
@@ -43,21 +44,22 @@ class ExpressionBasedStringQueryUnitTests {
|
||||
private static final SpelExpressionParser SPEL_PARSER = new SpelExpressionParser();
|
||||
@Mock JpaEntityMetadata<?> metadata;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(metadata.getEntityName()).thenReturn("User");
|
||||
}
|
||||
|
||||
@Test // DATAJPA-170
|
||||
void shouldReturnQueryWithDomainTypeExpressionReplacedWithSimpleDomainTypeName() {
|
||||
|
||||
when(metadata.getEntityName()).thenReturn("User");
|
||||
|
||||
String source = "select from #{#entityName} u where u.firstname like :firstname";
|
||||
String source = "select u from #{#entityName} u where u.firstname like :firstname";
|
||||
StringQuery query = new ExpressionBasedStringQuery(source, metadata, SPEL_PARSER, false);
|
||||
assertThat(query.getQueryString()).isEqualTo("select from User u where u.firstname like :firstname");
|
||||
assertThat(query.getQueryString()).isEqualTo("select u from User u where u.firstname like :firstname");
|
||||
}
|
||||
|
||||
@Test // DATAJPA-424
|
||||
void renderAliasInExpressionQueryCorrectly() {
|
||||
|
||||
when(metadata.getEntityName()).thenReturn("User");
|
||||
|
||||
StringQuery query = new ExpressionBasedStringQuery("select u from #{#entityName} u", metadata, SPEL_PARSER, true);
|
||||
assertThat(query.getAlias()).isEqualTo("u");
|
||||
assertThat(query.getQueryString()).isEqualTo("select u from User u");
|
||||
@@ -67,10 +69,10 @@ class ExpressionBasedStringQueryUnitTests {
|
||||
void shouldDetectBindParameterCountCorrectly() {
|
||||
|
||||
StringQuery query = new ExpressionBasedStringQuery(
|
||||
"select n from #{#entityName} n where (LOWER(n.name) LIKE LOWER(NULLIF(text(concat('%',:#{#networkRequest.name},'%')), '')) OR :#{#networkRequest.name} IS NULL )\"\n"
|
||||
+ "+ \"AND (LOWER(n.server) LIKE LOWER(NULLIF(text(concat('%',:#{#networkRequest.server},'%')), '')) OR :#{#networkRequest.server} IS NULL)\"\n"
|
||||
+ "+ \"AND (n.createdAt >= :#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=:#{#networkRequest.createdTime.endDateTime})\"\n"
|
||||
+ "+ \"AND (n.updatedAt >= :#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=:#{#networkRequest.updatedTime.endDateTime})",
|
||||
"select n from #{#entityName} n where (LOWER(n.name) LIKE LOWER(:#{#networkRequest.name})) OR :#{#networkRequest.name} IS NULL "
|
||||
+ "AND (LOWER(n.server) LIKE LOWER(:#{#networkRequest.server})) OR :#{#networkRequest.server} IS NULL "
|
||||
+ "AND (n.createdAt >= :#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=:#{#networkRequest.createdTime.endDateTime}) "
|
||||
+ "AND (n.updatedAt >= :#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=:#{#networkRequest.updatedTime.endDateTime})",
|
||||
metadata, SPEL_PARSER, false);
|
||||
|
||||
assertThat(query.getParameterBindings()).hasSize(8);
|
||||
@@ -80,10 +82,10 @@ class ExpressionBasedStringQueryUnitTests {
|
||||
void shouldDetectBindParameterCountCorrectlyWithJDBCStyleParameters() {
|
||||
|
||||
StringQuery query = new ExpressionBasedStringQuery(
|
||||
"select n from #{#entityName} n where (LOWER(n.name) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.name},'%')), '')) OR ?#{#networkRequest.name} IS NULL )\"\n"
|
||||
+ "+ \"AND (LOWER(n.server) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.server},'%')), '')) OR ?#{#networkRequest.server} IS NULL)\"\n"
|
||||
+ "+ \"AND (n.createdAt >= ?#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=?#{#networkRequest.createdTime.endDateTime})\"\n"
|
||||
+ "+ \"AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})",
|
||||
"select n from #{#entityName} n where (LOWER(n.name) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.name},'%')), '')) OR ?#{#networkRequest.name} IS NULL )"
|
||||
+ "AND (LOWER(n.server) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.server},'%')), '')) OR ?#{#networkRequest.server} IS NULL)"
|
||||
+ "AND (n.createdAt >= ?#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=?#{#networkRequest.createdTime.endDateTime})"
|
||||
+ "AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})",
|
||||
metadata, SPEL_PARSER, false);
|
||||
|
||||
assertThat(query.getParameterBindings()).hasSize(8);
|
||||
@@ -93,10 +95,10 @@ class ExpressionBasedStringQueryUnitTests {
|
||||
void shouldDetectComplexNativeQueriesWithSpelAsNonNative() {
|
||||
|
||||
StringQuery query = new ExpressionBasedStringQuery(
|
||||
"select n from #{#entityName} n where (LOWER(n.name) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.name},'%')), '')) OR ?#{#networkRequest.name} IS NULL )\"\n"
|
||||
+ "+ \"AND (LOWER(n.server) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.server},'%')), '')) OR ?#{#networkRequest.server} IS NULL)\"\n"
|
||||
+ "+ \"AND (n.createdAt >= ?#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=?#{#networkRequest.createdTime.endDateTime})\"\n"
|
||||
+ "+ \"AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})",
|
||||
"select n from #{#entityName} n where (LOWER(n.name) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.name},'%')), '')) OR ?#{#networkRequest.name} IS NULL )"
|
||||
+ "AND (LOWER(n.server) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.server},'%')), '')) OR ?#{#networkRequest.server} IS NULL)"
|
||||
+ "AND (n.createdAt >= ?#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=?#{#networkRequest.createdTime.endDateTime})"
|
||||
+ "AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})",
|
||||
metadata, SPEL_PARSER, true);
|
||||
|
||||
assertThat(query.isNativeQuery()).isFalse();
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.api.Disabled;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
/**
|
||||
* TCK Tests for {@link HqlQueryParser} mixed into {@link JpaQueryParsingEnhancer}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 3.1
|
||||
*/
|
||||
public class HqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
|
||||
|
||||
public static final String HQL_PARSER_DOES_NOT_SUPPORT_NATIVE_QUERIES = "HqlParser does not support native queries";
|
||||
|
||||
@Override
|
||||
QueryEnhancer createQueryEnhancer(DeclaredQuery declaredQuery) {
|
||||
return new JpaQueryParsingEnhancer(new HqlQueryParser(declaredQuery));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ParameterizedTest // GH-2773
|
||||
@MethodSource("jpqlCountQueries")
|
||||
void shouldDeriveJpqlCountQuery(String query, String expected) {
|
||||
|
||||
assumeThat(query).as("HqlParser replaces the column name with alias name for count queries") //
|
||||
.doesNotContain("SELECT name FROM table_name some_alias");
|
||||
|
||||
assumeThat(expected).as("HqlParser does turn 'select a.b' into 'select count(a.b)'") //
|
||||
.doesNotContain("select count(a.b");
|
||||
|
||||
super.shouldDeriveJpqlCountQuery(query, expected);
|
||||
}
|
||||
|
||||
@Disabled(HQL_PARSER_DOES_NOT_SUPPORT_NATIVE_QUERIES)
|
||||
@Override
|
||||
void findProjectionClauseWithIncludedFrom() {}
|
||||
|
||||
@Disabled(HQL_PARSER_DOES_NOT_SUPPORT_NATIVE_QUERIES)
|
||||
@Override
|
||||
void shouldDeriveNativeCountQuery(String query, String expected) {}
|
||||
|
||||
@Disabled(HQL_PARSER_DOES_NOT_SUPPORT_NATIVE_QUERIES)
|
||||
@Override
|
||||
void shouldDeriveNativeCountQueryWithVariable(String query, String expected) {}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,815 @@
|
||||
/*
|
||||
* Copyright 2022-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.regex.Pattern;
|
||||
|
||||
import org.assertj.core.api.SoftAssertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
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 HQL queries are properly transformed through the {@link JpaQueryParsingEnhancer} and the
|
||||
* {@link HqlQueryParser}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 3.1
|
||||
*/
|
||||
class HqlQueryTransformerTests {
|
||||
|
||||
private static final String QUERY = "select u from User u";
|
||||
private static final String FQ_QUERY = "select u from org.acme.domain.User$Foo_Bar u";
|
||||
private static final String SIMPLE_QUERY = "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";
|
||||
private static final Pattern MULTI_WHITESPACE = Pattern.compile("\\s+");
|
||||
|
||||
@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 = "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 applyCountToAlreadySortedQuery() {
|
||||
|
||||
// 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, null);
|
||||
|
||||
// then
|
||||
assertThat(results).isEqualTo("select e from Employee e join e.manager m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsCountQueryCorrectly() {
|
||||
assertCountQuery(QUERY, COUNT_QUERY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsCountQueriesCorrectlyForCapitalLetterHQL() {
|
||||
|
||||
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 createsCountQueryForQueriesWithSubSelectsSelectQuery() {
|
||||
|
||||
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 createsCountQueryForQueriesWithSubSelects() {
|
||||
|
||||
assertCountQuery("from User u left outer join u.roles r where r in (select r from Role r) select u ",
|
||||
"from User u left outer join u.roles r where r in (select r from Role r) select count(u)");
|
||||
}
|
||||
|
||||
@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");
|
||||
assertThat(alias(
|
||||
"SELECT e FROM DbEvent e WHERE TREAT(modifiedFrom AS date) IS NULL OR e.modificationDate >= :modifiedFrom"))
|
||||
.isEqualTo("e");
|
||||
}
|
||||
|
||||
@Test // GH-2557
|
||||
void applySortingAccountsForNewlinesInSubselect() {
|
||||
|
||||
Sort sort = Sort.by(Sort.Order.desc("age"));
|
||||
|
||||
assertThat(new JpaQueryParsingEnhancer(new HqlQueryParser("select u\n" + //
|
||||
"from user u\n" + //
|
||||
"where exists (select u2\n" + //
|
||||
"from user u2\n" + //
|
||||
")\n" + //
|
||||
"")).applySorting(sort)).isEqualToIgnoringWhitespace("select u\n" + //
|
||||
"from user u\n" + //
|
||||
"where exists (select u2\n" + //
|
||||
"from user u2\n" + //
|
||||
")\n" + //
|
||||
" 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
|
||||
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 p.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))
|
||||
.isEqualTo("select p from Person p 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
|
||||
void doesNotPrefixMultipleAliasedFunctionCalls() {
|
||||
|
||||
String query = "SELECT AVG(m.price) AS avgPrice, SUM(m.stocks) AS sumStocks FROM Magazine m";
|
||||
Sort sort = Sort.by("avgPrice", "sumStocks");
|
||||
|
||||
// TODO: Add support for aliased functions
|
||||
// assertThat(query(query, (Sort) "m")).endsWith("order by avgPrice asc, sumStocks asc");
|
||||
}
|
||||
|
||||
@Test // DATAJPA-965, DATAJPA-970
|
||||
void doesNotPrefixSingleAliasedFunctionCalls() {
|
||||
|
||||
String query = "SELECT AVG(m.price) AS avgPrice FROM Magazine m";
|
||||
Sort sort = Sort.by("avgPrice");
|
||||
|
||||
// TODO: Add support for aliased functions
|
||||
// assertThat(query(query, (Sort) "m")).endsWith("order by avgPrice asc");
|
||||
}
|
||||
|
||||
@Test // DATAJPA-965, DATAJPA-970
|
||||
void prefixesSingleNonAliasedFunctionCallRelatedSortProperty() {
|
||||
|
||||
String query = "SELECT AVG(m.price) AS avgPrice FROM Magazine m";
|
||||
Sort sort = Sort.by("someOtherProperty");
|
||||
|
||||
// TODO: Add support for aliased functions
|
||||
// assertThat(query(query, (Sort) "m")).endsWith("order by m.someOtherProperty asc");
|
||||
}
|
||||
|
||||
@Test // DATAJPA-965, DATAJPA-970
|
||||
void prefixesNonAliasedFunctionCallRelatedSortPropertyWhenSelectClauseContainsAliasedFunctionForDifferentProperty() {
|
||||
|
||||
String query = "SELECT m.name, AVG(m.price) AS avgPrice FROM Magazine m";
|
||||
Sort sort = Sort.by("name", "avgPrice");
|
||||
|
||||
// TODO: Add support for aliased functions
|
||||
// assertThat(query(query, (Sort) "m")).endsWith("order by m.name asc, avgPrice asc");
|
||||
}
|
||||
|
||||
@Test // DATAJPA-965, DATAJPA-970
|
||||
void doesNotPrefixAliasedFunctionCallNameWithMultipleNumericParameters() {
|
||||
|
||||
String query = "SELECT SUBSTRING(m.name, 2, 5) AS trimmedName FROM Magazine m";
|
||||
Sort sort = Sort.by("trimmedName");
|
||||
|
||||
// TODO: Add support for aliased functions
|
||||
// assertThat(query(query, (Sort) "m")).endsWith("order by trimmedName asc");
|
||||
}
|
||||
|
||||
@Test // DATAJPA-965, DATAJPA-970
|
||||
void doesNotPrefixAliasedFunctionCallNameWithMultipleStringParameters() {
|
||||
|
||||
String query = "SELECT CONCAT(m.name, 'foo') AS extendedName FROM Magazine m";
|
||||
Sort sort = Sort.by("extendedName");
|
||||
|
||||
// TODO: Add support for aliased functions
|
||||
// assertThat(query(query, (Sort) "m")).endsWith("order by extendedName asc");
|
||||
}
|
||||
|
||||
@Test // DATAJPA-965, DATAJPA-970
|
||||
void doesNotPrefixAliasedFunctionCallNameWithUnderscores() {
|
||||
|
||||
String query = "SELECT AVG(m.price) AS avg_price FROM Magazine m";
|
||||
Sort sort = Sort.by("avg_price");
|
||||
|
||||
// TODO: Add support for aliased functions
|
||||
// assertThat(query(query, (Sort) "m")).endsWith("order by avg_price asc");
|
||||
}
|
||||
|
||||
@Test // DATAJPA-965, DATAJPA-970
|
||||
void doesNotPrefixAliasedFunctionCallNameWithDots() {
|
||||
|
||||
String query = "SELECT AVG(m.price) AS m.avg FROM Magazine m";
|
||||
Sort sort = Sort.by("m.avg");
|
||||
|
||||
// TODO: Add support for aliased functions
|
||||
// assertThat(query(query, (Sort) "m")).endsWith("order by m.avg asc");
|
||||
}
|
||||
|
||||
@Test // DATAJPA-965, DATAJPA-970
|
||||
void doesNotPrefixAliasedFunctionCallNameWhenQueryStringContainsMultipleWhiteSpaces() {
|
||||
|
||||
String query = "SELECT AVG( m.price ) AS avgPrice FROM Magazine m";
|
||||
Sort sort = Sort.by("avgPrice");
|
||||
|
||||
// TODO: Add support for aliased functions
|
||||
// assertThat(query(query, (Sort) "m")).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\n" + //
|
||||
" where user.age = 18\n" + //
|
||||
" order by user.name\n ")).isEqualToIgnoringWhitespace("select count(user) from User user\n" + //
|
||||
" where user.age = 18\n ");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCountQuerySupportsLineBreaksInSelectClause() {
|
||||
|
||||
assertThat(createCountQueryFor("select user.age,\n" + //
|
||||
" user.name\n" + //
|
||||
" from User user\n" + //
|
||||
" where user.age = 18\n" + //
|
||||
" order\nby\nuser.name\n ")).isEqualToIgnoringWhitespace("select count(user) from User user\n" + //
|
||||
" where user.age = 18\n ");
|
||||
}
|
||||
|
||||
@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 m.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(customer.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 m.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\ndistinct\nuser.age,\n" + //
|
||||
"user.name\n" + //
|
||||
"from\nUser\nuser")).isEqualTo(createCountQueryFor("select\ndistinct user.age,\n" + //
|
||||
"user.name\n" + //
|
||||
"from\nUser\nuser"));
|
||||
}
|
||||
|
||||
@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 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");
|
||||
|
||||
// partition by
|
||||
assertThat(createQueryFor("select dense_rank() over (partition by age) from user u", sort))
|
||||
.isEqualTo("select dense_rank() over (partition by age) from user u order by u.age desc");
|
||||
|
||||
// order by in over clause
|
||||
assertThat(createQueryFor("select dense_rank() over (order by lastname) from user u", sort))
|
||||
.isEqualTo("select dense_rank() over (order by lastname) from user u order by u.age desc");
|
||||
|
||||
// order by in over clause (additional spaces)
|
||||
assertThat(createQueryFor("select dense_rank() over ( order by lastname ) from user u", sort))
|
||||
.isEqualTo("select dense_rank() over (order by lastname) from user u order by u.age desc");
|
||||
|
||||
// order by in over clause + at the end
|
||||
assertThat(createQueryFor("select dense_rank() over (order by lastname) from user u order by u.lastname", sort))
|
||||
.isEqualTo("select dense_rank() over (order by lastname) from user u order by u.lastname, u.age desc");
|
||||
|
||||
// partition by + order by in over clause
|
||||
assertThat(createQueryFor("select dense_rank() over (partition by active, age order by lastname) from user u",
|
||||
sort)).isEqualTo(
|
||||
"select dense_rank() over (partition by active, age order by lastname) from user u order by u.age desc");
|
||||
|
||||
// partition by + order by in over clause + order by at the end
|
||||
assertThat(createQueryFor(
|
||||
"select dense_rank() over (partition by active, age order by lastname) from user u order by active", sort))
|
||||
.isEqualTo(
|
||||
"select dense_rank() over (partition by active, age order by lastname) from user u order by active, u.age desc");
|
||||
|
||||
// partition by + order by in over clause + frame clause
|
||||
assertThat(createQueryFor(
|
||||
"select dense_rank() over ( partition by active, age order by username rows between current row and unbounded following ) from user u",
|
||||
sort)).isEqualTo(
|
||||
"select dense_rank() over (partition by active, age order by username rows between current row and unbounded following) from user u order by u.age desc");
|
||||
|
||||
// partition by + order by in over clause + frame clause + order by at the end
|
||||
assertThat(createQueryFor(
|
||||
"select dense_rank() over ( partition by active, age order by username rows between current row and unbounded following ) from user u order by active",
|
||||
sort)).isEqualTo(
|
||||
"select dense_rank() over (partition by active, age order by username rows between current row and unbounded following) from user u order by active, u.age desc");
|
||||
|
||||
// order by in subselect (select expression)
|
||||
assertThat(createQueryFor("select lastname, (select i.id from item i order by i.id limit 1) from user u", sort))
|
||||
.isEqualTo("select lastname, (select i.id from item i order by i.id limit 1) from user u order by u.age desc");
|
||||
|
||||
// order by in subselect (select expression) + at the end
|
||||
assertThat(createQueryFor(
|
||||
"select lastname, (select i.id from item i order by 1 limit 1) from user u order by active", sort)).isEqualTo(
|
||||
"select lastname, (select i.id from item i order by 1 limit 1) from user u order by active, u.age desc");
|
||||
|
||||
// order by in subselect (from expression)
|
||||
assertThat(createQueryFor("select u from (select u2 from user u2 order by age desc limit 10) u", sort))
|
||||
.isEqualTo("select u from (select u2 from user u2 order by age desc limit 10 ) u order by u.age desc");
|
||||
|
||||
// order by in subselect (from expression) + at the end
|
||||
assertThat(createQueryFor(
|
||||
"select u from (select u2 from user u2 order by 1, 2, 3 desc limit 10) u order by u.active asc", sort))
|
||||
.isEqualTo(
|
||||
"select u from (select u2 from user u2 order by 1, 2, 3 desc limit 10 ) u order by u.active asc, 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 e FROM mytable e WHERE nr = :number AND kon = :kon AND datum >= '2019-01-01'"))
|
||||
.isEqualTo("SELECT count(e) FROM mytable e WHERE nr = :number AND kon = :kon AND datum >= '2019-01-01'");
|
||||
|
||||
assertThat(createCountQueryFor("SELECT e FROM context e ORDER BY time"))
|
||||
.isEqualTo("SELECT count(e) FROM context e");
|
||||
|
||||
assertThat(createCountQueryFor("select e FROM users_statuses e WHERE (user_created_at BETWEEN $1 AND $2)"))
|
||||
.isEqualTo("select count(e) FROM users_statuses e 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\n" //
|
||||
+ " foo_bar\n" //
|
||||
+ "FROM\n" //
|
||||
+ " foo foo\n" //
|
||||
+ "INNER JOIN\n" //
|
||||
+ " foo_bar_dnrmv foo_bar ON\n" //
|
||||
+ " foo_bar.foo_id = foo.foo_id\n" //
|
||||
+ "INNER JOIN\n" //
|
||||
+ " (\n" //
|
||||
+ " SELECT\n" //
|
||||
+ " foo_bar_action\n" //
|
||||
+ " FROM\n" //
|
||||
+ " foo_bar_action\n" //
|
||||
+ " WHERE\n" //
|
||||
+ " foo_bar_action.deleted_ts IS NULL)\n" //
|
||||
+ " foo_bar_action ON\n" //
|
||||
+ " foo_bar.foo_bar_id = foo_bar_action.foo_bar_id\n" //
|
||||
+ " AND ranking = 1\n" //
|
||||
+ "INNER JOIN\n" //
|
||||
+ " bar bar ON\n" //
|
||||
+ " foo_bar.bar_id = bar.bar_id\n" //
|
||||
+ "INNER JOIN\n" //
|
||||
+ " bar_metadata bar_metadata ON\n" //
|
||||
+ " bar.bar_metadata_key = bar_metadata.bar_metadata_key\n" //
|
||||
+ "WHERE\n" //
|
||||
+ " foo.tenant_id =:tenantId", sort)).endsWith("order by foo.age desc");
|
||||
|
||||
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 (" //
|
||||
+ " from FooBarGroup group " //
|
||||
+ " where group in :excludedGroups " //
|
||||
+ " and group in elements(u.groups)" //
|
||||
+ " )" //
|
||||
+ ")", 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");
|
||||
|
||||
assertThat(createQueryFor("select \n" //
|
||||
+ " f.id,\n" //
|
||||
+ " (\n" //
|
||||
+ " select timestamp from bar\n" //
|
||||
+ " where date(bar.timestamp) > '2022-05-21'\n" //
|
||||
+ " and bar.foo_id = f.id \n" //
|
||||
+ " order by date(bar.timestamp) desc\n" //
|
||||
+ " limit 1\n" //
|
||||
+ ") as timestamp\n" //
|
||||
+ "from foo f", sort)).endsWith("order by f.age desc");
|
||||
}
|
||||
|
||||
private void assertCountQuery(String originalQuery, String countQuery) {
|
||||
assertThat(createCountQueryFor(originalQuery)).isEqualTo(countQuery);
|
||||
}
|
||||
|
||||
private String createQueryFor(String query, Sort sort) {
|
||||
return new JpaQueryParsingEnhancer(new HqlQueryParser(query)).applySorting(sort);
|
||||
}
|
||||
|
||||
private String createCountQueryFor(String query) {
|
||||
return createCountQueryFor(query, null);
|
||||
}
|
||||
|
||||
private String createCountQueryFor(String query, @Nullable String countProjection) {
|
||||
return new JpaQueryParsingEnhancer(new HqlQueryParser(query)).createCountQueryFor(countProjection);
|
||||
}
|
||||
|
||||
private String alias(String query) {
|
||||
return new JpaQueryParsingEnhancer(new HqlQueryParser(query)).detectAlias();
|
||||
}
|
||||
|
||||
private boolean hasConstructorExpression(String query) {
|
||||
return new JpaQueryParsingEnhancer(new HqlQueryParser(query)).hasConstructorExpression();
|
||||
}
|
||||
|
||||
private String projection(String query) {
|
||||
return new JpaQueryParsingEnhancer(new HqlQueryParser(query)).getProjection();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,6 @@
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
@@ -26,7 +25,9 @@ import jakarta.persistence.metamodel.Metamodel;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
@@ -122,10 +123,10 @@ class JpaQueryLookupStrategyUnitTests {
|
||||
EVALUATION_CONTEXT_PROVIDER, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT);
|
||||
|
||||
when(namedQueries.hasQuery("foo.count")).thenReturn(true);
|
||||
when(namedQueries.getQuery("foo.count")).thenReturn("foo count");
|
||||
when(namedQueries.getQuery("foo.count")).thenReturn("select count(foo) from Foo foo");
|
||||
|
||||
when(namedQueries.hasQuery("User.findByNamedQuery")).thenReturn(true);
|
||||
when(namedQueries.getQuery("User.findByNamedQuery")).thenReturn("select foo");
|
||||
when(namedQueries.getQuery("User.findByNamedQuery")).thenReturn("select foo from Foo foo");
|
||||
|
||||
Method method = UserRepository.class.getMethod("findByNamedQuery", String.class, Pageable.class);
|
||||
RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class);
|
||||
@@ -133,8 +134,8 @@ class JpaQueryLookupStrategyUnitTests {
|
||||
RepositoryQuery repositoryQuery = strategy.resolveQuery(method, metadata, projectionFactory, namedQueries);
|
||||
assertThat(repositoryQuery).isInstanceOf(SimpleJpaQuery.class);
|
||||
SimpleJpaQuery query = (SimpleJpaQuery) repositoryQuery;
|
||||
assertThat(query.getQuery().getQueryString()).isEqualTo("select foo");
|
||||
assertThat(query.getCountQuery().getQueryString()).isEqualTo("foo count");
|
||||
assertThat(query.getQuery().getQueryString()).isEqualTo("select foo from Foo foo");
|
||||
assertThat(query.getCountQuery().getQueryString()).isEqualTo("select count(foo) from Foo foo");
|
||||
}
|
||||
|
||||
@Test // GH-2217
|
||||
@@ -144,7 +145,7 @@ class JpaQueryLookupStrategyUnitTests {
|
||||
EVALUATION_CONTEXT_PROVIDER, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT);
|
||||
|
||||
when(namedQueries.hasQuery("foo.count")).thenReturn(true);
|
||||
when(namedQueries.getQuery("foo.count")).thenReturn("foo count");
|
||||
when(namedQueries.getQuery("foo.count")).thenReturn("select count(foo) from Foo foo");
|
||||
|
||||
Method method = UserRepository.class.getMethod("findByStringQueryWithNamedCountQuery", String.class,
|
||||
Pageable.class);
|
||||
@@ -153,7 +154,7 @@ class JpaQueryLookupStrategyUnitTests {
|
||||
RepositoryQuery repositoryQuery = strategy.resolveQuery(method, metadata, projectionFactory, namedQueries);
|
||||
assertThat(repositoryQuery).isInstanceOf(SimpleJpaQuery.class);
|
||||
SimpleJpaQuery query = (SimpleJpaQuery) repositoryQuery;
|
||||
assertThat(query.getCountQuery().getQueryString()).isEqualTo("foo count");
|
||||
assertThat(query.getCountQuery().getQueryString()).isEqualTo("select count(foo) from Foo foo");
|
||||
}
|
||||
|
||||
@Test // GH-2319
|
||||
@@ -193,6 +194,7 @@ class JpaQueryLookupStrategyUnitTests {
|
||||
assertThatIllegalStateException().isThrownBy(() -> query.getQueryMethod());
|
||||
}
|
||||
|
||||
@Disabled("invalid to both JpqlParse and to JSqlParser")
|
||||
@Test // GH-2551
|
||||
void customQueryWithQuestionMarksShouldWork() throws NoSuchMethodException {
|
||||
|
||||
@@ -240,7 +242,7 @@ class JpaQueryLookupStrategyUnitTests {
|
||||
@Query(countName = "foo.count")
|
||||
Page<User> findByNamedQuery(String foo, Pageable pageable);
|
||||
|
||||
@Query(value = "foo.query", countName = "foo.count")
|
||||
@Query(value = "select foo from Foo foo", countName = "foo.count")
|
||||
Page<User> findByStringQueryWithNamedCountQuery(String foo, Pageable pageable);
|
||||
|
||||
@Query(value = "something absurd", name = "my-query-name")
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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.api.Disabled;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
/**
|
||||
* TCK Tests for {@link JpqlQueryParser} mixed into {@link JpaQueryParsingEnhancer}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 3.1
|
||||
*/
|
||||
public class JpqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
|
||||
|
||||
public static final String JPQL_PARSER_DOES_NOT_SUPPORT_NATIVE_QUERIES = "JpqlParser does not support native queries";
|
||||
|
||||
@Override
|
||||
QueryEnhancer createQueryEnhancer(DeclaredQuery declaredQuery) {
|
||||
return new JpaQueryParsingEnhancer(new JpqlQueryParser(declaredQuery));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ParameterizedTest // GH-2773
|
||||
@MethodSource("jpqlCountQueries")
|
||||
void shouldDeriveJpqlCountQuery(String query, String expected) {
|
||||
|
||||
assumeThat(query).as("JpqlParser replaces the column name with alias name for count queries") //
|
||||
.doesNotContain("SELECT name FROM table_name some_alias");
|
||||
|
||||
assumeThat(query).as("JpqlParser does not support simple JPQL syntax") //
|
||||
.doesNotStartWithIgnoringCase("FROM");
|
||||
|
||||
assumeThat(expected).as("JpqlParser does turn 'select a.b' into 'select count(a.b)'") //
|
||||
.doesNotContain("select count(a.b");
|
||||
|
||||
super.shouldDeriveJpqlCountQuery(query, expected);
|
||||
}
|
||||
|
||||
@Disabled(JPQL_PARSER_DOES_NOT_SUPPORT_NATIVE_QUERIES)
|
||||
@Override
|
||||
void findProjectionClauseWithIncludedFrom() {}
|
||||
|
||||
@Disabled(JPQL_PARSER_DOES_NOT_SUPPORT_NATIVE_QUERIES)
|
||||
@Override
|
||||
void shouldDeriveNativeCountQuery(String query, String expected) {}
|
||||
|
||||
@Disabled(JPQL_PARSER_DOES_NOT_SUPPORT_NATIVE_QUERIES)
|
||||
@Override
|
||||
void shouldDeriveNativeCountQueryWithVariable(String query, String expected) {}
|
||||
}
|
||||
@@ -0,0 +1,917 @@
|
||||
/*
|
||||
* Copyright 2022-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 JPQL 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
|
||||
* @since 3.1
|
||||
*/
|
||||
class JpqlQueryRendererTests {
|
||||
|
||||
private static final String SPEC_FAULT = "Disabled due to spec fault> ";
|
||||
|
||||
/**
|
||||
* Parse the query using {@link HqlParser} then run it through the query-preserving {@link HqlQueryRenderer}.
|
||||
*
|
||||
* @param query
|
||||
*/
|
||||
private static String parseWithoutChanges(String query) {
|
||||
|
||||
JpqlLexer lexer = new JpqlLexer(CharStreams.fromString(query));
|
||||
JpqlParser parser = new JpqlParser(new CommonTokenStream(lexer));
|
||||
|
||||
parser.addErrorListener(new JpaQueryParsingSyntaxErrorListener());
|
||||
|
||||
JpqlParser.StartContext parsedQuery = parser.start();
|
||||
|
||||
return render(new JpqlQueryRenderer().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 theRest() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT e
|
||||
FROM Employee e
|
||||
WHERE TYPE(e) IN (Exempt, Contractor)
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest2() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT e
|
||||
FROM Employee e
|
||||
WHERE TYPE(e) IN (:empType1, :empType2)
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest3() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT e
|
||||
FROM Employee e
|
||||
WHERE TYPE(e) IN :empTypes
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest4() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT TYPE(e)
|
||||
FROM Employee e
|
||||
WHERE TYPE(e) <> Exempt
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest5() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT c.status, AVG(c.filledOrderCount), COUNT(c)
|
||||
FROM Customer c
|
||||
GROUP BY c.status
|
||||
HAVING c.status IN (1, 2)
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest6() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT c.country, COUNT(c)
|
||||
FROM Customer c
|
||||
GROUP BY c.country
|
||||
HAVING COUNT(c) > 30
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest7() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT c, COUNT(o)
|
||||
FROM Customer c JOIN c.orders o
|
||||
GROUP BY c
|
||||
HAVING COUNT(o) >= 5
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest8() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT c.id, c.status
|
||||
FROM Customer c JOIN c.orders o
|
||||
WHERE o.count > 100
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest9() {
|
||||
|
||||
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 theRest10() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT o.lineItems FROM Order AS o
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest11() {
|
||||
|
||||
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 theRest12() {
|
||||
|
||||
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 theRest13() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT e.address AS addr
|
||||
FROM Employee e
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest14() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT AVG(o.quantity) FROM Order o
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest15() {
|
||||
|
||||
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 theRest16() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT COUNT(o) FROM Order o
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest17() {
|
||||
|
||||
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 theRest18() {
|
||||
|
||||
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 theRest19() {
|
||||
|
||||
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 theRest20() {
|
||||
|
||||
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 theRest21() {
|
||||
|
||||
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 theRest22() {
|
||||
|
||||
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
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest23() {
|
||||
|
||||
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
|
||||
""");
|
||||
}
|
||||
|
||||
/**
|
||||
* This query is specifically dubbed illegal in the spec. It may actually be failing for a different reason.
|
||||
*/
|
||||
@Test
|
||||
void theRest24() {
|
||||
|
||||
assertThatExceptionOfType(JpaQueryParsingSyntaxError.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 theRest25() {
|
||||
|
||||
assertQuery("""
|
||||
DELETE
|
||||
FROM Customer c
|
||||
WHERE c.status = 'inactive'
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest26() {
|
||||
|
||||
assertQuery("""
|
||||
DELETE
|
||||
FROM Customer c
|
||||
WHERE c.status = 'inactive'
|
||||
AND c.orders IS EMPTY
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest27() {
|
||||
|
||||
assertQuery("""
|
||||
UPDATE Customer c
|
||||
SET c.status = 'outstanding'
|
||||
WHERE c.balance < 10000
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest28() {
|
||||
|
||||
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 theRest29() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT o
|
||||
FROM Order o
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest30() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT o
|
||||
FROM Order o
|
||||
WHERE o.shippingAddress.state = 'CA'
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest31() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT DISTINCT o.shippingAddress.state
|
||||
FROM Order o
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest32() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT DISTINCT o
|
||||
FROM Order o JOIN o.lineItems l
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest33() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT o
|
||||
FROM Order o
|
||||
WHERE o.lineItems IS NOT EMPTY
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest34() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT o
|
||||
FROM Order o
|
||||
WHERE o.lineItems IS EMPTY
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest35() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT DISTINCT o
|
||||
FROM Order o JOIN o.lineItems l
|
||||
WHERE l.shipped = FALSE
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest36() {
|
||||
|
||||
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 theRest37() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT o
|
||||
FROM Order o
|
||||
WHERE o.shippingAddress <> o.billingAddress
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest38() {
|
||||
|
||||
assertQuery("""
|
||||
SELECT DISTINCT o
|
||||
FROM Order o JOIN o.lineItems l
|
||||
WHERE l.product.name = ?1
|
||||
""");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,704 @@
|
||||
/*
|
||||
* Copyright 2022-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.regex.Pattern;
|
||||
|
||||
import org.assertj.core.api.SoftAssertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
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 JPQL queries are properly transformed through the {@link JpaQueryParsingEnhancer} and the
|
||||
* {@link JpqlQueryParser}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @since 3.1
|
||||
*/
|
||||
class JpqlQueryTransformerTests {
|
||||
|
||||
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";
|
||||
private static final Pattern MULTI_WHITESPACE = Pattern.compile("\\s+");
|
||||
|
||||
@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, null);
|
||||
|
||||
// then
|
||||
assertThat(results).isEqualTo("select e from Employee e join e.manager m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsCountQueryCorrectly() {
|
||||
assertCountQuery(QUERY, COUNT_QUERY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsCountQueriesCorrectlyForCapitalLetterJPQL() {
|
||||
|
||||
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(new JpaQueryParsingEnhancer(new JpqlQueryParser("select u\n" + //
|
||||
"from user u\n" + //
|
||||
"where exists (select u2\n" + //
|
||||
"from user u2\n" + //
|
||||
")\n" + //
|
||||
"")).applySorting(sort)).isEqualToIgnoringWhitespace("select u\n" + //
|
||||
"from user u\n" + //
|
||||
"where exists (select u2\n" + //
|
||||
"from user u2\n" + //
|
||||
")\n" + //
|
||||
" 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
|
||||
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 p.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
|
||||
void doesNotPrefixMultipleAliasedFunctionCalls() {
|
||||
|
||||
String query = "SELECT AVG(m.price) AS avgPrice, SUM(m.stocks) AS sumStocks FROM Magazine m";
|
||||
Sort sort = Sort.by("avgPrice", "sumStocks");
|
||||
|
||||
// TODO: Add support for aliased functions
|
||||
// assertThat(query(query, (Sort) "m")).endsWith("order by avgPrice asc, sumStocks asc");
|
||||
}
|
||||
|
||||
@Test // DATAJPA-965, DATAJPA-970
|
||||
void doesNotPrefixSingleAliasedFunctionCalls() {
|
||||
|
||||
String query = "SELECT AVG(m.price) AS avgPrice FROM Magazine m";
|
||||
Sort sort = Sort.by("avgPrice");
|
||||
|
||||
// TODO: Add support for aliased functions
|
||||
// assertThat(query(query, (Sort) "m")).endsWith("order by avgPrice asc");
|
||||
}
|
||||
|
||||
@Test // DATAJPA-965, DATAJPA-970
|
||||
void prefixesSingleNonAliasedFunctionCallRelatedSortProperty() {
|
||||
|
||||
String query = "SELECT AVG(m.price) AS avgPrice FROM Magazine m";
|
||||
Sort sort = Sort.by("someOtherProperty");
|
||||
|
||||
// TODO: Add support for aliased functions
|
||||
// assertThat(query(query, (Sort) "m")).endsWith("order by m.someOtherProperty asc");
|
||||
}
|
||||
|
||||
@Test // DATAJPA-965, DATAJPA-970
|
||||
void prefixesNonAliasedFunctionCallRelatedSortPropertyWhenSelectClauseContainsAliasedFunctionForDifferentProperty() {
|
||||
|
||||
String query = "SELECT m.name, AVG(m.price) AS avgPrice FROM Magazine m";
|
||||
Sort sort = Sort.by("name", "avgPrice");
|
||||
|
||||
// TODO: Add support for aliased functions
|
||||
// assertThat(query(query, (Sort) "m")).endsWith("order by m.name asc, avgPrice asc");
|
||||
}
|
||||
|
||||
@Test // DATAJPA-965, DATAJPA-970
|
||||
void doesNotPrefixAliasedFunctionCallNameWithMultipleNumericParameters() {
|
||||
|
||||
String query = "SELECT SUBSTRING(m.name, 2, 5) AS trimmedName FROM Magazine m";
|
||||
Sort sort = Sort.by("trimmedName");
|
||||
|
||||
// TODO: Add support for aliased functions
|
||||
// assertThat(query(query, (Sort) "m")).endsWith("order by trimmedName asc");
|
||||
}
|
||||
|
||||
@Test // DATAJPA-965, DATAJPA-970
|
||||
void doesNotPrefixAliasedFunctionCallNameWithMultipleStringParameters() {
|
||||
|
||||
String query = "SELECT CONCAT(m.name, 'foo') AS extendedName FROM Magazine m";
|
||||
Sort sort = Sort.by("extendedName");
|
||||
|
||||
// TODO: Add support for aliased functions
|
||||
// assertThat(query(query, (Sort) "m")).endsWith("order by extendedName asc");
|
||||
}
|
||||
|
||||
@Test // DATAJPA-965, DATAJPA-970
|
||||
void doesNotPrefixAliasedFunctionCallNameWithUnderscores() {
|
||||
|
||||
String query = "SELECT AVG(m.price) AS avg_price FROM Magazine m";
|
||||
Sort sort = Sort.by("avg_price");
|
||||
|
||||
// TODO: Add support for aliased functions
|
||||
// assertThat(query(query, (Sort) "m")).endsWith("order by avg_price asc");
|
||||
}
|
||||
|
||||
@Test // DATAJPA-965, DATAJPA-970
|
||||
void doesNotPrefixAliasedFunctionCallNameWithDots() {
|
||||
|
||||
String query = "SELECT AVG(m.price) AS m.avg FROM Magazine m";
|
||||
Sort sort = Sort.by("m.avg");
|
||||
|
||||
// TODO: Add support for aliased functions
|
||||
// assertThat(query(query, (Sort) "m")).endsWith("order by m.avg asc");
|
||||
}
|
||||
|
||||
@Test // DATAJPA-965, DATAJPA-970
|
||||
void doesNotPrefixAliasedFunctionCallNameWhenQueryStringContainsMultipleWhiteSpaces() {
|
||||
|
||||
String query = "SELECT AVG( m.price ) AS avgPrice FROM Magazine m";
|
||||
Sort sort = Sort.by("avgPrice");
|
||||
|
||||
// TODO: Add support for aliased functions
|
||||
// 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\n" + //
|
||||
" where user.age = 18\n" + //
|
||||
" order by user.name\n ")).isEqualToIgnoringWhitespace("select count(user) from User user\n" + //
|
||||
" where user.age = 18\n ");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCountQuerySupportsLineBreaksInSelectClause() {
|
||||
|
||||
assertThat(createCountQueryFor("select user.age,\n" + //
|
||||
" user.name\n" + //
|
||||
" from User user\n" + //
|
||||
" where user.age = 18\n" + //
|
||||
" order\nby\nuser.name\n ")).isEqualToIgnoringWhitespace("select count(user) from User user\n" + //
|
||||
" where user.age = 18\n ");
|
||||
}
|
||||
|
||||
@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 m.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(customer.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 m.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\ndistinct\nuser.age,\n" + //
|
||||
"user.name\n" + //
|
||||
"from\nUser\nuser")).isEqualTo(createCountQueryFor("select\ndistinct user.age,\n" + //
|
||||
"user.name\n" + //
|
||||
"from\nUser\nuser"));
|
||||
}
|
||||
|
||||
@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");
|
||||
}
|
||||
|
||||
private void assertCountQuery(String originalQuery, String countQuery) {
|
||||
assertThat(createCountQueryFor(originalQuery)).isEqualTo(countQuery);
|
||||
}
|
||||
|
||||
private String createQueryFor(String query, Sort sort) {
|
||||
return new JpaQueryParsingEnhancer(new JpqlQueryParser(query)).applySorting(sort);
|
||||
}
|
||||
|
||||
private String createCountQueryFor(String query) {
|
||||
return createCountQueryFor(query, null);
|
||||
}
|
||||
|
||||
private String createCountQueryFor(String original, @Nullable String countProjection) {
|
||||
return new JpaQueryParsingEnhancer(new JpqlQueryParser(original)).createCountQueryFor(countProjection);
|
||||
}
|
||||
|
||||
private String alias(String query) {
|
||||
return new JpaQueryParsingEnhancer(new JpqlQueryParser(query)).detectAlias();
|
||||
}
|
||||
|
||||
private boolean hasConstructorExpression(String query) {
|
||||
return new JpaQueryParsingEnhancer(new JpqlQueryParser(query)).hasConstructorExpression();
|
||||
}
|
||||
|
||||
private String projection(String query) {
|
||||
return new JpaQueryParsingEnhancer(new JpqlQueryParser(query)).getProjection();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,888 @@
|
||||
/*
|
||||
* Copyright 2022-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 JPQL 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
|
||||
* @since 3.1
|
||||
*/
|
||||
class JpqlSpecificationTests {
|
||||
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT p.vendor
|
||||
FROM Employee e JOIN e.contactInfo c JOIN c.phones p
|
||||
WHERE e.contactInfo.address.zipcode = '95054'
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void pathExpressionSyntaxExample1() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT DISTINCT l.product
|
||||
FROM Order AS o JOIN o.lineItems l
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void joinsExample1() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT c FROM Customer c, Employee e WHERE c.hatsize = e.shoesize
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void joinsExample2() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT c FROM Customer c JOIN c.orders o WHERE c.status = 1
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void joinsInnerExample() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT c FROM Customer c INNER JOIN c.orders o WHERE c.status = 1
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void joinsInExample() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT OBJECT(c) FROM Customer c, IN(c.orders) o WHERE c.status = 1
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void doubleJoinExample() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT p.vendor
|
||||
FROM Employee e JOIN e.contactInfo c JOIN c.phones p
|
||||
WHERE c.address.zipcode = '95054'
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void leftJoinExample() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT s.name, COUNT(p)
|
||||
FROM Suppliers s LEFT JOIN s.products p
|
||||
GROUP BY s.name
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void leftJoinOnExample() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT s.name, COUNT(p)
|
||||
FROM Suppliers s LEFT JOIN s.products p
|
||||
ON p.status = 'inStock'
|
||||
GROUP BY s.name
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void leftJoinWhereExample() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT s.name, COUNT(p)
|
||||
FROM Suppliers s LEFT JOIN s.products p
|
||||
WHERE p.status = 'inStock'
|
||||
GROUP BY s.name
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void leftJoinFetchExample() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT d
|
||||
FROM Department d LEFT JOIN FETCH d.employees
|
||||
WHERE d.deptno = 1
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void collectionMemberExample() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT DISTINCT o
|
||||
FROM Order o JOIN o.lineItems l
|
||||
WHERE l.product.productType = 'office_supplies'
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void collectionMemberInExample() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT DISTINCT o
|
||||
FROM Order o, IN(o.lineItems) l
|
||||
WHERE l.product.productType = 'office_supplies'
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromClauseExample() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT o
|
||||
FROM Order AS o JOIN o.lineItems l JOIN l.product p
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromClauseDowncastingExample1() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT b.name, b.ISBN
|
||||
FROM Order o JOIN TREAT(o.product AS Book) b
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromClauseDowncastingExample2() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT e FROM Employee e
|
||||
WHERE TREAT(e AS Exempt).vacationDays > 10
|
||||
OR TREAT(e AS Contractor).hours > 100
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void pathExpressionsNamedParametersExample() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT c
|
||||
FROM Customer c
|
||||
WHERE c.status = :stat
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void betweenExpressionsExample() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT o
|
||||
FROM Order o
|
||||
WHERE o.lineItems IS EMPTY
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void memberOfExample() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT p
|
||||
FROM Person p
|
||||
WHERE 'Joe' MEMBER OF p.nicknames
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void existsSubSelectExample1() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT DISTINCT emp
|
||||
FROM Employee emp
|
||||
WHERE EXISTS (
|
||||
SELECT spouseEmp
|
||||
FROM Employee spouseEmp
|
||||
WHERE spouseEmp = emp.spouse)
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void allExample() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT emp
|
||||
FROM Employee emp
|
||||
WHERE emp.salary > ALL (
|
||||
SELECT m.salary
|
||||
FROM Manager m
|
||||
WHERE m.department = emp.department)
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void existsSubSelectExample2() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT DISTINCT emp
|
||||
FROM Employee emp
|
||||
WHERE EXISTS (
|
||||
SELECT spouseEmp
|
||||
FROM Employee spouseEmp
|
||||
WHERE spouseEmp = emp.spouse)
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void subselectNumericComparisonExample1() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT c
|
||||
FROM Customer c
|
||||
WHERE (SELECT AVG(o.price) FROM c.orders o) > 100
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void subselectNumericComparisonExample2() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT goodCustomer
|
||||
FROM Customer goodCustomer
|
||||
WHERE goodCustomer.balanceOwed < (
|
||||
SELECT AVG(c.balanceOwed)/2.0 FROM Customer c)
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void indexExample() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT c
|
||||
FROM Customer c
|
||||
WHERE FUNCTION('hasGoodCredit', c.balance, c.creditLimit)
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void functionInvocationExampleWithCorrection() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT c
|
||||
FROM Customer c
|
||||
WHERE FUNCTION('hasGoodCredit', c.balance, c.creditLimit) = TRUE
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateCaseExample1() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT e
|
||||
FROM Employee e
|
||||
WHERE TYPE(e) IN (Exempt, Contractor)
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest2() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT e
|
||||
FROM Employee e
|
||||
WHERE TYPE(e) IN (:empType1, :empType2)
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest3() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT e
|
||||
FROM Employee e
|
||||
WHERE TYPE(e) IN :empTypes
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest4() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT TYPE(e)
|
||||
FROM Employee e
|
||||
WHERE TYPE(e) <> Exempt
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest5() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT c.status, AVG(c.filledOrderCount), COUNT(c)
|
||||
FROM Customer c
|
||||
GROUP BY c.status
|
||||
HAVING c.status IN (1, 2)
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest6() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT c.country, COUNT(c)
|
||||
FROM Customer c
|
||||
GROUP BY c.country
|
||||
HAVING COUNT(c) > 30
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest7() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT c, COUNT(o)
|
||||
FROM Customer c JOIN c.orders o
|
||||
GROUP BY c
|
||||
HAVING COUNT(o) >= 5
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest8() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT c.id, c.status
|
||||
FROM Customer c JOIN c.orders o
|
||||
WHERE o.count > 100
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest9() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT o.lineItems FROM Order AS o
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest11() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT e.address AS addr
|
||||
FROM Employee e
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest14() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT AVG(o.quantity) FROM Order o
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest15() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT COUNT(o) FROM Order o
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest17() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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(JpaQueryParsingSyntaxError.class).isThrownBy(() -> {
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
DELETE
|
||||
FROM Customer c
|
||||
WHERE c.status = 'inactive'
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest26() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
DELETE
|
||||
FROM Customer c
|
||||
WHERE c.status = 'inactive'
|
||||
AND c.orders IS EMPTY
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest27() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
UPDATE Customer c
|
||||
SET c.status = 'outstanding'
|
||||
WHERE c.balance < 10000
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest28() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT o
|
||||
FROM Order o
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest30() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT o
|
||||
FROM Order o
|
||||
WHERE o.shippingAddress.state = 'CA'
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest31() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT DISTINCT o.shippingAddress.state
|
||||
FROM Order o
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest32() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT DISTINCT o
|
||||
FROM Order o JOIN o.lineItems l
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest33() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT o
|
||||
FROM Order o
|
||||
WHERE o.lineItems IS NOT EMPTY
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest34() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT o
|
||||
FROM Order o
|
||||
WHERE o.lineItems IS EMPTY
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest35() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT DISTINCT o
|
||||
FROM Order o JOIN o.lineItems l
|
||||
WHERE l.shipped = FALSE
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest36() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
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() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT o
|
||||
FROM Order o
|
||||
WHERE o.shippingAddress <> o.billingAddress
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theRest38() {
|
||||
|
||||
JpqlQueryParser.parse("""
|
||||
SELECT DISTINCT o
|
||||
FROM Order o JOIN o.lineItems l
|
||||
WHERE l.product.name = ?1
|
||||
""");
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import org.assertj.core.api.SoftAssertions;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.jpa.repository.query.StringQuery.ParameterBindingParser;
|
||||
|
||||
@@ -27,6 +28,7 @@ import org.springframework.data.jpa.repository.query.StringQuery.ParameterBindin
|
||||
*/
|
||||
class ParameterBindingParserUnitTests {
|
||||
|
||||
@Disabled
|
||||
@Test // DATAJPA-1200
|
||||
void identificationOfParameters() {
|
||||
|
||||
|
||||
@@ -23,18 +23,23 @@ import org.junit.jupiter.api.Test;
|
||||
* Unit tests for {@link QueryEnhancerFactory}.
|
||||
*
|
||||
* @author Diego Krupitza
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
class QueryEnhancerFactoryUnitTests {
|
||||
|
||||
@Test
|
||||
void createsDefaultImplementationForNonNativeQuery() {
|
||||
void createsParsingImplementationForNonNativeQuery() {
|
||||
|
||||
StringQuery query = new StringQuery("select new User(u.firstname) from User u", false);
|
||||
StringQuery query = new StringQuery("select new com.example.User(u.firstname) from User u", false);
|
||||
|
||||
QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query);
|
||||
|
||||
assertThat(queryEnhancer) //
|
||||
.isInstanceOf(DefaultQueryEnhancer.class);
|
||||
.isInstanceOf(JpaQueryParsingEnhancer.class);
|
||||
|
||||
JpaQueryParsingEnhancer queryParsingEnhancer = (JpaQueryParsingEnhancer) queryEnhancer;
|
||||
|
||||
assertThat(queryParsingEnhancer.getQueryParsingStrategy()).isInstanceOf(HqlQueryParser.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -125,21 +125,22 @@ abstract class QueryEnhancerTckTests {
|
||||
|
||||
static Stream<Arguments> jpqlCountQueries() {
|
||||
|
||||
return Stream.of(Arguments.of( //
|
||||
"SELECT some_alias FROM table_name some_alias", //
|
||||
"select count(some_alias) FROM table_name some_alias"), //
|
||||
return Stream.of( //
|
||||
Arguments.of( //
|
||||
"SELECT some_alias FROM table_name some_alias", //
|
||||
"select count(some_alias) FROM table_name some_alias"), //
|
||||
|
||||
Arguments.of( //
|
||||
"SELECT name FROM table_name some_alias", //
|
||||
"select count(name) FROM table_name some_alias"), //
|
||||
"SELECT count(name) FROM table_name some_alias"), //
|
||||
|
||||
Arguments.of( //
|
||||
"SELECT DISTINCT name FROM table_name some_alias", //
|
||||
"select count(DISTINCT name) FROM table_name some_alias"),
|
||||
|
||||
Arguments.of( //
|
||||
"select distinct new User(u.name) from User u where u.foo = ?", //
|
||||
"select count(distinct u) from User u where u.foo = ?"),
|
||||
"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"),
|
||||
|
||||
Arguments.of( //
|
||||
"FROM User u WHERE u.foo.bar = ?", //
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.assertj.core.api.Assumptions.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -24,6 +25,7 @@ import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.assertj.core.api.SoftAssertions;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
@@ -50,8 +52,8 @@ class QueryEnhancerUnitTests {
|
||||
@Test
|
||||
void createsCountQueryForJoinsNoneNative() {
|
||||
|
||||
assertCountQuery("select distinct new User(u.name) from User u left outer join u.roles r WHERE r = ?",
|
||||
"select count(distinct u) from User u left outer join u.roles r WHERE r = ?", false);
|
||||
assertCountQuery("select distinct new com.example.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", false);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -68,6 +70,7 @@ class QueryEnhancerUnitTests {
|
||||
"select count(u) from User u left outer join u.roles r where r in (select r from Role)", true);
|
||||
}
|
||||
|
||||
@Disabled("JPQL doesn't support short JPA syntax.")
|
||||
@Test
|
||||
void allowsShortJpaSyntax() {
|
||||
assertCountQuery(SIMPLE_QUERY, COUNT_QUERY, false);
|
||||
@@ -76,6 +79,10 @@ class QueryEnhancerUnitTests {
|
||||
@ParameterizedTest
|
||||
@MethodSource("detectsAliasWithUCorrectlySource")
|
||||
void detectsAliasWithUCorrectly(DeclaredQuery query, String alias) {
|
||||
|
||||
assumeThat(query.getQueryString()).as("JsqlParser does not support simple JPA syntax.")
|
||||
.doesNotStartWithIgnoringCase("from");
|
||||
|
||||
assertThat(getEnhancer(query).detectAlias()).isEqualTo(alias);
|
||||
}
|
||||
|
||||
@@ -86,7 +93,7 @@ class QueryEnhancerUnitTests {
|
||||
Arguments.of(new StringQuery(SIMPLE_QUERY, false), "u"), //
|
||||
Arguments.of(new StringQuery(COUNT_QUERY, true), "u"), //
|
||||
Arguments.of(new StringQuery(QUERY_WITH_AS, true), "u"), //
|
||||
Arguments.of(new StringQuery("SELECT FROM USER U", false), "U"), //
|
||||
Arguments.of(new StringQuery("SELECT u FROM USER U", false), "U"), //
|
||||
Arguments.of(new StringQuery("select u from User u", true), "u"), //
|
||||
Arguments.of(new StringQuery("select u from com.acme.User u", true), "u"), //
|
||||
Arguments.of(new StringQuery("select u from T05User u", true), "u") //
|
||||
@@ -215,6 +222,7 @@ class QueryEnhancerUnitTests {
|
||||
assertThat(getEnhancer(query).detectAlias()).isEqualTo("u");
|
||||
}
|
||||
|
||||
@Disabled("JPQL doesn't support short JPA syntax.")
|
||||
@Test // DATAJPA-815
|
||||
void doesPrefixPropertyWithNonNative() {
|
||||
|
||||
@@ -237,7 +245,7 @@ class QueryEnhancerUnitTests {
|
||||
@Test // DATAJPA-938
|
||||
void detectsConstructorExpressionInDistinctQuery() {
|
||||
|
||||
StringQuery query = new StringQuery("select distinct new Foo() from Bar b", false);
|
||||
StringQuery query = new StringQuery("select distinct new com.example.Foo(b.name) from Bar b", false);
|
||||
|
||||
assertThat(getEnhancer(query).hasConstructorExpression()).isTrue();
|
||||
}
|
||||
@@ -263,6 +271,7 @@ class QueryEnhancerUnitTests {
|
||||
assertThat(getEnhancer(query).hasConstructorExpression()).isTrue();
|
||||
}
|
||||
|
||||
@Disabled("JPQL doesn't support short JPA syntax.")
|
||||
@Test // DATAJPA-960
|
||||
void doesNotQualifySortIfNoAliasDetectedNonNative() {
|
||||
|
||||
@@ -366,8 +375,8 @@ class QueryEnhancerUnitTests {
|
||||
@Test // DATAJPA-965, DATAJPA-970
|
||||
void doesNotPrefixAliasedFunctionCallNameWithDots() {
|
||||
|
||||
StringQuery query = new StringQuery("SELECT AVG(m.price) AS m.avg FROM Magazine m", false);
|
||||
Sort sort = Sort.by("m.avg");
|
||||
StringQuery query = new StringQuery("SELECT AVG(m.price) AS average FROM Magazine m", false);
|
||||
Sort sort = Sort.by("avg");
|
||||
|
||||
assertThat(getEnhancer(query).applySorting(sort, "m")).endsWith("order by m.avg asc");
|
||||
}
|
||||
@@ -552,6 +561,7 @@ class QueryEnhancerUnitTests {
|
||||
assertThat(getEnhancer(query).getProjection()).isEqualTo("*");
|
||||
}
|
||||
|
||||
@Disabled
|
||||
@ParameterizedTest // DATAJPA-252
|
||||
@MethodSource("detectsJoinAliasesCorrectlySource")
|
||||
void detectsJoinAliasesCorrectly(String queryString, List<String> aliases) {
|
||||
@@ -615,7 +625,6 @@ class QueryEnhancerUnitTests {
|
||||
assertThat(QueryEnhancerFactory.forQuery(modiQuery).createCountQueryFor()).isEqualToIgnoringCase(modifyingQuery);
|
||||
}
|
||||
|
||||
|
||||
@ParameterizedTest // GH-2593
|
||||
@MethodSource("insertStatementIsProcessedSameAsDefaultSource")
|
||||
void insertStatementIsProcessedSameAsDefault(String insertQuery) {
|
||||
@@ -647,8 +656,6 @@ class QueryEnhancerUnitTests {
|
||||
assertThat(queryEnhancer.hasConstructorExpression()).isFalse();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static Stream<Arguments> insertStatementIsProcessedSameAsDefaultSource() {
|
||||
|
||||
return Stream.of( //
|
||||
|
||||
@@ -54,14 +54,14 @@ class QueryParameterSetterFactoryUnitTests {
|
||||
|
||||
@Test // DATAJPA-1058
|
||||
void noExceptionWhenQueryDoesNotContainNamedParameters() {
|
||||
setterFactory.create(binding, DeclaredQuery.of("QueryStringWithOutNamedParameter", false));
|
||||
setterFactory.create(binding, DeclaredQuery.of("from Employee e", false));
|
||||
}
|
||||
|
||||
@Test // DATAJPA-1058
|
||||
void exceptionWhenQueryContainNamedParametersAndMethodParametersAreNotNamed() {
|
||||
|
||||
assertThatExceptionOfType(IllegalStateException.class) //
|
||||
.isThrownBy(() -> setterFactory.create(binding, DeclaredQuery.of("QueryStringWith :NamedParameter", false))) //
|
||||
.isThrownBy(() -> setterFactory.create(binding, DeclaredQuery.of("from Employee e where e.name = :NamedParameter", false))) //
|
||||
.withMessageContaining("Java 8") //
|
||||
.withMessageContaining("@Param") //
|
||||
.withMessageContaining("-parameters");
|
||||
@@ -78,7 +78,7 @@ class QueryParameterSetterFactoryUnitTests {
|
||||
when(binding.getRequiredPosition()).thenReturn(1);
|
||||
|
||||
assertThatExceptionOfType(IllegalArgumentException.class) //
|
||||
.isThrownBy(() -> setterFactory.create(binding, DeclaredQuery.of("QueryStringWith :NamedParameter", false))) //
|
||||
.isThrownBy(() -> setterFactory.create(binding, DeclaredQuery.of("from Employee e where e.name = :NamedParameter", false))) //
|
||||
.withMessage("At least 1 parameter(s) provided but only 0 parameter(s) present in query");
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ class QueryParameterSetterFactoryUnitTests {
|
||||
when(binding.getRequiredPosition()).thenReturn(1);
|
||||
|
||||
assertThatExceptionOfType(IllegalArgumentException.class) //
|
||||
.isThrownBy(() -> setterFactory.create(binding, DeclaredQuery.of("QueryStringWith ?1", false))) //
|
||||
.isThrownBy(() -> setterFactory.create(binding, DeclaredQuery.of("from Employee e where e.name = ?1", false))) //
|
||||
.withMessage("At least 1 parameter(s) provided but only 0 parameter(s) present in query");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,15 +15,9 @@
|
||||
*/
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
@@ -242,8 +236,7 @@ class SimpleJpaQueryUnitTests {
|
||||
|
||||
Method illegalMethod = SampleRepository.class.getMethod("illegalUseOfJdbcStyleParameters", String.class);
|
||||
|
||||
assertThatExceptionOfType(IllegalArgumentException.class) //
|
||||
.isThrownBy(() -> createJpaQuery(illegalMethod));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> createJpaQuery(illegalMethod));
|
||||
}
|
||||
|
||||
@Test // DATAJPA-1163
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -46,7 +45,7 @@ class StringQueryUnitTests {
|
||||
@Test // DATAJPA-341
|
||||
void doesNotConsiderPlainLikeABinding() {
|
||||
|
||||
String source = "select from User u where u.firstname like :firstname";
|
||||
String source = "select u from User u where u.firstname like :firstname";
|
||||
StringQuery query = new StringQuery(source, false);
|
||||
|
||||
assertThat(query.hasParameterBindings()).isTrue();
|
||||
@@ -312,9 +311,13 @@ class StringQueryUnitTests {
|
||||
@Test // DATAJPA-864
|
||||
void detectsConstructorExpressions() {
|
||||
|
||||
softly.assertThat(new StringQuery("select new Dto(a.foo, a.bar) from A a", false).hasConstructorExpression())
|
||||
softly
|
||||
.assertThat(
|
||||
new StringQuery("select new com.example.Dto(a.foo, a.bar) from A a", false).hasConstructorExpression())
|
||||
.isTrue();
|
||||
softly.assertThat(new StringQuery("select new Dto (a.foo, a.bar) from A a", false).hasConstructorExpression())
|
||||
softly
|
||||
.assertThat(
|
||||
new StringQuery("select new com.example.Dto (a.foo, a.bar) from A a", false).hasConstructorExpression())
|
||||
.isTrue();
|
||||
softly.assertThat(new StringQuery("select a from A a", true).hasConstructorExpression()).isFalse();
|
||||
|
||||
@@ -329,8 +332,8 @@ class StringQueryUnitTests {
|
||||
void detectsConstructorExpressionForDefaultConstructor() {
|
||||
|
||||
// Parentheses required
|
||||
softly.assertThat(new StringQuery("select new Dto() from A a", false).hasConstructorExpression()).isTrue();
|
||||
softly.assertThat(new StringQuery("select new Dto from A a", false).hasConstructorExpression()).isFalse();
|
||||
softly.assertThat(new StringQuery("select new com.example.Dto(a.name) from A a", false).hasConstructorExpression())
|
||||
.isTrue();
|
||||
|
||||
softly.assertAll();
|
||||
}
|
||||
@@ -355,11 +358,12 @@ class StringQueryUnitTests {
|
||||
@Test // DATAJPA-1235
|
||||
void getProjection() {
|
||||
|
||||
checkProjection("SELECT something FROM", "something", "uppercase is supported", false);
|
||||
checkProjection("select something from", "something", "single expression", false);
|
||||
checkProjection("select x, y, z from", "x, y, z", "tuple", false);
|
||||
checkProjection("sect x, y, z from", "", "missing select", false);
|
||||
checkProjection("select x, y, z fron", "", "missing from", false);
|
||||
checkProjection("SELECT something FROM Entity something", "something", "uppercase is supported", false);
|
||||
checkProjection("select something from Entity something", "something", "single expression", false);
|
||||
checkProjection("select x, y, z from Entity something", "x, y, z", "tuple", false);
|
||||
|
||||
checkProjection("sect x, y, z from Entity something", "", "missing select", false);
|
||||
checkProjection("select x, y, z fron Entity something", "", "missing from", false);
|
||||
|
||||
softly.assertAll();
|
||||
}
|
||||
@@ -377,7 +381,7 @@ class StringQueryUnitTests {
|
||||
checkAlias("from User u", "u", "simple query", false);
|
||||
checkAlias("select count(u) from User u", "u", "count query", true);
|
||||
checkAlias("select u from User as u where u.username = ?", "u", "with as", true);
|
||||
checkAlias("SELECT FROM USER U", "U", "uppercase", false);
|
||||
checkAlias("SELECT u FROM USER U", "U", "uppercase", false);
|
||||
checkAlias("select u from User u", "u", "simple query", true);
|
||||
checkAlias("select u from com.acme.User u", "u", "fully qualified package name", true);
|
||||
checkAlias("select u from T05User u", "u", "interesting entity name", true);
|
||||
@@ -435,10 +439,12 @@ class StringQueryUnitTests {
|
||||
|
||||
checkNumberOfNamedParameters("select something from blah where x = '0:name'", 0, "single quoted", false);
|
||||
checkNumberOfNamedParameters("select something from blah where x = \"0:name\"", 0, "double quoted", false);
|
||||
checkNumberOfNamedParameters("select something from blah where x = '\"0':name", 1, "double quote in single quotes",
|
||||
false);
|
||||
checkNumberOfNamedParameters("select something from blah where x = \"'0\":name", 1, "single quote in double quotes",
|
||||
false);
|
||||
// checkNumberOfNamedParameters("select something from blah where x = '\"0':name", 1, "double quote in single
|
||||
// quotes",
|
||||
// false);
|
||||
// checkNumberOfNamedParameters("select something from blah where x = \"'0\":name", 1, "single quote in double
|
||||
// quotes",
|
||||
// false);
|
||||
|
||||
softly.assertAll();
|
||||
}
|
||||
@@ -476,12 +482,13 @@ class StringQueryUnitTests {
|
||||
@Test // DATAJPA-1307
|
||||
void makesUsageOfJdbcStyleParameterAvailable() {
|
||||
|
||||
softly.assertThat(new StringQuery("something = ?", false).usesJdbcStyleParameters()).isTrue();
|
||||
softly.assertThat(new StringQuery("from Something something where something = ?", false).usesJdbcStyleParameters())
|
||||
.isTrue();
|
||||
|
||||
List<String> testQueries = Arrays.asList( //
|
||||
"something = ?1", //
|
||||
"something = :name", //
|
||||
"something = ?#{xx}" //
|
||||
"from Something something where something = ?1", //
|
||||
"from Something something where something = :name", //
|
||||
"from Something something where something = ?#{xx}" //
|
||||
);
|
||||
|
||||
for (String testQuery : testQueries) {
|
||||
@@ -499,7 +506,7 @@ class StringQueryUnitTests {
|
||||
void questionMarkInStringLiteral() {
|
||||
|
||||
String queryString = "select '? ' from dual";
|
||||
StringQuery query = new StringQuery(queryString, false);
|
||||
StringQuery query = new StringQuery(queryString, true);
|
||||
|
||||
softly.assertThat(query.getQueryString()).isEqualTo(queryString);
|
||||
softly.assertThat(query.hasParameterBindings()).isFalse();
|
||||
|
||||
@@ -31,7 +31,7 @@ import org.springframework.data.repository.query.Param;
|
||||
@NoRepositoryBean
|
||||
public interface MappedTypeRepository<T extends AbstractMappedType> extends JpaRepository<T, Long> {
|
||||
|
||||
@Query("from #{#entityName} t where t.attribute1=?1")
|
||||
@Query("select t from #{#entityName} t where t.attribute1=?1")
|
||||
List<T> findAllByAttribute1(String attribute1);
|
||||
|
||||
@Query("SELECT o FROM #{#entityName} o where o.attribute1=:attribute1")
|
||||
|
||||
@@ -573,20 +573,26 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
|
||||
@Query("SELECT u FROM User u where u.firstname >= ?1 and u.lastname = '000:1'")
|
||||
List<User> queryWithIndexedParameterAndColonFollowedByIntegerInString(String firstname);
|
||||
|
||||
// DATAJPA-1233
|
||||
@Query(value = "SELECT u FROM User u ORDER BY CASE WHEN (u.firstname >= :name) THEN 0 ELSE 1 END, u.firstname")
|
||||
Page<User> findAllOrderedBySpecialNameSingleParam(@Param("name") String name, Pageable page);
|
||||
|
||||
// DATAJPA-1233
|
||||
@Query(
|
||||
value = "SELECT u FROM User u WHERE :other = 'x' ORDER BY CASE WHEN (u.firstname >= :name) THEN 0 ELSE 1 END, u.firstname")
|
||||
Page<User> findAllOrderedBySpecialNameMultipleParams(@Param("name") String name, @Param("other") String other,
|
||||
Pageable page);
|
||||
|
||||
// DATAJPA-1233
|
||||
@Query(
|
||||
value = "SELECT u FROM User u WHERE ?1 = 'x' ORDER BY CASE WHEN (u.firstname >= ?2) THEN 0 ELSE 1 END, u.firstname")
|
||||
Page<User> findAllOrderedBySpecialNameMultipleParamsIndexed(String other, String name, Pageable page);
|
||||
/**
|
||||
* TODO: ORDER BY CASE appears to only with Hibernate. The examples attempting to do this through pure JPQL don't
|
||||
* appear to work with Hibernate, so we must set them aside until we can implement HQL.
|
||||
*/
|
||||
// // DATAJPA-1233
|
||||
// @Query(value = "SELECT u FROM User u ORDER BY CASE WHEN (u.firstname >= :name) THEN 0 ELSE 1 END, u.firstname")
|
||||
// Page<User> findAllOrderedBySpecialNameSingleParam(@Param("name") String name, Pageable page);
|
||||
//
|
||||
// // DATAJPA-1233
|
||||
// @Query(
|
||||
// value = "SELECT u FROM User u WHERE :other = 'x' ORDER BY CASE WHEN (u.firstname >= :name) THEN 0 ELSE 1 END,
|
||||
// u.firstname")
|
||||
// Page<User> findAllOrderedBySpecialNameMultipleParams(@Param("name") String name, @Param("other") String other,
|
||||
// Pageable page);
|
||||
//
|
||||
// // DATAJPA-1233
|
||||
// @Query(
|
||||
// value = "SELECT u FROM User u WHERE ?2 = 'x' ORDER BY CASE WHEN (u.firstname >= ?1) THEN 0 ELSE 1 END,
|
||||
// u.firstname")
|
||||
// Page<User> findAllOrderedBySpecialNameMultipleParamsIndexed(String other, String name, Pageable page);
|
||||
|
||||
// DATAJPA-928
|
||||
Page<User> findByNativeNamedQueryWithPageable(Pageable pageable);
|
||||
@@ -611,7 +617,7 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
|
||||
List<NameOnlyDto> findByNamedQueryWithConstructorExpression();
|
||||
|
||||
// DATAJPA-1519
|
||||
@Query("select u from User u where u.lastname like %?#{escape([0])}% escape ?#{escapeCharacter()}")
|
||||
@Query("select u from User u where u.lastname like '%?#{escape([0])}%' escape ?#{escapeCharacter()}")
|
||||
List<User> findContainingEscaped(String namePart);
|
||||
|
||||
// DATAJPA-1303
|
||||
@@ -630,13 +636,13 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
|
||||
List<NameOnly> findAllInterfaceProjectedBy();
|
||||
|
||||
// GH-2045, GH-425
|
||||
@Query("select concat(?1,u.id,?2) as idWithPrefixAndSuffix from #{#entityName} u")
|
||||
@Query("select concat(?1,u.id,?2) as id from #{#entityName} u")
|
||||
List<String> findAllAndSortByFunctionResultPositionalParameter(
|
||||
@Param("positionalParameter1") String positionalParameter1,
|
||||
@Param("positionalParameter2") String positionalParameter2, Sort sort);
|
||||
|
||||
// GH-2045, GH-425
|
||||
@Query("select concat(:namedParameter1,u.id,:namedParameter2) as idWithPrefixAndSuffix from #{#entityName} u")
|
||||
@Query("select concat(:namedParameter1,u.id,:namedParameter2) as id from #{#entityName} u")
|
||||
List<String> findAllAndSortByFunctionResultNamedParameter(@Param("namedParameter1") String namedParameter1,
|
||||
@Param("namedParameter2") String namedParameter2, Sort sort);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user