Skip to content

Commit 46dd7e7

Browse files
committed
Add support for JPA 3.2 additions to JPQL.
See #3136.
1 parent 563f1ed commit 46dd7e7

File tree

5 files changed

+183
-32
lines changed

5 files changed

+183
-32
lines changed

Diff for: spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4

+19-2
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,13 @@ ql_statement
4242
;
4343

4444
select_statement
45-
: select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)?
45+
: select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (setOperator_with_select_statement)*
46+
;
47+
48+
setOperator_with_select_statement
49+
: INTERSECT select_statement
50+
| UNION select_statement
51+
| EXCEPT select_statement
4652
;
4753

4854
update_statement
@@ -234,7 +240,11 @@ orderby_clause
234240

235241
// TODO Error in spec BNF, correctly shown elsewhere in spec.
236242
orderby_item
237-
: (state_field_path_expression | general_identification_variable | result_variable ) (ASC | DESC)?
243+
: (state_field_path_expression | general_identification_variable | result_variable ) (ASC | DESC)? nullsPrecedence?
244+
;
245+
246+
nullsPrecedence
247+
: NULLS (FIRST | LAST)
238248
;
239249

240250
subquery
@@ -428,6 +438,7 @@ string_expression
428438
| aggregate_expression
429439
| case_expression
430440
| function_invocation
441+
| string_expression op='||' string_expression
431442
| '(' subquery ')'
432443
;
433444

@@ -785,11 +796,13 @@ ELSE : E L S E;
785796
EMPTY : E M P T Y;
786797
ENTRY : E N T R Y;
787798
ESCAPE : E S C A P E;
799+
EXCEPT : E X C E P T;
788800
EXISTS : E X I S T S;
789801
EXP : E X P;
790802
EXTRACT : E X T R A C T;
791803
FALSE : F A L S E;
792804
FETCH : F E T C H;
805+
FIRST : F I R S T;
793806
FLOOR : F L O O R;
794807
FROM : F R O M;
795808
FUNCTION : F U N C T I O N;
@@ -798,9 +811,11 @@ HAVING : H A V I N G;
798811
IN : I N;
799812
INDEX : I N D E X;
800813
INNER : I N N E R;
814+
INTERSECT : I N T E R S E C T;
801815
IS : I S;
802816
JOIN : J O I N;
803817
KEY : K E Y;
818+
LAST : L A S T;
804819
LEADING : L E A D I N G;
805820
LEFT : L E F T;
806821
LENGTH : L E N G T H;
@@ -817,6 +832,7 @@ NEW : N E W;
817832
NOT : N O T;
818833
NULL : N U L L;
819834
NULLIF : N U L L I F;
835+
NULLS : N U L L S;
820836
OBJECT : O B J E C T;
821837
OF : O F;
822838
ON : O N;
@@ -840,6 +856,7 @@ TREAT : T R E A T;
840856
TRIM : T R I M;
841857
TRUE : T R U E;
842858
TYPE : T Y P E;
859+
UNION : U N I O N;
843860
UPDATE : U P D A T E;
844861
UPPER : U P P E R;
845862
VALUE : V A L U E;

Diff for: spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java

+47
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,29 @@ public List<JpaQueryParsingToken> visitSelect_statement(JpqlParser.Select_statem
7171
tokens.addAll(visit(ctx.orderby_clause()));
7272
}
7373

74+
ctx.setOperator_with_select_statement().forEach(setOperatorWithSelectStatementContext -> {
75+
tokens.addAll(visit(setOperatorWithSelectStatementContext));
76+
});
77+
78+
return tokens;
79+
}
80+
81+
@Override
82+
public List<JpaQueryParsingToken> visitSetOperator_with_select_statement(
83+
JpqlParser.SetOperator_with_select_statementContext ctx) {
84+
85+
List<JpaQueryParsingToken> tokens = new ArrayList<>();
86+
87+
if (ctx.INTERSECT() != null) {
88+
tokens.add(new JpaQueryParsingToken(ctx.INTERSECT()));
89+
} else if (ctx.UNION() != null) {
90+
tokens.add(new JpaQueryParsingToken(ctx.UNION()));
91+
} else if (ctx.EXCEPT() != null) {
92+
tokens.add(new JpaQueryParsingToken(ctx.EXCEPT()));
93+
}
94+
95+
tokens.addAll(visit(ctx.select_statement()));
96+
7497
return tokens;
7598
}
7699

@@ -869,6 +892,25 @@ public List<JpaQueryParsingToken> visitOrderby_item(JpqlParser.Orderby_itemConte
869892
if (ctx.DESC() != null) {
870893
tokens.add(new JpaQueryParsingToken(ctx.DESC()));
871894
}
895+
if (ctx.nullsPrecedence() != null) {
896+
tokens.addAll(visit(ctx.nullsPrecedence()));
897+
}
898+
899+
return tokens;
900+
}
901+
902+
@Override
903+
public List<JpaQueryParsingToken> visitNullsPrecedence(JpqlParser.NullsPrecedenceContext ctx) {
904+
905+
List<JpaQueryParsingToken> tokens = new ArrayList<>();
906+
907+
tokens.add(new JpaQueryParsingToken(ctx.NULLS()));
908+
909+
if (ctx.FIRST() != null) {
910+
tokens.add(new JpaQueryParsingToken(ctx.FIRST()));
911+
} else if (ctx.LAST() != null) {
912+
tokens.add(new JpaQueryParsingToken(ctx.LAST()));
913+
}
872914

873915
return tokens;
874916
}
@@ -1527,6 +1569,11 @@ public List<JpaQueryParsingToken> visitString_expression(JpqlParser.String_expre
15271569
tokens.addAll(visit(ctx.case_expression()));
15281570
} else if (ctx.function_invocation() != null) {
15291571
tokens.addAll(visit(ctx.function_invocation()));
1572+
} else if (ctx.op != null) {
1573+
1574+
tokens.addAll(visit(ctx.string_expression(0)));
1575+
tokens.add(new JpaQueryParsingToken(ctx.op));
1576+
tokens.addAll(visit(ctx.string_expression(1)));
15301577
} else if (ctx.subquery() != null) {
15311578

15321579
tokens.add(TOKEN_OPEN_PAREN);

Diff for: spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformer.java

+4
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ public List<JpaQueryParsingToken> visitSelect_statement(JpqlParser.Select_statem
125125
}
126126
}
127127

128+
ctx.setOperator_with_select_statement().forEach(setOperatorWithSelectStatementContext -> {
129+
tokens.addAll(visit(setOperatorWithSelectStatementContext));
130+
});
131+
128132
return tokens;
129133
}
130134

Diff for: spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java

+60-30
Original file line numberDiff line numberDiff line change
@@ -1450,42 +1450,36 @@ void hqlQueries() {
14501450
@Test // GH-2962
14511451
void orderByWithNullsFirstOrLastShouldWork() {
14521452

1453-
assertThatNoException().isThrownBy(() -> {
1454-
parseWithoutChanges("""
1455-
select a,
1456-
case
1457-
when a.geaendertAm is null then a.erstelltAm
1458-
else a.geaendertAm end as mutationAm
1459-
from Element a
1460-
where a.erstelltDurch = :variable
1461-
order by mutationAm desc nulls first
1462-
""");
1463-
});
1464-
1465-
assertThatNoException().isThrownBy(() -> {
1466-
parseWithoutChanges("""
1467-
select a,
1468-
case
1469-
when a.geaendertAm is null then a.erstelltAm
1470-
else a.geaendertAm end as mutationAm
1471-
from Element a
1472-
where a.erstelltDurch = :variable
1473-
order by mutationAm desc nulls last
1453+
assertQuery("""
1454+
select a,
1455+
case
1456+
when a.geaendertAm is null then a.erstelltAm
1457+
else a.geaendertAm end as mutationAm
1458+
from Element a
1459+
where a.erstelltDurch = :variable
1460+
order by mutationAm desc nulls first
1461+
""");
1462+
1463+
assertQuery("""
1464+
select a,
1465+
case
1466+
when a.geaendertAm is null then a.erstelltAm
1467+
else a.geaendertAm end as mutationAm
1468+
from Element a
1469+
where a.erstelltDurch = :variable
1470+
order by mutationAm desc nulls last
14741471
""");
1475-
});
14761472
}
14771473

14781474
@Test // GH-2964
14791475
void roundFunctionShouldWorkLikeAnyOtherFunction() {
14801476

1481-
assertThatNoException().isThrownBy(() -> {
1482-
parseWithoutChanges("""
1483-
select round(count(ri) * 100 / max(ri.receipt.positions), 0) as perc
1484-
from StockOrderItem oi
1485-
right join StockReceiptItem ri
1486-
on ri.article = oi.article
1487-
""");
1488-
});
1477+
assertQuery("""
1478+
select round(count(ri)*100/max(ri.receipt.positions), 0) as perc
1479+
from StockOrderItem oi
1480+
right join StockReceiptItem ri
1481+
on ri.article = oi.article
1482+
""");
14891483
}
14901484

14911485
@Test // GH-2981
@@ -1605,4 +1599,40 @@ void newShouldBeLegalAsPartOfAStateFieldPathExpression() {
16051599
void powerShouldBeLegalInAQuery() {
16061600
assertQuery("select e.power.id from MyEntity e");
16071601
}
1602+
1603+
@Test // GH-3136
1604+
void doublePipeShouldBeValidAsAStringConcatOperator() {
1605+
1606+
assertQuery("""
1607+
select e.name || ' ' || e.title
1608+
from Employee e
1609+
""");
1610+
}
1611+
1612+
@Test // GH-3136
1613+
void additionalStringOperationsShouldWork() {
1614+
1615+
assertQuery("""
1616+
select
1617+
replace(e.name, 'Baggins', 'Proudfeet'),
1618+
left(e.role, 4),
1619+
right(e.home, 5),
1620+
cast(e.distance_from_home, int)
1621+
from Employee e
1622+
""");
1623+
}
1624+
1625+
@Test // GH-3136
1626+
void combinedSelectStatementsShouldWork() {
1627+
1628+
assertQuery("""
1629+
select e from Employee e where e.last_name = 'Baggins'
1630+
intersect
1631+
select e from Employee e where e.first_name = 'Samwise'
1632+
union
1633+
select e from Employee e where e.home = 'The Shire'
1634+
except
1635+
select e from Employee e where e.home = 'Isengard'
1636+
""");
1637+
}
16081638
}

Diff for: spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java

+53
Original file line numberDiff line numberDiff line change
@@ -988,4 +988,57 @@ void newShouldBeLegalAsPartOfAStateFieldPathExpression() {
988988
void powerShouldBeLegalInAQuery() {
989989
assertQuery("select e.power.id from MyEntity e");
990990
}
991+
992+
@Test // GH-3136
993+
void doublePipeShouldBeValidAsAStringConcatOperator() {
994+
995+
assertQuery("""
996+
select e.name || ' ' || e.title
997+
from Employee e
998+
""");
999+
}
1000+
1001+
@Test // GH-3136
1002+
void combinedSelectStatementsShouldWork() {
1003+
1004+
assertQuery("""
1005+
select e from Employee e where e.last_name = 'Baggins'
1006+
intersect
1007+
select e from Employee e where e.first_name = 'Samwise'
1008+
union
1009+
select e from Employee e where e.home = 'The Shire'
1010+
except
1011+
select e from Employee e where e.home = 'Isengard'
1012+
""");
1013+
}
1014+
1015+
@Disabled
1016+
@Test // GH-3136
1017+
void additionalStringOperationsShouldWork() {
1018+
1019+
assertQuery("""
1020+
select
1021+
replace(e.name, 'Baggins', 'Proudfeet'),
1022+
left(e.role, 4),
1023+
right(e.home, 5),
1024+
cast(e.distance_from_home, int)
1025+
from Employee e
1026+
""");
1027+
}
1028+
1029+
@Test // GH-3136
1030+
void orderByWithNullsFirstOrLastShouldWork() {
1031+
1032+
assertQuery("""
1033+
select a
1034+
from Element a
1035+
order by mutationAm desc nulls first
1036+
""");
1037+
1038+
assertQuery("""
1039+
select a
1040+
from Element a
1041+
order by mutationAm desc nulls last
1042+
""");
1043+
}
9911044
}

0 commit comments

Comments
 (0)