From 3b07ed91c3bfecd3d7e97cc6137e1746331e8722 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Sat, 21 Sep 2024 04:19:34 +0200 Subject: [PATCH] HHH-18497 Add xmlagg function --- .../chapters/query/hql/QueryLanguage.adoc | 25 ++ .../chapters/query/hql/extras/xmlagg_bnf.txt | 1 + .../community/dialect/DB2LegacyDialect.java | 1 + .../dialect/OracleLegacyDialect.java | 1 + .../dialect/PostgreSQLLegacyDialect.java | 1 + .../dialect/SQLServerLegacyDialect.java | 1 + .../org/hibernate/grammars/hql/HqlLexer.g4 | 1 + .../org/hibernate/grammars/hql/HqlParser.g4 | 9 + .../org/hibernate/dialect/DB2Dialect.java | 1 + .../org/hibernate/dialect/OracleDialect.java | 1 + .../hibernate/dialect/PostgreSQLDialect.java | 1 + .../hibernate/dialect/SQLServerDialect.java | 1 + .../function/CommonFunctionFactory.java | 32 +++ .../function/xml/SQLServerXmlAggFunction.java | 244 ++++++++++++++++++ .../dialect/function/xml/XmlAggFunction.java | 126 +++++++++ .../criteria/HibernateCriteriaBuilder.java | 37 +++ .../spi/HibernateCriteriaBuilderDelegate.java | 24 ++ .../hql/internal/SemanticQueryBuilder.java | 20 ++ .../org/hibernate/query/sqm/NodeBuilder.java | 12 + .../function/NamedSqmFunctionDescriptor.java | 2 +- .../sqm/internal/SqmCriteriaNodeBuilder.java | 20 ++ .../ast/tree/from/FunctionTableReference.java | 2 +- .../orm/test/function/xml/XmlAggTest.java | 37 +++ .../orm/test/query/hql/XmlFunctionTests.java | 15 ++ .../orm/junit/DialectFeatureChecks.java | 6 + 25 files changed, 619 insertions(+), 2 deletions(-) create mode 100644 documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlagg_bnf.txt create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlAggFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlAggFunction.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlAggTest.java diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index 49ec94ee97..b9689d0548 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -2178,6 +2178,7 @@ it is necessary to enable the `hibernate.query.hql.xml_functions_enabled` config | `xmlpi()` | Constructs an XML processing instruction | `xmlquery()` | Extracts content from XML document using XQuery or XPath | `xmlexists()` | Checks if an XQuery or XPath expression exists in an XML document +| `xmlagg()` | Aggregates XML elements by concatenation |=== @@ -2350,6 +2351,30 @@ include::{xml-example-dir-hql}/XmlExistsTest.java[tags=hql-xmlexists-example] WARNING: SAP HANA, MySQL, MariaDB and HSQLDB do not support this function. +[[hql-xmlagg-function]] +===== `xmlagg()` + +Aggregates XML elements by concatenation. + +[[hql-xmlexists-bnf]] +[source, antlrv4, indent=0] +---- +include::{extrasdir}/xmlagg_bnf.txt[] +---- + +This aggregate function is similar to an <> +since it allows to specify the order in which elements are aggregated, but uses a special syntax. + +[[hql-xmlagg-example]] +==== +[source, java, indent=0] +---- +include::{xml-example-dir-hql}/XmlAggTest.java[tags=hql-xmlagg-example] +---- +==== + +WARNING: SAP HANA, MySQL, MariaDB and HSQLDB do not support this function. + [[hql-user-defined-functions]] ==== Native and user-defined functions diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlagg_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlagg_bnf.txt new file mode 100644 index 0000000000..ea6637bd7c --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlagg_bnf.txt @@ -0,0 +1 @@ +"xmlagg(" expression orderByClause? ")" filterClause? overClause? \ No newline at end of file diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java index 7ce0eb6cc1..d2b1d7655d 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java @@ -448,6 +448,7 @@ public class DB2LegacyDialect extends Dialect { functionFactory.xmlpi(); functionFactory.xmlquery_db2(); functionFactory.xmlexists(); + functionFactory.xmlagg(); } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java index 4f60897297..d7dd7bcc48 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java @@ -333,6 +333,7 @@ public class OracleLegacyDialect extends Dialect { functionFactory.xmlpi(); functionFactory.xmlquery_oracle(); functionFactory.xmlexists(); + functionFactory.xmlagg(); } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java index 890f7a45f0..b2209a66bc 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java @@ -676,6 +676,7 @@ public class PostgreSQLLegacyDialect extends Dialect { functionFactory.xmlpi(); functionFactory.xmlquery_postgresql(); functionFactory.xmlexists(); + functionFactory.xmlagg(); if ( getVersion().isSameOrAfter( 9, 4 ) ) { functionFactory.makeDateTimeTimestamp(); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java index c2533daee5..d95e8e468d 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java @@ -420,6 +420,7 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect { functionFactory.xmlpi_sqlserver(); functionFactory.xmlquery_sqlserver(); functionFactory.xmlexists_sqlserver(); + functionFactory.xmlagg_sqlserver(); if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); functionFactory.jsonArrayAgg_sqlserver( getVersion().isSameOrAfter( 16 ) ); diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 index a7f9f16b26..ea55107b22 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 @@ -330,6 +330,7 @@ WITH : [wW] [iI] [tT] [hH]; WITHIN : [wW] [iI] [tT] [hH] [iI] [nN]; WITHOUT : [wW] [iI] [tT] [hH] [oO] [uU] [tT]; WRAPPER : [wW] [rR] [aA] [pP] [pP] [eE] [rR]; +XMLAGG : [xX] [mM] [lL] [aA] [gG] [gG]; XMLATTRIBUTES : [xX] [mM] [lL] [aA] [tT] [tT] [rR] [iI] [bB] [uU] [tT] [eE] [sS]; XMLELEMENT : [xX] [mM] [lL] [eE] [lL] [eE] [mM] [eE] [nN] [tT]; XMLEXISTS : [xX] [mM] [lL] [eE] [xX] [iI] [sS] [tT] [sS]; diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 index 0ace1b3587..a4e867ed2c 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 @@ -1723,6 +1723,7 @@ xmlFunction | xmlpiFunction | xmlqueryFunction | xmlexistsFunction + | xmlaggFunction ; /** @@ -1767,6 +1768,13 @@ xmlexistsFunction : XMLEXISTS LEFT_PAREN expression PASSING expression RIGHT_PAREN ; +/** + * The 'xmlexists()' function + */ +xmlaggFunction + : XMLAGG LEFT_PAREN expression orderByClause? RIGHT_PAREN filterClause? overClause? + ; + /** * Support for "soft" keywords which may be used as identifiers * @@ -1973,6 +1981,7 @@ xmlexistsFunction | WITHIN | WITHOUT | WRAPPER + | XMLAGG | XMLATTRIBUTES | XMLELEMENT | XMLEXISTS diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java index 5cb6fe471b..f749dee6c7 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -433,6 +433,7 @@ public class DB2Dialect extends Dialect { functionFactory.xmlpi(); functionFactory.xmlquery_db2(); functionFactory.xmlexists(); + functionFactory.xmlagg(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index 0d0c522888..c64f10b4fe 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -423,6 +423,7 @@ public class OracleDialect extends Dialect { functionFactory.xmlpi(); functionFactory.xmlquery_oracle(); functionFactory.xmlexists(); + functionFactory.xmlagg(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index ef3660c8fa..016ba056f8 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -637,6 +637,7 @@ public class PostgreSQLDialect extends Dialect { functionFactory.xmlpi(); functionFactory.xmlquery_postgresql(); functionFactory.xmlexists(); + functionFactory.xmlagg(); functionFactory.makeDateTimeTimestamp(); // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index 6b932817e5..d00622b886 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -438,6 +438,7 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { functionFactory.xmlpi_sqlserver(); functionFactory.xmlquery_sqlserver(); functionFactory.xmlexists_sqlserver(); + functionFactory.xmlagg_sqlserver(); if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); functionFactory.jsonArrayAgg_sqlserver( getVersion().isSameOrAfter( 16 ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java index 08c7ec7d46..9a92da77cc 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java @@ -158,13 +158,17 @@ import org.hibernate.dialect.function.xml.H2XmlElementFunction; import org.hibernate.dialect.function.xml.H2XmlForestFunction; import org.hibernate.dialect.function.xml.H2XmlPiFunction; import org.hibernate.dialect.function.xml.PostgreSQLXmlQueryFunction; +import org.hibernate.dialect.function.xml.SQLServerXmlAggFunction; import org.hibernate.dialect.function.xml.SQLServerXmlConcatFunction; import org.hibernate.dialect.function.xml.SQLServerXmlElementFunction; +import org.hibernate.dialect.function.xml.SQLServerXmlExistsFunction; import org.hibernate.dialect.function.xml.SQLServerXmlForestFunction; import org.hibernate.dialect.function.xml.SQLServerXmlPiFunction; import org.hibernate.dialect.function.xml.SQLServerXmlQueryFunction; +import org.hibernate.dialect.function.xml.XmlAggFunction; import org.hibernate.dialect.function.xml.XmlConcatFunction; import org.hibernate.dialect.function.xml.XmlElementFunction; +import org.hibernate.dialect.function.xml.XmlExistsFunction; import org.hibernate.dialect.function.xml.XmlForestFunction; import org.hibernate.dialect.function.xml.XmlPiFunction; import org.hibernate.dialect.function.xml.XmlQueryFunction; @@ -4246,4 +4250,32 @@ public class CommonFunctionFactory { public void xmlquery_sqlserver() { functionRegistry.register( "xmlquery", new SQLServerXmlQueryFunction( typeConfiguration ) ); } + + /** + * Standard xmlexists() function + */ + public void xmlexists() { + functionRegistry.register( "xmlexists", new XmlExistsFunction( typeConfiguration ) ); + } + + /** + * SQL Server xmlexists() function + */ + public void xmlexists_sqlserver() { + functionRegistry.register( "xmlexists", new SQLServerXmlExistsFunction( typeConfiguration ) ); + } + + /** + * Standard xmlagg() function + */ + public void xmlagg() { + functionRegistry.register( "xmlagg", new XmlAggFunction( typeConfiguration ) ); + } + + /** + * SQL Server xmlagg() function + */ + public void xmlagg_sqlserver() { + functionRegistry.register( "xmlagg", new SQLServerXmlAggFunction( typeConfiguration ) ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlAggFunction.java new file mode 100644 index 0000000000..125740f723 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlAggFunction.java @@ -0,0 +1,244 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.hibernate.dialect.function.json.ExpressionTypeHelper; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.function.SelfRenderingFunctionSqlAstExpression; +import org.hibernate.query.sqm.function.SelfRenderingOrderedSetAggregateFunctionSqlAstExpression; +import org.hibernate.query.sqm.function.SelfRenderingSqmOrderedSetAggregateFunction; +import org.hibernate.query.sqm.spi.SqmCreationHelper; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.predicate.SqmPredicate; +import org.hibernate.query.sqm.tree.select.SqmOrderByClause; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.SqlAstJoinType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.AbstractSqlAstWalker; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Distinct; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.FunctionExpression; +import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression; +import org.hibernate.sql.ast.tree.from.FunctionTableGroup; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupJoin; +import org.hibernate.sql.ast.tree.from.TableReferenceJoin; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.spi.TypeConfiguration; + + +/** + * SQL Server xmlagg function. + */ +public class SQLServerXmlAggFunction extends XmlAggFunction { + + public SQLServerXmlAggFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public SelfRenderingSqmOrderedSetAggregateFunction generateSqmOrderedSetAggregateFunctionExpression( + List> arguments, + SqmPredicate filter, + SqmOrderByClause withinGroupClause, + ReturnableType impliedResultType, + QueryEngine queryEngine) { + return new SelfRenderingSqmOrderedSetAggregateFunction<>( + this, + this, + arguments, + filter, + withinGroupClause, + impliedResultType, + getArgumentsValidator(), + getReturnTypeResolver(), + queryEngine.getCriteriaBuilder(), + getName() + ) { + @Override + public Expression convertToSqlAst(SqmToSqlAstConverter walker) { + // SQL Server can't aggregate an argument that contains a subquery, + // which is a bummer because xmlelement and xmlforest implementations require subqueries, + // but we can apply a trick to make this still work. + // Essentially, we try to move the subquery into the from clause and mark it as lateral. + // Then we can replace the original expression with that new table reference. + final SelfRenderingOrderedSetAggregateFunctionSqlAstExpression expression = (SelfRenderingOrderedSetAggregateFunctionSqlAstExpression) super.convertToSqlAst( walker ); + final Expression xml = (Expression) expression.getArguments().get( 0 ); + final Set qualifiers = ColumnQualifierCollectorSqlAstWalker.determineColumnQualifiers( xml ); + // If the argument contains a subquery, we will receive the column qualifiers that are used + if ( !qualifiers.isEmpty() ) { + // Register a query transformer to register the lateral table group join + walker.registerQueryTransformer( (cteContainer, querySpec, converter) -> { + // Find the table group which the subquery refers to + final TableGroup tableGroup = querySpec.getFromClause().queryTableGroups( + tg -> { + final String primaryVariable = tg.getPrimaryTableReference() + .getIdentificationVariable(); + if ( qualifiers.contains( primaryVariable ) ) { + return tg; + } + for ( TableReferenceJoin tableReferenceJoin : tg.getTableReferenceJoins() ) { + final String variable = tableReferenceJoin.getJoinedTableReference() + .getIdentificationVariable(); + if ( qualifiers.contains( variable ) ) { + return tg; + } + } + return null; + } + ); + if ( tableGroup != null ) { + // Generate the lateral subquery + final String alias = "gen" + SqmCreationHelper.acquireUniqueAlias(); + final FunctionTableGroup lateralGroup = new FunctionTableGroup( + new NavigablePath( "generated", alias ), + null, + new SelfRenderingFunctionSqlAstExpression( + "helper", + (sqlAppender, sqlAstArguments, returnType, walker1) -> { + sqlAppender.appendSql( "(select " ); + xml.accept( walker1 ); + sqlAppender.appendSql( " v)" ); + }, + List.of(), + null, + null + ), + alias, + List.of("v"), + true, + true, + null + ); + tableGroup.addTableGroupJoin( + new TableGroupJoin( + lateralGroup.getNavigablePath(), + SqlAstJoinType.INNER, + lateralGroup + ) + ); + // Replace the original expression that contains a subquery with a simple column reference, + // that points to the newly created lateral table group + //noinspection unchecked + ( (List) expression.getArguments() ).set( + 0, + new ColumnReference( + lateralGroup.getPrimaryTableReference(), + "v", + expression.getJdbcMapping() + ) + ); + } + return querySpec; + } ); + } + return expression; + } + }; + } + + static class ColumnQualifierCollectorSqlAstWalker extends AbstractSqlAstWalker { + + private static final Set POTENTIAL_SUBQUERY_FUNCTIONS = Set.of( + "xmlelement", + "xmlforest" + ); + private final Set columnQualifiers = new HashSet<>(); + private boolean potentialSubquery; + + public static Set determineColumnQualifiers(SqlAstNode node) { + final ColumnQualifierCollectorSqlAstWalker walker = new ColumnQualifierCollectorSqlAstWalker(); + node.accept( walker ); + return walker.potentialSubquery ? walker.columnQualifiers : Set.of(); + } + + @Override + public void visitSelfRenderingExpression(SelfRenderingExpression expression) { + if ( expression instanceof FunctionExpression functionExpression + && POTENTIAL_SUBQUERY_FUNCTIONS.contains( functionExpression.getFunctionName() ) ) { + potentialSubquery = true; + } + super.visitSelfRenderingExpression( expression ); + } + + @Override + public void visitColumnReference(ColumnReference columnReference) { + if ( columnReference.getQualifier() != null ) { + columnQualifiers.add( columnReference.getQualifier() ); + } + } + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + ReturnableType returnType, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null && !translator.supportsFilterClause(); + sqlAppender.appendSql( "cast(string_agg(" ); + final SqlAstNode firstArg = sqlAstArguments.get( 0 ); + final Expression arg; + if ( firstArg instanceof Distinct ) { + sqlAppender.appendSql( "distinct " ); + arg = ( (Distinct) firstArg ).getExpression(); + } + else { + arg = (Expression) firstArg; + } + final boolean needsCast = ExpressionTypeHelper.isXml( arg ); + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + if ( caseWrapper ) { + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + sqlAppender.appendSql( " then " ); + arg.accept( translator ); + sqlAppender.appendSql( " else null end" ); + translator.getCurrentClauseStack().pop(); + } + else { + arg.accept( translator ); + } + if ( needsCast ) { + sqlAppender.appendSql( " as nvarchar(max))" ); + } + sqlAppender.appendSql( ",'')" ); + if ( withinGroup != null && !withinGroup.isEmpty() ) { + translator.getCurrentClauseStack().push( Clause.WITHIN_GROUP ); + sqlAppender.appendSql( " within group (order by " ); + withinGroup.get( 0 ).accept( translator ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( translator ); + } + sqlAppender.appendSql( ')' ); + translator.getCurrentClauseStack().pop(); + } + if ( !caseWrapper && filter != null ) { + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( " filter (where " ); + filter.accept( translator ); + sqlAppender.appendSql( ')' ); + translator.getCurrentClauseStack().pop(); + } + sqlAppender.appendSql( " as xml)" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlAggFunction.java new file mode 100644 index 0000000000..0b2c35d8da --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlAggFunction.java @@ -0,0 +1,126 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.Collections; +import java.util.List; + +import org.hibernate.dialect.function.json.ExpressionTypeHelper; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Distinct; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.XML; + +/** + * Standard xmlagg function. + */ +public class XmlAggFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public XmlAggFunction(TypeConfiguration typeConfiguration) { + super( + "xmlagg", + FunctionKind.ORDERED_SET_AGGREGATE, + StandardArgumentsValidators.composite( + new ArgumentTypesValidator( null, XML ) + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.SQLXML ) + ), + StandardFunctionArgumentTypeResolvers.impliedOrInvariant( typeConfiguration, XML ) + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + render( sqlAppender, sqlAstArguments, null, Collections.emptyList(), returnType, walker ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + ReturnableType returnType, + SqlAstTranslator walker) { + render( sqlAppender, sqlAstArguments, filter, Collections.emptyList(), returnType, walker ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + ReturnableType returnType, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null && !translator.supportsFilterClause(); + sqlAppender.appendSql( "xmlagg(" ); + final SqlAstNode firstArg = sqlAstArguments.get( 0 ); + final Expression arg; + if ( firstArg instanceof Distinct ) { + sqlAppender.appendSql( "distinct " ); + arg = ( (Distinct) firstArg ).getExpression(); + } + else { + arg = (Expression) firstArg; + } + final boolean needsCast = !ExpressionTypeHelper.isXml( arg ); + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + if ( caseWrapper ) { + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + sqlAppender.appendSql( " then " ); + arg.accept( translator ); + sqlAppender.appendSql( " else null end" ); + translator.getCurrentClauseStack().pop(); + } + else { + arg.accept( translator ); + } + if ( needsCast ) { + sqlAppender.appendSql( " as xml)" ); + } + if ( withinGroup != null && !withinGroup.isEmpty() ) { + translator.getCurrentClauseStack().push( Clause.WITHIN_GROUP ); + sqlAppender.appendSql( " order by " ); + withinGroup.get( 0 ).accept( translator ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( translator ); + } + translator.getCurrentClauseStack().pop(); + } + sqlAppender.appendSql( ')' ); + if ( !caseWrapper && filter != null ) { + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( " filter (where " ); + filter.accept( translator ); + sqlAppender.appendSql( ')' ); + translator.getCurrentClauseStack().pop(); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java index edeee1c9a9..62219014c9 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java @@ -4145,6 +4145,43 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { @Incubating JpaExpression xmlexists(Expression query, Expression xmlDocument); + /** + * @see #xmlagg(JpaOrder, JpaPredicate, JpaWindow, Expression) + */ + @Incubating + JpaExpression xmlagg(JpaOrder order, Expression argument); + + /** + * @see #xmlagg(JpaOrder, JpaPredicate, JpaWindow, Expression) + */ + @Incubating + JpaExpression xmlagg(JpaOrder order, JpaPredicate filter, Expression argument); + + /** + * @see #xmlagg(JpaOrder, JpaPredicate, JpaWindow, Expression) + */ + @Incubating + JpaExpression xmlagg(JpaOrder order, JpaWindow window, Expression argument); + + /** + * Create a {@code xmlagg} ordered set-aggregate function expression. + * + * @param order order by clause used in within group + * @param filter optional filter clause + * @param window optional window over which to apply the function + * @param argument values to join + * + * @return ordered set-aggregate expression + * + * @see #functionWithinGroup(String, Class, JpaOrder, JpaPredicate, JpaWindow, Expression...) + */ + @Incubating + JpaExpression xmlagg( + JpaOrder order, + JpaPredicate filter, + JpaWindow window, + Expression argument); + /** * Creates a named expression. The name is important for the result of the expression, * e.g. when building an {@code xmlforest}, the name acts as the XML element name. diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java index a39425b37a..00c00aea00 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java @@ -3705,6 +3705,30 @@ public class HibernateCriteriaBuilderDelegate implements HibernateCriteriaBuilde return criteriaBuilder.xmlexists( query, xmlDocument ); } + @Override + @Incubating + public JpaExpression xmlagg(JpaOrder order, Expression argument) { + return criteriaBuilder.xmlagg( order, argument ); + } + + @Override + @Incubating + public JpaExpression xmlagg(JpaOrder order, JpaPredicate filter, Expression argument) { + return criteriaBuilder.xmlagg( order, filter, argument ); + } + + @Override + @Incubating + public JpaExpression xmlagg(JpaOrder order, JpaWindow window, Expression argument) { + return criteriaBuilder.xmlagg( order, window, argument ); + } + + @Override + @Incubating + public JpaExpression xmlagg(JpaOrder order, JpaPredicate filter, JpaWindow window, Expression argument) { + return criteriaBuilder.xmlagg( order, filter, window, argument ); + } + @Override @Incubating public JpaExpression named(Expression expression, String name) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index ea2644047c..b9761e532c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -3059,6 +3059,26 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem return creationContext.getNodeBuilder().xmlexists( query, xmlDocument ); } + @Override + public SqmExpression visitXmlaggFunction(HqlParser.XmlaggFunctionContext ctx) { + checkXmlFunctionsEnabled( ctx ); + final ArrayList> arguments = new ArrayList<>( 1 ); + arguments.add( (SqmTypedNode) ctx.expression().accept( this ) ); + + return applyOverClause( + ctx.overClause(), + getFunctionDescriptor( "xmlagg" ).generateOrderedSetAggregateSqmExpression( + arguments, + getFilterExpression( ctx ), + ctx.orderByClause() == null + ? null + : visitOrderByClause( ctx.orderByClause(), false ), + null, + creationContext.getQueryEngine() + ) + ); + } + private void checkXmlFunctionsEnabled(ParserRuleContext ctx) { if ( !creationOptions.isXmlFunctionsEnabled() ) { throw new SemanticException( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java index 3dda15c549..131a67bc15 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java @@ -789,6 +789,18 @@ public interface NodeBuilder extends HibernateCriteriaBuilder, BindingContext { @Override SqmExpression xmlexists(Expression query, Expression xmlDocument); + @Override + SqmExpression xmlagg(JpaOrder order, Expression argument); + + @Override + SqmExpression xmlagg(JpaOrder order, JpaPredicate filter, Expression argument); + + @Override + SqmExpression xmlagg(JpaOrder order, JpaWindow window, Expression argument); + + @Override + SqmExpression xmlagg(JpaOrder order, JpaPredicate filter, JpaWindow window, Expression argument); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Covariant overrides diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/NamedSqmFunctionDescriptor.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/NamedSqmFunctionDescriptor.java index c2b046f56e..612b6ee27d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/NamedSqmFunctionDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/NamedSqmFunctionDescriptor.java @@ -201,7 +201,7 @@ public class NamedSqmFunctionDescriptor if ( withinGroup != null && !withinGroup.isEmpty() ) { translator.getCurrentClauseStack().push( Clause.WITHIN_GROUP ); - sqlAppender.appendSql( " within group (order by" ); + sqlAppender.appendSql( " within group (order by " ); translator.render( withinGroup.get( 0 ), argumentRenderingMode ); for ( int i = 1; i < withinGroup.size(); i++ ) { sqlAppender.appendSql( SqlAppender.COMMA_SEPARATOR_CHAR ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index 1a9b9e9c68..78730ff618 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java @@ -5792,4 +5792,24 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable { queryEngine ); } + + @Override + public SqmExpression xmlagg(JpaOrder order, Expression argument) { + return xmlagg( order, null, null, argument ); + } + + @Override + public SqmExpression xmlagg(JpaOrder order, JpaPredicate filter, Expression argument) { + return xmlagg( order, filter, null, argument ); + } + + @Override + public SqmExpression xmlagg(JpaOrder order, JpaWindow window, Expression argument) { + return xmlagg( order, null, window, argument ); + } + + @Override + public SqmExpression xmlagg(JpaOrder order, JpaPredicate filter, JpaWindow window, Expression argument) { + return functionWithinGroup( "xmlagg", String.class, order, filter, window, argument ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/FunctionTableReference.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/FunctionTableReference.java index 3315ec11e7..89f65a5586 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/FunctionTableReference.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/FunctionTableReference.java @@ -36,7 +36,7 @@ public class FunctionTableReference extends DerivedTableReference { @Override public void accept(SqlAstWalker sqlTreeWalker) { - functionExpression.accept( sqlTreeWalker ); + sqlTreeWalker.visitFunctionTableReference( this ); } @Override diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlAggTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlAggTest.java new file mode 100644 index 0000000000..aeacfdc819 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlAggTest.java @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.function.xml; + +import org.hibernate.cfg.QuerySettings; + +import org.hibernate.testing.orm.domain.StandardDomainModel; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +/** + * @author Christian Beikov + */ +@DomainModel(standardModels = StandardDomainModel.GAMBIT) +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.XML_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsXmlagg.class) +public class XmlAggTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-xmlagg-example[] + em.createQuery( "select xmlagg(xmlelement(name a, e.theString) order by e.id) from EntityOfBasics e" ).getResultList(); + //end::hql-xmlagg-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/XmlFunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/XmlFunctionTests.java index cdf352123d..64158947ba 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/XmlFunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/XmlFunctionTests.java @@ -230,6 +230,21 @@ public class XmlFunctionTests { ); } + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsXmlagg.class) + public void testXmlagg(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Tuple tuple = session.createQuery( + "select xmlagg(xmlelement(name a, e.theString) order by e.id) " + + "from from EntityOfBasics e", + Tuple.class + ).getSingleResult(); + assertXmlEquals( "DogCat", "" + tuple.get( 0, String.class ) + "" ); + } + ); + } + private void assertXmlEquals(String expected, String actual) { final Document expectedDoc = parseXml( xmlNormalize( expected ) ); final Document actualDoc = parseXml( xmlNormalize( actual ) ); diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index 456f93b0e3..c727d7090e 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -880,6 +880,12 @@ abstract public class DialectFeatureChecks { } } + public static class SupportsXmlagg implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "xmlagg" ); + } + } + public static class IsJtds implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return dialect instanceof SybaseDialect && ( (SybaseDialect) dialect ).getDriverKind() == SybaseDriverKind.JTDS;