Skip to content

Commit b44271d

Browse files
gregturnchristophstrobl
authored andcommitted
Add support for JPA 3.2 additions to JPQL.
See: #3136 Original Pull Request: #3695
1 parent 1c79fa8 commit b44271d

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
@@ -1646,42 +1646,36 @@ void hqlQueries() {
16461646
@Test // GH-2962
16471647
void orderByWithNullsFirstOrLastShouldWork() {
16481648

1649-
assertThatNoException().isThrownBy(() -> {
1650-
parseWithoutChanges("""
1651-
select a,
1652-
case
1653-
when a.geaendertAm is null then a.erstelltAm
1654-
else a.geaendertAm end as mutationAm
1655-
from Element a
1656-
where a.erstelltDurch = :variable
1657-
order by mutationAm desc nulls first
1658-
""");
1659-
});
1660-
1661-
assertThatNoException().isThrownBy(() -> {
1662-
parseWithoutChanges("""
1663-
select a,
1664-
case
1665-
when a.geaendertAm is null then a.erstelltAm
1666-
else a.geaendertAm end as mutationAm
1667-
from Element a
1668-
where a.erstelltDurch = :variable
1669-
order by mutationAm desc nulls last
1649+
assertQuery("""
1650+
select a,
1651+
case
1652+
when a.geaendertAm is null then a.erstelltAm
1653+
else a.geaendertAm end as mutationAm
1654+
from Element a
1655+
where a.erstelltDurch = :variable
1656+
order by mutationAm desc nulls first
1657+
""");
1658+
1659+
assertQuery("""
1660+
select a,
1661+
case
1662+
when a.geaendertAm is null then a.erstelltAm
1663+
else a.geaendertAm end as mutationAm
1664+
from Element a
1665+
where a.erstelltDurch = :variable
1666+
order by mutationAm desc nulls last
16701667
""");
1671-
});
16721668
}
16731669

16741670
@Test // GH-2964
16751671
void roundFunctionShouldWorkLikeAnyOtherFunction() {
16761672

1677-
assertThatNoException().isThrownBy(() -> {
1678-
parseWithoutChanges("""
1679-
select round(count(ri) * 100 / max(ri.receipt.positions), 0) as perc
1680-
from StockOrderItem oi
1681-
right join StockReceiptItem ri
1682-
on ri.article = oi.article
1683-
""");
1684-
});
1673+
assertQuery("""
1674+
select round(count(ri)*100/max(ri.receipt.positions), 0) as perc
1675+
from StockOrderItem oi
1676+
right join StockReceiptItem ri
1677+
on ri.article = oi.article
1678+
""");
16851679
}
16861680

16871681
@Test // GH-3711
@@ -1831,6 +1825,42 @@ void powerShouldBeLegalInAQuery() {
18311825
assertQuery("select e.power.id from MyEntity e");
18321826
}
18331827

1828+
@Test // GH-3136
1829+
void doublePipeShouldBeValidAsAStringConcatOperator() {
1830+
1831+
assertQuery("""
1832+
select e.name || ' ' || e.title
1833+
from Employee e
1834+
""");
1835+
}
1836+
1837+
@Test // GH-3136
1838+
void additionalStringOperationsShouldWork() {
1839+
1840+
assertQuery("""
1841+
select
1842+
replace(e.name, 'Baggins', 'Proudfeet'),
1843+
left(e.role, 4),
1844+
right(e.home, 5),
1845+
cast(e.distance_from_home, int)
1846+
from Employee e
1847+
""");
1848+
}
1849+
1850+
@Test // GH-3136
1851+
void combinedSelectStatementsShouldWork() {
1852+
1853+
assertQuery("""
1854+
select e from Employee e where e.last_name = 'Baggins'
1855+
intersect
1856+
select e from Employee e where e.first_name = 'Samwise'
1857+
union
1858+
select e from Employee e where e.home = 'The Shire'
1859+
except
1860+
select e from Employee e where e.home = 'Isengard'
1861+
""");
1862+
}
1863+
18341864
@Test // GH-3219
18351865
void extractFunctionShouldSupportAdditionalExtensions() {
18361866

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)