Skip to content

Commit 75368b0

Browse files
gregturnmp911de
authored andcommitted
Add support for JPA 3.2 additions to JPQL.
See #3136.
1 parent 5b423a5 commit 75368b0

File tree

4 files changed

+171
-31
lines changed

4 files changed

+171
-31
lines changed

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

+11-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,13 @@ ql_statement
4343
;
4444

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

4955
update_statement
@@ -434,6 +440,7 @@ string_expression
434440
| aggregate_expression
435441
| case_expression
436442
| function_invocation
443+
| string_expression op='||' string_expression
437444
| '(' subquery ')'
438445
;
439446

@@ -878,6 +885,7 @@ ELSE : E L S E;
878885
EMPTY : E M P T Y;
879886
ENTRY : E N T R Y;
880887
ESCAPE : E S C A P E;
888+
EXCEPT : E X C E P T;
881889
EXISTS : E X I S T S;
882890
EXP : E X P;
883891
EXTRACT : E X T R A C T;
@@ -892,6 +900,7 @@ HAVING : H A V I N G;
892900
IN : I N;
893901
INDEX : I N D E X;
894902
INNER : I N N E R;
903+
INTERSECT : I N T E R S E C T;
895904
IS : I S;
896905
JOIN : J O I N;
897906
KEY : K E Y;
@@ -936,6 +945,7 @@ TREAT : T R E A T;
936945
TRIM : T R I M;
937946
TRUE : T R U E;
938947
TYPE : T Y P E;
948+
UNION : U N I O N;
939949
UPDATE : U P D A T E;
940950
UPPER : U P P E R;
941951
VALUE : V A L U E;

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

+47
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,29 @@ public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext
7979
builder.appendExpression(visit(ctx.orderby_clause()));
8080
}
8181

82+
ctx.setOperator_with_select_statement().forEach(setOperatorWithSelectStatementContext -> {
83+
tokens.addAll(visit(setOperatorWithSelectStatementContext));
84+
});
85+
86+
return tokens;
87+
}
88+
89+
@Override
90+
public List<JpaQueryParsingToken> visitSetOperator_with_select_statement(
91+
JpqlParser.SetOperator_with_select_statementContext ctx) {
92+
93+
List<JpaQueryParsingToken> tokens = new ArrayList<>();
94+
95+
if (ctx.INTERSECT() != null) {
96+
tokens.add(new JpaQueryParsingToken(ctx.INTERSECT()));
97+
} else if (ctx.UNION() != null) {
98+
tokens.add(new JpaQueryParsingToken(ctx.UNION()));
99+
} else if (ctx.EXCEPT() != null) {
100+
tokens.add(new JpaQueryParsingToken(ctx.EXCEPT()));
101+
}
102+
103+
tokens.addAll(visit(ctx.select_statement()));
104+
82105
return builder;
83106
}
84107

@@ -799,6 +822,25 @@ public QueryTokenStream visitOrderby_item(JpqlParser.Orderby_itemContext ctx) {
799822
if (ctx.nullsPrecedence() != null) {
800823
builder.append(visit(ctx.nullsPrecedence()));
801824
}
825+
if (ctx.nullsPrecedence() != null) {
826+
tokens.addAll(visit(ctx.nullsPrecedence()));
827+
}
828+
829+
return tokens;
830+
}
831+
832+
@Override
833+
public List<JpaQueryParsingToken> visitNullsPrecedence(JpqlParser.NullsPrecedenceContext ctx) {
834+
835+
List<JpaQueryParsingToken> tokens = new ArrayList<>();
836+
837+
tokens.add(new JpaQueryParsingToken(ctx.NULLS()));
838+
839+
if (ctx.FIRST() != null) {
840+
tokens.add(new JpaQueryParsingToken(ctx.FIRST()));
841+
} else if (ctx.LAST() != null) {
842+
tokens.add(new JpaQueryParsingToken(ctx.LAST()));
843+
}
802844

803845
return builder;
804846
}
@@ -1441,6 +1483,11 @@ public QueryTokenStream visitString_expression(JpqlParser.String_expressionConte
14411483
builder.append(visit(ctx.case_expression()));
14421484
} else if (ctx.function_invocation() != null) {
14431485
builder.append(visit(ctx.function_invocation()));
1486+
} else if (ctx.op != null) {
1487+
1488+
tokens.addAll(visit(ctx.string_expression(0)));
1489+
tokens.add(new JpaQueryParsingToken(ctx.op));
1490+
tokens.addAll(visit(ctx.string_expression(1)));
14441491
} else if (ctx.subquery() != null) {
14451492

14461493
builder.append(TOKEN_OPEN_PAREN);

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

+60-30
Original file line numberDiff line numberDiff line change
@@ -1470,42 +1470,36 @@ void hqlQueries() {
14701470
@Test // GH-2962
14711471
void orderByWithNullsFirstOrLastShouldWork() {
14721472

1473-
assertThatNoException().isThrownBy(() -> {
1474-
parseWithoutChanges("""
1475-
select a,
1476-
case
1477-
when a.geaendertAm is null then a.erstelltAm
1478-
else a.geaendertAm end as mutationAm
1479-
from Element a
1480-
where a.erstelltDurch = :variable
1481-
order by mutationAm desc nulls first
1482-
""");
1483-
});
1484-
1485-
assertThatNoException().isThrownBy(() -> {
1486-
parseWithoutChanges("""
1487-
select a,
1488-
case
1489-
when a.geaendertAm is null then a.erstelltAm
1490-
else a.geaendertAm end as mutationAm
1491-
from Element a
1492-
where a.erstelltDurch = :variable
1493-
order by mutationAm desc nulls last
1473+
assertQuery("""
1474+
select a,
1475+
case
1476+
when a.geaendertAm is null then a.erstelltAm
1477+
else a.geaendertAm end as mutationAm
1478+
from Element a
1479+
where a.erstelltDurch = :variable
1480+
order by mutationAm desc nulls first
1481+
""");
1482+
1483+
assertQuery("""
1484+
select a,
1485+
case
1486+
when a.geaendertAm is null then a.erstelltAm
1487+
else a.geaendertAm end as mutationAm
1488+
from Element a
1489+
where a.erstelltDurch = :variable
1490+
order by mutationAm desc nulls last
14941491
""");
1495-
});
14961492
}
14971493

14981494
@Test // GH-2964
14991495
void roundFunctionShouldWorkLikeAnyOtherFunction() {
15001496

1501-
assertThatNoException().isThrownBy(() -> {
1502-
parseWithoutChanges("""
1503-
select round(count(ri) * 100 / max(ri.receipt.positions), 0) as perc
1504-
from StockOrderItem oi
1505-
right join StockReceiptItem ri
1506-
on ri.article = oi.article
1507-
""");
1508-
});
1497+
assertQuery("""
1498+
select round(count(ri)*100/max(ri.receipt.positions), 0) as perc
1499+
from StockOrderItem oi
1500+
right join StockReceiptItem ri
1501+
on ri.article = oi.article
1502+
""");
15091503
}
15101504

15111505
@Test // GH-2981
@@ -1632,6 +1626,42 @@ void powerShouldBeLegalInAQuery() {
16321626
assertQuery("select e.power.id from MyEntity e");
16331627
}
16341628

1629+
@Test // GH-3136
1630+
void doublePipeShouldBeValidAsAStringConcatOperator() {
1631+
1632+
assertQuery("""
1633+
select e.name || ' ' || e.title
1634+
from Employee e
1635+
""");
1636+
}
1637+
1638+
@Test // GH-3136
1639+
void additionalStringOperationsShouldWork() {
1640+
1641+
assertQuery("""
1642+
select
1643+
replace(e.name, 'Baggins', 'Proudfeet'),
1644+
left(e.role, 4),
1645+
right(e.home, 5),
1646+
cast(e.distance_from_home, int)
1647+
from Employee e
1648+
""");
1649+
}
1650+
1651+
@Test // GH-3136
1652+
void combinedSelectStatementsShouldWork() {
1653+
1654+
assertQuery("""
1655+
select e from Employee e where e.last_name = 'Baggins'
1656+
intersect
1657+
select e from Employee e where e.first_name = 'Samwise'
1658+
union
1659+
select e from Employee e where e.home = 'The Shire'
1660+
except
1661+
select e from Employee e where e.home = 'Isengard'
1662+
""");
1663+
}
1664+
16351665
@Test // GH-3219
16361666
void extractFunctionShouldSupportAdditionalExtensions() {
16371667

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

+53
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,59 @@ void powerShouldBeLegalInAQuery() {
10101010
assertQuery("select e.power.id from MyEntity e");
10111011
}
10121012

1013+
@Test // GH-3136
1014+
void doublePipeShouldBeValidAsAStringConcatOperator() {
1015+
1016+
assertQuery("""
1017+
select e.name || ' ' || e.title
1018+
from Employee e
1019+
""");
1020+
}
1021+
1022+
@Test // GH-3136
1023+
void combinedSelectStatementsShouldWork() {
1024+
1025+
assertQuery("""
1026+
select e from Employee e where e.last_name = 'Baggins'
1027+
intersect
1028+
select e from Employee e where e.first_name = 'Samwise'
1029+
union
1030+
select e from Employee e where e.home = 'The Shire'
1031+
except
1032+
select e from Employee e where e.home = 'Isengard'
1033+
""");
1034+
}
1035+
1036+
@Disabled
1037+
@Test // GH-3136
1038+
void additionalStringOperationsShouldWork() {
1039+
1040+
assertQuery("""
1041+
select
1042+
replace(e.name, 'Baggins', 'Proudfeet'),
1043+
left(e.role, 4),
1044+
right(e.home, 5),
1045+
cast(e.distance_from_home, int)
1046+
from Employee e
1047+
""");
1048+
}
1049+
1050+
@Test // GH-3136
1051+
void orderByWithNullsFirstOrLastShouldWork() {
1052+
1053+
assertQuery("""
1054+
select a
1055+
from Element a
1056+
order by mutationAm desc nulls first
1057+
""");
1058+
1059+
assertQuery("""
1060+
select a
1061+
from Element a
1062+
order by mutationAm desc nulls last
1063+
""");
1064+
}
1065+
10131066
@ParameterizedTest // GH-3342
10141067
@ValueSource(strings = { "select 1 as value from User u", "select -1 as value from User u",
10151068
"select +1 as value from User u", "select +1 * -100 as value from User u",

0 commit comments

Comments
 (0)