Skip to content

Commit cd98253

Browse files
gregturnmp911de
authored andcommitted
Add support for JPA 3.2 additions to JPQL.
See #3136.
1 parent e048b0c commit cd98253

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
@@ -1471,42 +1471,36 @@ void hqlQueries() {
14711471
@Test // GH-2962
14721472
void orderByWithNullsFirstOrLastShouldWork() {
14731473

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

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

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

15121506
@Test // GH-2981
@@ -1638,6 +1632,42 @@ void powerShouldBeLegalInAQuery() {
16381632
assertQuery("select e.power.id from MyEntity e");
16391633
}
16401634

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

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)