Skip to content

Commit d1dc6dc

Browse files
committed
Add support for HQL XML functions.
We now support the xmlelement(…), xmlforest(…), xmlpi(…), xmlquery(…), xmlexists(…), xmlagg(…), and xmltable(…) functions. Closes #3883
1 parent 4861e0b commit d1dc6dc

File tree

3 files changed

+215
-11
lines changed

3 files changed

+215
-11
lines changed

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1337,11 +1337,7 @@ jsonObjectAggFunction
13371337
: JSON_OBJECTAGG '(' KEY? expressionOrPredicate (VALUE | ':') expressionOrPredicate jsonNullClause? jsonUniqueKeysClause? ')' filterClause?;
13381338

13391339
jsonPassingClause
1340-
: PASSING jsonPassingItem (',' jsonPassingItem)*
1341-
;
1342-
1343-
jsonPassingItem
1344-
: expressionOrPredicate AS identifier
1340+
: PASSING aliasedExpressionOrPredicate (',' aliasedExpressionOrPredicate)*
13451341
;
13461342

13471343
jsonNullClause
@@ -1387,11 +1383,11 @@ xmlElementFunction
13871383
;
13881384

13891385
xmlAttributesFunction
1390-
: XMLATTRIBUTES '(' expressionOrPredicate AS identifier (',' expressionOrPredicate AS identifier)* ')'
1386+
: XMLATTRIBUTES '(' aliasedExpressionOrPredicate (',' aliasedExpressionOrPredicate)* ')'
13911387
;
13921388

13931389
xmlForestFunction
1394-
: XMLFOREST '(' expressionOrPredicate (AS identifier)? (',' expressionOrPredicate (AS identifier)?)* ')'
1390+
: XMLFOREST '(' potentiallyAliasedExpressionOrPredicate (',' potentiallyAliasedExpressionOrPredicate)* ')'
13951391
;
13961392

13971393
xmlPiFunction
@@ -1406,6 +1402,14 @@ xmlExistsFunction
14061402
xmlAggFunction
14071403
: XMLAGG '(' expression orderByClause? ')' filterClause? overClause?;
14081404

1405+
aliasedExpressionOrPredicate
1406+
: expressionOrPredicate AS identifier
1407+
;
1408+
1409+
potentiallyAliasedExpressionOrPredicate
1410+
: expressionOrPredicate (AS identifier)?
1411+
;
1412+
14091413
xmlTableFunction
14101414
: XMLTABLE '(' expression PASSING expression xmlTableColumnsClause ')';
14111415

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

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1447,7 +1447,7 @@ public QueryTokenStream visitJsonPassingClause(HqlParser.JsonPassingClauseContex
14471447
QueryRendererBuilder builder = QueryRenderer.builder();
14481448

14491449
builder.append(QueryTokens.expression(ctx.PASSING()));
1450-
builder.append(QueryTokenStream.concat(ctx.jsonPassingItem(), this::visit, TOKEN_COMMA));
1450+
builder.append(QueryTokenStream.concat(ctx.aliasedExpressionOrPredicate(), this::visit, TOKEN_COMMA));
14511451

14521452
return builder;
14531453
}
@@ -1482,6 +1482,142 @@ public QueryTokenStream visitJsonTableColumns(HqlParser.JsonTableColumnsContext
14821482
return QueryTokenStream.concat(ctx.jsonTableColumn(), this::visit, TOKEN_COMMA);
14831483
}
14841484

1485+
@Override
1486+
public QueryTokenStream visitXmlElementFunction(HqlParser.XmlElementFunctionContext ctx) {
1487+
1488+
QueryRendererBuilder builder = QueryRenderer.builder();
1489+
1490+
builder.append(QueryTokens.expression(ctx.NAME()));
1491+
builder.append(visit(ctx.identifier()));
1492+
1493+
if (ctx.xmlAttributesFunction() != null) {
1494+
builder.append(TOKEN_COMMA);
1495+
builder.append(visit(ctx.xmlAttributesFunction()));
1496+
}
1497+
1498+
if (!CollectionUtils.isEmpty(ctx.expressionOrPredicate())) {
1499+
builder.append(TOKEN_COMMA);
1500+
builder.append(QueryTokenStream.concat(ctx.expressionOrPredicate(), this::visit, TOKEN_COMMA));
1501+
}
1502+
1503+
return QueryTokenStream.ofFunction(ctx.XMLELEMENT(), builder);
1504+
}
1505+
1506+
@Override
1507+
public QueryTokenStream visitXmlAttributesFunction(HqlParser.XmlAttributesFunctionContext ctx) {
1508+
1509+
QueryRendererBuilder builder = QueryRenderer.builder();
1510+
1511+
builder.appendExpression(QueryTokenStream.concat(ctx.aliasedExpressionOrPredicate(), this::visit, TOKEN_COMMA));
1512+
1513+
return QueryTokenStream.ofFunction(ctx.XMLATTRIBUTES(), builder);
1514+
}
1515+
1516+
@Override
1517+
public QueryTokenStream visitXmlForestFunction(HqlParser.XmlForestFunctionContext ctx) {
1518+
1519+
QueryRendererBuilder builder = QueryRenderer.builder();
1520+
1521+
builder.appendExpression(
1522+
QueryTokenStream.concat(ctx.potentiallyAliasedExpressionOrPredicate(), this::visit, TOKEN_COMMA));
1523+
1524+
return QueryTokenStream.ofFunction(ctx.XMLFOREST(), builder);
1525+
}
1526+
1527+
@Override
1528+
public QueryTokenStream visitXmlPiFunction(HqlParser.XmlPiFunctionContext ctx) {
1529+
1530+
QueryRendererBuilder builder = QueryRenderer.builder();
1531+
1532+
builder.append(QueryTokens.expression(ctx.NAME()));
1533+
builder.append(visit(ctx.identifier()));
1534+
1535+
if (ctx.expression() != null) {
1536+
builder.append(TOKEN_COMMA);
1537+
builder.append(visit(ctx.expression()));
1538+
}
1539+
1540+
return QueryTokenStream.ofFunction(ctx.XMLPI(), builder);
1541+
}
1542+
1543+
@Override
1544+
public QueryTokenStream visitXmlQueryFunction(HqlParser.XmlQueryFunctionContext ctx) {
1545+
1546+
QueryRendererBuilder builder = QueryRenderer.builder();
1547+
1548+
builder.appendExpression(visit(ctx.expression(0)));
1549+
builder.append(QueryTokens.expression(ctx.PASSING()));
1550+
builder.appendExpression(visit(ctx.expression(1)));
1551+
1552+
return QueryTokenStream.ofFunction(ctx.XMLQUERY(), builder);
1553+
}
1554+
1555+
@Override
1556+
public QueryTokenStream visitXmlExistsFunction(HqlParser.XmlExistsFunctionContext ctx) {
1557+
1558+
QueryRendererBuilder builder = QueryRenderer.builder();
1559+
1560+
builder.appendExpression(visit(ctx.expression(0)));
1561+
builder.append(QueryTokens.expression(ctx.PASSING()));
1562+
builder.appendExpression(visit(ctx.expression(1)));
1563+
1564+
return QueryTokenStream.ofFunction(ctx.XMLEXISTS(), builder);
1565+
}
1566+
1567+
@Override
1568+
public QueryTokenStream visitXmlAggFunction(HqlParser.XmlAggFunctionContext ctx) {
1569+
1570+
QueryRendererBuilder args = QueryRenderer.builder();
1571+
1572+
args.appendExpression(visit(ctx.expression()));
1573+
if (ctx.orderByClause() != null) {
1574+
args.appendExpression(visit(ctx.orderByClause()));
1575+
}
1576+
1577+
QueryTokenStream function = QueryTokenStream.ofFunction(ctx.XMLAGG(), args);
1578+
1579+
if (ctx.filterClause() == null && ctx.overClause() == null) {
1580+
return function;
1581+
}
1582+
1583+
QueryRendererBuilder builder = QueryRenderer.builder();
1584+
builder.appendExpression(function);
1585+
1586+
if (ctx.filterClause() != null) {
1587+
builder.appendExpression(visit(ctx.filterClause()));
1588+
}
1589+
1590+
if (ctx.overClause() != null) {
1591+
builder.appendExpression(visit(ctx.overClause()));
1592+
}
1593+
1594+
return builder;
1595+
}
1596+
1597+
@Override
1598+
public QueryTokenStream visitXmlTableFunction(HqlParser.XmlTableFunctionContext ctx) {
1599+
1600+
QueryRendererBuilder args = QueryRenderer.builder();
1601+
1602+
args.appendExpression(visit(ctx.expression(0)));
1603+
args.append(QueryTokens.expression(ctx.PASSING()));
1604+
args.appendExpression(visit(ctx.expression(1)));
1605+
args.appendExpression(visit(ctx.xmlTableColumnsClause()));
1606+
1607+
return QueryTokenStream.ofFunction(ctx.XMLTABLE(), args);
1608+
}
1609+
1610+
@Override
1611+
public QueryTokenStream visitXmlTableColumnsClause(HqlParser.XmlTableColumnsClauseContext ctx) {
1612+
1613+
QueryRendererBuilder builder = QueryRenderer.builder();
1614+
1615+
builder.append(QueryTokens.expression(ctx.COLUMNS()));
1616+
builder.append(QueryTokenStream.concat(ctx.xmlTableColumn(), this::visit, TOKEN_COMMA));
1617+
1618+
return builder;
1619+
}
1620+
14851621
@Override
14861622
public QueryTokenStream visitPath(HqlParser.PathContext ctx) {
14871623
return QueryTokenStream.concat(ctx.children, this::visit, EMPTY_TOKEN);

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

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@
2626
import org.junit.jupiter.params.provider.ValueSource;
2727

2828
/**
29-
* Tests built around examples of HQL found in
30-
* https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc and
31-
* https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#query-language<br/>
29+
* Tests built around examples of HQL found in <a href=
30+
* "https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc">...</a> and
31+
* <a href=
32+
* "https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#query-language">...</a><br/>
3233
* <br/>
3334
* IMPORTANT: Purely verifies the parser without any transformations.
3435
*
@@ -2757,4 +2758,67 @@ join lateral json_table(e.json, '$' columns(theInt Integer,
27572758
nonExisting exists) ERROR ON ERROR)
27582759
""");
27592760
}
2761+
2762+
@Test // GH-3883
2763+
void xmlElement() {
2764+
2765+
assertQuery("select xmlelement(name myelement)");
2766+
assertQuery(
2767+
"select xmlelement(name `my-element`, xmlattributes(123 as attr1, '456' as `attr-2`), 'myContent', xmlelement(name empty))");
2768+
}
2769+
2770+
@Test // GH-3883
2771+
void xmlForest() {
2772+
2773+
assertQuery("select xmlforest(123 as e1)");
2774+
assertQuery("select xmlforest(123 as e1, 'text' as e2)");
2775+
}
2776+
2777+
@Test // GH-3883
2778+
void xmlPi() {
2779+
2780+
assertQuery("select xmlpi(name php)");
2781+
assertQuery("select xmlpi(name php, foo)");
2782+
}
2783+
2784+
@Test // GH-3883
2785+
void xmlQuery() {
2786+
2787+
assertQuery("select xmlquery('/a/val' passing '<a><val>asd</val></a>')");
2788+
assertQuery("select xmlquery('/a/val' passing e.xml) from Entity e");
2789+
}
2790+
2791+
@Test // GH-3883
2792+
void xmlExists() {
2793+
2794+
assertQuery("select xmlexists('/a/val' passing '<a><val>asd</val></a>')");
2795+
assertQuery("select xmlexists('/a/val' passing e.xml) from Entity e");
2796+
}
2797+
2798+
@Test // GH-3883
2799+
void xmlAgg() {
2800+
2801+
assertQuery("select xmlagg(xmlelement(name a, e.theString))");
2802+
assertQuery(
2803+
"select xmlagg(xmlelement(name a, e.theString) order by e.id) FILTER (WHERE foo = bar) OVER (PARTITION BY expression) from Entity e");
2804+
}
2805+
2806+
@Test // GH-3883
2807+
void xmlTable() {
2808+
2809+
assertQuery("""
2810+
select
2811+
t.nonExistingWithDefault
2812+
from xmltable('/root/elem' passing :xml columns theInt Integer,
2813+
theFloat Float,
2814+
theString String,
2815+
theBoolean Boolean,
2816+
theNull String,
2817+
theObject XML,
2818+
theNestedString String path 'theObject/nested',
2819+
nonExisting String,
2820+
nonExistingWithDefault String default 'none') t
2821+
""");
2822+
}
2823+
27602824
}

0 commit comments

Comments
 (0)