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 455fb039bf..8af68a4983 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -7,6 +7,7 @@ :example-dir-hql: {core-project-dir}/src/test/java/org/hibernate/orm/test/hql :array-example-dir-hql: {core-project-dir}/src/test/java/org/hibernate/orm/test/function/array :json-example-dir-hql: {core-project-dir}/src/test/java/org/hibernate/orm/test/function/json +:xml-example-dir-hql: {core-project-dir}/src/test/java/org/hibernate/orm/test/function/xml :extrasdir: extras This chapter describes Hibernate Query Language (HQL) and Jakarta Persistence Query Language (JPQL). @@ -2158,6 +2159,56 @@ include::{json-example-dir-hql}/JsonArrayInsertTest.java[tags=hql-json-array-ins WARNING: SAP HANA, DB2, H2 and HSQLDB do not support this function. +[[hql-functions-xml]] +==== Functions for dealing with XML + +The following functions deal with SQL XML types, which are not supported on every database. + +NOTE: The following functions are incubating/tech-preview and to use them in HQL, +it is necessary to enable the `hibernate.query.hql.xml_functions_enabled` configuration setting. + +[[hql-xml-functions]] +|=== +| Function | Purpose + +| `xmlelement()` | Constructs an XML element from arguments +|=== + + +[[hql-xmlelement-function]] +===== `xmlelement()` + +Constructs an XML element from the arguments. + +[[hql-xmlelement-bnf]] +[source, antlrv4, indent=0] +---- +include::{extrasdir}/xmlelement_bnf.txt[] +---- + +The identifier represents the XML element name and can be quoted by using backticks. + +[[hql-xmlelement-example]] +==== +[source, java, indent=0] +---- +include::{xml-example-dir-hql}/XmlElementTest.java[tags=hql-xmlelement-example] +---- +==== + +XML element attributes can be defined by using the `xmlattributes` function as second argument. +All following arguments represent the XML content. + +[[hql-xmlelement-attributes-content-example]] +==== +[source, java, indent=0] +---- +include::{xml-example-dir-hql}/XmlElementTest.java[tags=hql-xmlelement-attributes-content-example] +---- +==== + +WARNING: SAP HANA, MySQL, MariaDB, H2 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/xmlelement_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlelement_bnf.txt new file mode 100644 index 0000000000..4e71635823 --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlelement_bnf.txt @@ -0,0 +1,5 @@ +"xmlelement(name " identifier xmlattributes? ("," expressionOrPredicate)* ")" + +xmlattributes + : "xmlattributes(" expressionOrPredicate " as " identifier ("," expressionOrPredicate " as " identifier)* ")" + ; \ 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 276adb29bf..abb2904efd 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 @@ -440,6 +440,8 @@ public class DB2LegacyDialect extends Dialect { functionFactory.jsonObjectAgg_db2(); } } + + functionFactory.xmlelement(); } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java index 8cc4b20d29..233ab7223b 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java @@ -417,6 +417,8 @@ public class H2LegacyDialect extends Dialect { // Use group_concat until 2.x as listagg was buggy functionFactory.listagg_groupConcat(); } + + functionFactory.xmlelement_h2(); } else { functionFactory.listagg_groupConcat(); 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 2e66da8197..d69f49a310 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 @@ -325,6 +325,8 @@ public class OracleLegacyDialect extends Dialect { functionFactory.jsonArrayAppend_oracle(); functionFactory.jsonArrayInsert_oracle(); } + + functionFactory.xmlelement(); } @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 6dc5e338ff..ceab48f54f 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 @@ -669,6 +669,8 @@ public class PostgreSQLLegacyDialect extends Dialect { functionFactory.jsonArrayAppend_postgresql( getVersion().isSameOrAfter( 13 ) ); functionFactory.jsonArrayInsert_postgresql(); + functionFactory.xmlelement(); + if ( getVersion().isSameOrAfter( 9, 4 ) ) { functionFactory.makeDateTimeTimestamp(); // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions 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 dfed524539..05451294aa 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 @@ -413,6 +413,7 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect { functionFactory.jsonArrayAppend_sqlserver( getVersion().isSameOrAfter( 16 ) ); functionFactory.jsonArrayInsert_sqlserver(); } + functionFactory.xmlelement_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 1737e80444..1527af719b 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 @@ -258,6 +258,7 @@ MINELEMENT : [mM] [iI] [nN] [eE] [lL] [eE] [mM] [eE] [nN] [tT]; MININDEX : [mM] [iI] [nN] [iI] [nN] [dD] [eE] [xX]; MINUTE : [mM] [iI] [nN] [uU] [tT] [eE]; MONTH : [mM] [oO] [nN] [tT] [hH]; +NAME : [nN] [aA] [mM] [eE]; NANOSECOND : [nN] [aA] [nN] [oO] [sS] [eE] [cC] [oO] [nN] [dD]; NEW : [nN] [eE] [wW]; NEXT : [nN] [eE] [xX] [tT]; @@ -329,6 +330,8 @@ 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]; +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]; YEAR : [yY] [eE] [aA] [rR]; ZONED : [zZ] [oO] [nN] [eE] [dD]; 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 dfcc5aa1a5..a70b942e54 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 @@ -1110,6 +1110,7 @@ function | jpaNonstandardFunction | columnFunction | jsonFunction + | xmlFunction | genericFunction ; @@ -1716,6 +1717,24 @@ jsonUniqueKeysClause : (WITH|WITHOUT) UNIQUE KEYS ; +xmlFunction + : xmlelementFunction + ; + +/** + * The 'xmlelement()' function + */ +xmlelementFunction + : XMLELEMENT LEFT_PAREN NAME identifier (COMMA xmlattributesFunction)? (COMMA expressionOrPredicate)* RIGHT_PAREN + ; + +/** + * The 'xmlattributes()' function + */ +xmlattributesFunction + : XMLATTRIBUTES LEFT_PAREN expressionOrPredicate AS identifier (COMMA expressionOrPredicate AS identifier)* RIGHT_PAREN + ; + /** * Support for "soft" keywords which may be used as identifiers * @@ -1847,6 +1866,7 @@ jsonUniqueKeysClause | MININDEX | MINUTE | MONTH + | NAME | NANOSECOND | NATURALID | NEW @@ -1921,6 +1941,8 @@ jsonUniqueKeysClause | WITHIN | WITHOUT | WRAPPER + | XMLATTRIBUTES + | XMLELEMENT | YEAR | ZONED) { logUseOfReservedWordAsIdentifier( getCurrentToken() ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java index ea6f1e21a7..ba40caa8b5 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java @@ -131,6 +131,7 @@ import static org.hibernate.cfg.PersistenceSettings.UNOWNED_ASSOCIATION_TRANSIEN import static org.hibernate.cfg.QuerySettings.DEFAULT_NULL_ORDERING; import static org.hibernate.cfg.QuerySettings.JSON_FUNCTIONS_ENABLED; import static org.hibernate.cfg.QuerySettings.PORTABLE_INTEGER_DIVISION; +import static org.hibernate.cfg.QuerySettings.XML_FUNCTIONS_ENABLED; import static org.hibernate.engine.config.spi.StandardConverters.BOOLEAN; import static org.hibernate.internal.CoreLogging.messageLogger; import static org.hibernate.internal.log.DeprecationLogger.DEPRECATION_LOGGER; @@ -276,6 +277,7 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions { private final boolean portableIntegerDivisionEnabled; private final boolean jsonFunctionsEnabled; + private final boolean xmlFunctionsEnabled; private final int queryStatisticsMaxSize; @@ -614,6 +616,10 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions { JSON_FUNCTIONS_ENABLED, configurationSettings ); + this.xmlFunctionsEnabled = getBoolean( + XML_FUNCTIONS_ENABLED, + configurationSettings + ); this.queryStatisticsMaxSize = getInt( QUERY_STATISTICS_MAX_SIZE, @@ -1244,6 +1250,11 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions { return jsonFunctionsEnabled; } + @Override + public boolean isXmlFunctionsEnabled() { + return xmlFunctionsEnabled; + } + @Override public boolean isPortableIntegerDivisionEnabled() { return portableIntegerDivisionEnabled; diff --git a/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java b/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java index 47e1bce732..85461af84d 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java @@ -433,6 +433,11 @@ public class AbstractDelegatingSessionFactoryOptions implements SessionFactoryOp return delegate.isJsonFunctionsEnabled(); } + @Override + public boolean isXmlFunctionsEnabled() { + return delegate.isXmlFunctionsEnabled(); + } + @Override public boolean isPortableIntegerDivisionEnabled() { return delegate.isPortableIntegerDivisionEnabled(); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java b/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java index 7967c85b1a..2939955db7 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java @@ -285,6 +285,14 @@ public interface SessionFactoryOptions extends QueryEngineOptions { return false; } + /** + * @see org.hibernate.cfg.AvailableSettings#XML_FUNCTIONS_ENABLED + */ + @Override + default boolean isXmlFunctionsEnabled() { + return false; + } + /** * @see org.hibernate.cfg.AvailableSettings#PORTABLE_INTEGER_DIVISION */ diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java b/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java index e135523cf5..71e529c80e 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java @@ -24,6 +24,16 @@ public interface QuerySettings { */ @Incubating String JSON_FUNCTIONS_ENABLED = "hibernate.query.hql.json_functions_enabled"; + + /** + * Boolean setting to control if the use of tech preview XML functions in HQL is enabled. + * By default, this is {@code false} i.e. disabled since the functions are still incubating. + * + * @since 7.0 + */ + @Incubating + String XML_FUNCTIONS_ENABLED = "hibernate.query.hql.xml_functions_enabled"; + /** * Specifies that division of two integers should produce an integer on all * databases. By default, integer division in HQL can produce a non-integer 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 023a550fbe..beadca37d0 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -425,6 +425,8 @@ public class DB2Dialect extends Dialect { functionFactory.jsonArrayAgg_db2(); functionFactory.jsonObjectAgg_db2(); } + + functionFactory.xmlelement(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index d8465a0dc2..ee2ce1db82 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -352,6 +352,8 @@ public class H2Dialect extends Dialect { functionFactory.jsonArrayAgg_h2(); functionFactory.jsonObjectAgg_h2(); } + + functionFactory.xmlelement_h2(); } /** 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 5574e03d45..fa111e3d54 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -415,6 +415,8 @@ public class OracleDialect extends Dialect { functionFactory.jsonMergepatch_oracle(); functionFactory.jsonArrayAppend_oracle(); functionFactory.jsonArrayInsert_oracle(); + + functionFactory.xmlelement(); } @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 e7900ad373..fd7b9a1c36 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -630,6 +630,8 @@ public class PostgreSQLDialect extends Dialect { functionFactory.jsonArrayAppend_postgresql( getVersion().isSameOrAfter( 13 ) ); functionFactory.jsonArrayInsert_postgresql(); + functionFactory.xmlelement(); + functionFactory.makeDateTimeTimestamp(); // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions functionFactory.inverseDistributionOrderedSetAggregates(); 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 a18eef1473..155b8f120e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -431,6 +431,7 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { functionFactory.jsonArrayAppend_sqlserver( getVersion().isSameOrAfter( 16 ) ); functionFactory.jsonArrayInsert_sqlserver(); } + functionFactory.xmlelement_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 a2d1114e30..ad3ea3a4f7 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 @@ -153,6 +153,9 @@ import org.hibernate.dialect.function.json.SQLServerJsonRemoveFunction; import org.hibernate.dialect.function.json.SQLServerJsonReplaceFunction; import org.hibernate.dialect.function.json.SQLServerJsonSetFunction; import org.hibernate.dialect.function.json.SQLServerJsonValueFunction; +import org.hibernate.dialect.function.xml.H2XmlElementFunction; +import org.hibernate.dialect.function.xml.SQLServerXmlElementFunction; +import org.hibernate.dialect.function.xml.XmlElementFunction; import org.hibernate.query.sqm.function.SqmFunctionRegistry; import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; import org.hibernate.query.sqm.produce.function.FunctionParameterType; @@ -4097,4 +4100,25 @@ public class CommonFunctionFactory { public void jsonArrayInsert_sqlserver() { functionRegistry.register( "json_array_insert", new SQLServerJsonArrayInsertFunction( typeConfiguration ) ); } + + /** + * Standard xmlelement() function + */ + public void xmlelement() { + functionRegistry.register( "xmlelement", new XmlElementFunction( typeConfiguration ) ); + } + + /** + * H2 xmlelement() function + */ + public void xmlelement_h2() { + functionRegistry.register( "xmlelement", new H2XmlElementFunction( typeConfiguration ) ); + } + + /** + * SQL Server xmlelement() function + */ + public void xmlelement_sqlserver() { + functionRegistry.register( "xmlelement", new SQLServerXmlElementFunction( typeConfiguration ) ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/H2XmlElementFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/H2XmlElementFunction.java new file mode 100644 index 0000000000..a293a0a7c5 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/H2XmlElementFunction.java @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.Map; + +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * H2 xmlelement function. + */ +public class H2XmlElementFunction extends XmlElementFunction { + + public H2XmlElementFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + XmlElementArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( "xmlnode(" ); + sqlAppender.appendSingleQuoteEscapedString( arguments.elementName() ); + if ( arguments.attributes() != null ) { + String separator = ","; + for ( Map.Entry entry : arguments.attributes().getAttributes().entrySet() ) { + sqlAppender.appendSql( separator ); + sqlAppender.appendSql( "xmlattr(" ); + sqlAppender.appendSingleQuoteEscapedString( entry.getKey() ); + sqlAppender.appendSql( ',' ); + entry.getValue().accept( walker ); + sqlAppender.appendSql( ')' ); + separator = "||"; + } + } + else { + sqlAppender.appendSql( ",null" ); + } + if ( !arguments.content().isEmpty() ) { + String separator = ","; + for ( Expression expression : arguments.content() ) { + sqlAppender.appendSql( separator ); + expression.accept( walker ); + separator = "||"; + } + } + else { + sqlAppender.appendSql( ",null" ); + } + sqlAppender.appendSql( ",false)" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlElementFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlElementFunction.java new file mode 100644 index 0000000000..1d5389d5a0 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlElementFunction.java @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.Map; + +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server xmlelement function. + */ +public class SQLServerXmlElementFunction extends XmlElementFunction { + + public SQLServerXmlElementFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + XmlElementArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( "(select 1 tag,null parent" ); + final String aliasPrefix = " [" + arguments.elementName() + "!1"; + if ( arguments.attributes() != null ) { + for ( Map.Entry entry : arguments.attributes().getAttributes().entrySet() ) { + sqlAppender.appendSql( ',' ); + entry.getValue().accept( walker ); + sqlAppender.appendSql( aliasPrefix ); + sqlAppender.appendSql( '!' ); + sqlAppender.appendSql( entry.getKey() ); + sqlAppender.appendSql( ']' ); + } + } + else if ( arguments.content().isEmpty() ) { + sqlAppender.appendSql( ",null" ); + sqlAppender.appendSql( aliasPrefix ); + sqlAppender.appendSql( ']' ); + } + if ( !arguments.content().isEmpty() ) { + for ( Expression expression : arguments.content() ) { + sqlAppender.appendSql( ',' ); + expression.accept( walker ); + sqlAppender.appendSql( aliasPrefix ); + sqlAppender.appendSql( ']' ); + } + } + sqlAppender.appendSql( " for xml explicit, type)" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlElementFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlElementFunction.java new file mode 100644 index 0000000000..631b85be5e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlElementFunction.java @@ -0,0 +1,193 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.List; +import java.util.Map; + +import org.hibernate.query.ReturnableType; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.function.SelfRenderingSqmFunction; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.ArgumentsValidator; +import org.hibernate.query.sqm.produce.function.FunctionArgumentException; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.expression.SqmExpression; +import org.hibernate.query.sqm.tree.expression.SqmLiteral; +import org.hibernate.query.sqm.tree.expression.SqmXmlAttributesExpression; +import org.hibernate.query.sqm.tree.expression.SqmXmlElementExpression; +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.Expression; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.sql.ast.tree.expression.XmlAttributes; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import static java.lang.Character.isLetter; +import static java.lang.Character.isLetterOrDigit; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING; + +/** + * Standard xmlelement function. + */ +public class XmlElementFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public XmlElementFunction(TypeConfiguration typeConfiguration) { + super( + "xmlelement", + FunctionKind.NORMAL, + StandardArgumentsValidators.composite( + new ArgumentTypesValidator( StandardArgumentsValidators.min( 1 ), STRING ), + new ArgumentsValidator() { + @Override + public void validate( + List> arguments, + String functionName, + TypeConfiguration typeConfiguration) { + //noinspection unchecked + final String elementName = ( (SqmLiteral) arguments.get( 0 ) ).getLiteralValue(); + if ( !isValidXmlName( elementName ) ) { + throw new FunctionArgumentException( + String.format( + "Invalid XML element name passed to 'xmlelement()': %s", + 1, + elementName + ) + ); + } + if ( arguments.size() > 1 + && arguments.get( 1 ) instanceof SqmXmlAttributesExpression attributesExpression ) { + final Map> attributes = attributesExpression.getAttributes(); + for ( Map.Entry> entry : attributes.entrySet() ) { + if ( !isValidXmlName( entry.getKey() ) ) { + throw new FunctionArgumentException( + String.format( + "Invalid XML attribute name passed to 'xmlattributes()': %s", + 1, + entry.getKey() + ) + ); + } + } + } + } + + private static boolean isValidXmlName(String name) { + if ( name.isEmpty() + || !isValidXmlNameStart( name.charAt( 0 ) ) + || name.regionMatches( true, 0, "xml", 0, 3 ) ) { + return false; + } + for ( int i = 1; i < name.length(); i++ ) { + if ( !isValidXmlNameChar( name.charAt( i ) ) ) { + return false; + } + } + return true; + } + + private static boolean isValidXmlNameStart(char c) { + return isLetter( c ) || c == '_' || c == ':'; + } + + private static boolean isValidXmlNameChar(char c) { + return isLetterOrDigit( c ) || c == '_' || c == ':' || c == '-' || c == '.'; + } + + } + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.SQLXML ) + ), + null + ); + } + + @Override + protected SelfRenderingSqmFunction generateSqmFunctionExpression( + List> arguments, + ReturnableType impliedResultType, + QueryEngine queryEngine) { + //noinspection unchecked + return (SelfRenderingSqmFunction) new SqmXmlElementExpression( + this, + this, + arguments, + (ReturnableType) impliedResultType, + getArgumentsValidator(), + getReturnTypeResolver(), + queryEngine.getCriteriaBuilder(), + getName() + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + render( sqlAppender, XmlElementArguments.extract( sqlAstArguments ), returnType, walker ); + } + + protected void render( + SqlAppender sqlAppender, + XmlElementArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( "xmlelement(name " ); + sqlAppender.appendDoubleQuoteEscapedString( arguments.elementName() ); + if ( arguments.attributes() != null ) { + sqlAppender.appendSql( ",xmlattributes" ); + char separator = '('; + for ( Map.Entry entry : arguments.attributes().getAttributes().entrySet() ) { + sqlAppender.appendSql( separator ); + entry.getValue().accept( walker ); + sqlAppender.appendSql( " as " ); + sqlAppender.appendDoubleQuoteEscapedString( entry.getKey() ); + separator = ','; + } + sqlAppender.appendSql( ')' ); + } + if ( !arguments.content().isEmpty() ) { + for ( Expression expression : arguments.content() ) { + sqlAppender.appendSql( ',' ); + expression.accept( walker ); + } + } + sqlAppender.appendSql( ')' ); + } + + protected record XmlElementArguments( + String elementName, + @Nullable XmlAttributes attributes, + List content) { + static XmlElementArguments extract(List arguments) { + final Literal elementName = (Literal) arguments.get( 0 ); + final XmlAttributes attributes; + final List content; + + int index = 1; + if ( arguments.size() > index && arguments.get( index ) instanceof XmlAttributes ) { + attributes = (XmlAttributes) arguments.get( index ); + index++; + } + else { + attributes = null; + } + //noinspection unchecked + content = (List) arguments.subList( index, arguments.size() ); + return new XmlElementArguments( (String) elementName.getLiteralValue(), attributes, content ); + } + } +} 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 73d78e8a77..dc64d90765 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 @@ -4047,6 +4047,14 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { @Incubating JpaExpression jsonMergepatch(String document, Expression patch); + /** + * Creates an XML element with the given element name. + * + * @since 7.0 + */ + @Incubating + JpaXmlElementExpression xmlelement(String elementName); + @Override JpaPredicate and(List restrictions); diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaXmlElementExpression.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaXmlElementExpression.java new file mode 100644 index 0000000000..c44c68fe7c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaXmlElementExpression.java @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.criteria; + +import java.util.List; + +import org.hibernate.Incubating; + +import jakarta.persistence.criteria.Expression; + +/** + * A special expression for the {@code xmlelement} function. + * @since 7.0 + */ +@Incubating +public interface JpaXmlElementExpression extends JpaExpression { + + /** + * Passes the given {@link Expression} as value for the XML attribute with the given name. + * + * @return {@code this} for method chaining + */ + JpaXmlElementExpression attribute(String attributeName, Expression expression); + + /** + * Passes the given {@link Expression}s as value for the XML content of this element. + * + * @return {@code this} for method chaining + */ + JpaXmlElementExpression content(List> expressions); + + /** + * Passes the given {@link Expression}s as value for the XML content of this element. + * + * @return {@code this} for method chaining + */ + JpaXmlElementExpression content(Expression... expressions); +} 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 d025c6bacb..ceb1503abc 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 @@ -57,6 +57,7 @@ import org.hibernate.query.criteria.JpaSubQuery; import org.hibernate.query.criteria.JpaValues; import org.hibernate.query.criteria.JpaWindow; import org.hibernate.query.criteria.JpaWindowFrame; +import org.hibernate.query.criteria.JpaXmlElementExpression; import org.hibernate.query.sqm.TemporalUnit; import jakarta.persistence.Tuple; @@ -3631,4 +3632,10 @@ public class HibernateCriteriaBuilderDelegate implements HibernateCriteriaBuilde public JpaExpression jsonMergepatch(String document, Expression patch) { return criteriaBuilder.jsonMergepatch( document, patch ); } + + @Override + @Incubating + public JpaXmlElementExpression xmlelement(String elementName) { + return criteriaBuilder.xmlelement( elementName ); + } } 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 e87e69c81f..8a547b076e 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 @@ -165,6 +165,7 @@ import org.hibernate.query.sqm.tree.expression.SqmToDuration; import org.hibernate.query.sqm.tree.expression.SqmTrimSpecification; import org.hibernate.query.sqm.tree.expression.SqmTuple; import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation; +import org.hibernate.query.sqm.tree.expression.SqmXmlElementExpression; import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; import org.hibernate.query.sqm.tree.from.SqmCrossJoin; import org.hibernate.query.sqm.tree.from.SqmCteJoin; @@ -229,6 +230,7 @@ import org.hibernate.type.spi.TypeConfiguration; import org.jboss.logging.Logger; +import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.metamodel.Bindable; import jakarta.persistence.metamodel.SingularAttribute; @@ -2982,6 +2984,36 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem } } + @Override + public SqmExpression visitXmlelementFunction(HqlParser.XmlelementFunctionContext ctx) { + checkXmlFunctionsEnabled( ctx ); + final String elementName = visitIdentifier( ctx.identifier() ); + final SqmXmlElementExpression xmlelement = creationContext.getNodeBuilder().xmlelement( elementName ); + final HqlParser.XmlattributesFunctionContext attributeCtx = ctx.xmlattributesFunction(); + if ( attributeCtx != null ) { + final List expressions = attributeCtx.expressionOrPredicate(); + final List attributeNames = attributeCtx.identifier(); + for ( int i = 0; i < expressions.size(); i++ ) { + xmlelement.attribute( + visitIdentifier( attributeNames.get( i ) ), + (Expression) expressions.get( i ).accept( this ) + ); + } + } + xmlelement.content( visitExpressions( ctx ) ); + return xmlelement; + } + + private void checkXmlFunctionsEnabled(ParserRuleContext ctx) { + if ( !creationOptions.isXmlFunctionsEnabled() ) { + throw new SemanticException( + "Can't use function '" + ctx.children.get( 0 ).getText() + + "', because tech preview XML functions are not enabled. To enable, set the '" + QuerySettings.XML_FUNCTIONS_ENABLED + "' setting to 'true'.", + query + ); + } + } + @Override public SqmPredicate visitIncludesPredicate(HqlParser.IncludesPredicateContext ctx) { final boolean negated = ctx.NOT() != null; diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SqmCreationOptions.java b/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SqmCreationOptions.java index 26b14ee3a6..dee06842ec 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SqmCreationOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SqmCreationOptions.java @@ -31,6 +31,13 @@ public interface SqmCreationOptions { return false; } + /** + * @see org.hibernate.cfg.AvailableSettings#XML_FUNCTIONS_ENABLED + */ + default boolean isXmlFunctionsEnabled() { + return false; + } + /** * @see org.hibernate.cfg.AvailableSettings#PORTABLE_INTEGER_DIVISION */ diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/QueryEngineOptions.java b/hibernate-core/src/main/java/org/hibernate/query/spi/QueryEngineOptions.java index 11fd627652..75a5ebd807 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/QueryEngineOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/QueryEngineOptions.java @@ -81,6 +81,11 @@ public interface QueryEngineOptions { */ boolean isJsonFunctionsEnabled(); + /** + * @see org.hibernate.cfg.AvailableSettings#XML_FUNCTIONS_ENABLED + */ + boolean isXmlFunctionsEnabled(); + /** * @see org.hibernate.cfg.AvailableSettings#PORTABLE_INTEGER_DIVISION */ 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 e63a93aabe..e85a6219a5 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 @@ -47,6 +47,7 @@ import org.hibernate.query.sqm.tree.expression.SqmJsonQueryExpression; import org.hibernate.query.sqm.tree.expression.SqmJsonValueExpression; import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression; import org.hibernate.query.sqm.tree.expression.SqmTuple; +import org.hibernate.query.sqm.tree.expression.SqmXmlElementExpression; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement; @@ -749,6 +750,9 @@ public interface NodeBuilder extends HibernateCriteriaBuilder, BindingContext { @Override SqmExpression jsonMergepatch(Expression document, Expression patch); + @Override + SqmXmlElementExpression xmlelement(String elementName); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Covariant overrides diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCreationOptionsStandard.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCreationOptionsStandard.java index b2c93af769..461fc23b1f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCreationOptionsStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCreationOptionsStandard.java @@ -27,6 +27,11 @@ public class SqmCreationOptionsStandard implements SqmCreationOptions { return queryEngineOptions.isJsonFunctionsEnabled(); } + @Override + public boolean isXmlFunctionsEnabled() { + return queryEngineOptions.isXmlFunctionsEnabled(); + } + @Override public boolean isPortableIntegerDivisionEnabled() { return queryEngineOptions.isPortableIntegerDivisionEnabled(); 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 4f9857b4dd..4b58cc1490 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 @@ -136,6 +136,7 @@ import org.hibernate.query.sqm.tree.expression.SqmTuple; import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation; import org.hibernate.query.sqm.tree.expression.SqmWindow; import org.hibernate.query.sqm.tree.expression.SqmWindowFrame; +import org.hibernate.query.sqm.tree.expression.SqmXmlElementExpression; import org.hibernate.query.sqm.tree.expression.ValueBindJpaCriteriaParameter; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; @@ -218,6 +219,7 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable { private transient BasicType integerType; private transient BasicType longType; private transient BasicType characterType; + private transient BasicType stringType; private transient FunctionReturnTypeResolver sumReturnTypeResolver; private transient FunctionReturnTypeResolver avgReturnTypeResolver; private final transient Map, HibernateCriteriaBuilder> extensions; @@ -311,6 +313,16 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable { return characterType; } + public BasicType getStringType() { + final BasicType stringType = this.stringType; + if ( stringType == null ) { + return this.stringType = + getTypeConfiguration().getBasicTypeRegistry() + .resolve( StandardBasicTypes.STRING ); + } + return stringType; + } + public FunctionReturnTypeResolver getSumReturnTypeResolver() { final FunctionReturnTypeResolver resolver = sumReturnTypeResolver; if ( resolver == null ) { @@ -5664,4 +5676,15 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable { queryEngine ); } + + @Override + public SqmXmlElementExpression xmlelement(String elementName) { + final List> arguments = new ArrayList<>( 3 ); + arguments.add( new SqmLiteral<>( elementName, getStringType(), this ) ); + return (SqmXmlElementExpression) getFunctionDescriptor( "xmlelement" ).generateSqmExpression( + arguments, + null, + queryEngine + ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonNullBehavior.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonNullBehavior.java index ce9cbc7efc..4586583aaf 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonNullBehavior.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonNullBehavior.java @@ -35,7 +35,7 @@ public enum SqmJsonNullBehavior implements SqmTypedNode { @Override public NodeBuilder nodeBuilder() { - return null; + throw new UnsupportedOperationException(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonObjectAggUniqueKeysBehavior.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonObjectAggUniqueKeysBehavior.java index ef7bf81f24..65b3c829cc 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonObjectAggUniqueKeysBehavior.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonObjectAggUniqueKeysBehavior.java @@ -35,7 +35,7 @@ public enum SqmJsonObjectAggUniqueKeysBehavior implements SqmTypedNode { @Override public NodeBuilder nodeBuilder() { - return null; + throw new UnsupportedOperationException(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmXmlAttributesExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmXmlAttributesExpression.java new file mode 100644 index 0000000000..738070c85d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmXmlAttributesExpression.java @@ -0,0 +1,94 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.sqm.tree.expression; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.hibernate.Incubating; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.sql.ast.tree.expression.XmlAttributes; + +import jakarta.persistence.criteria.Expression; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Special expression for the json_query function that also captures special syntax elements like error and empty behavior. + * + * @since 7.0 + */ +@Incubating +public class SqmXmlAttributesExpression implements SqmTypedNode { + + private final Map> attributes; + + public SqmXmlAttributesExpression(String attributeName, Expression expression) { + final Map> attributes = new LinkedHashMap<>(); + attributes.put( attributeName, (SqmExpression) expression ); + this.attributes = attributes; + } + + private SqmXmlAttributesExpression(Map> attributes) { + this.attributes = attributes; + } + + public void attribute(String attributeName, Expression expression) { + attributes.put( attributeName, (SqmExpression) expression ); + } + + public Map> getAttributes() { + return attributes; + } + + @Override + public @Nullable SqmExpressible getNodeType() { + return null; + } + + @Override + public NodeBuilder nodeBuilder() { + throw new UnsupportedOperationException(); + } + + @Override + public X accept(SemanticQueryWalker walker) { + final Map attributes = new LinkedHashMap<>(); + for ( Map.Entry> entry : this.attributes.entrySet() ) { + attributes.put( entry.getKey(), (org.hibernate.sql.ast.tree.expression.Expression) entry.getValue().accept( walker ) ); + } + //noinspection unchecked + return (X) new XmlAttributes( attributes ); + } + + @Override + public SqmXmlAttributesExpression copy(SqmCopyContext context) { + final SqmXmlAttributesExpression existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final Map> attributes = new LinkedHashMap<>(); + for ( Map.Entry> entry : this.attributes.entrySet() ) { + attributes.put( entry.getKey(), entry.getValue().copy( context ) ); + } + return context.registerCopy( this, new SqmXmlAttributesExpression( attributes ) ); + } + + @Override + public void appendHqlString(StringBuilder sb) { + String separator = "xmlattributes("; + for ( Map.Entry> entry : attributes.entrySet() ) { + sb.append( separator ); + entry.getValue().appendHqlString( sb ); + sb.append( " as " ); + sb.append( entry.getKey() ); + separator = ", "; + } + sb.append( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmXmlElementExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmXmlElementExpression.java new file mode 100644 index 0000000000..1baf9bdb9f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmXmlElementExpression.java @@ -0,0 +1,128 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.sqm.tree.expression; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.hibernate.Incubating; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.criteria.JpaXmlElementExpression; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.function.FunctionRenderer; +import org.hibernate.query.sqm.function.SelfRenderingSqmFunction; +import org.hibernate.query.sqm.function.SqmFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.ArgumentsValidator; +import org.hibernate.query.sqm.produce.function.FunctionReturnTypeResolver; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmTypedNode; + +import jakarta.persistence.criteria.Expression; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Special expression for the xmlelement function that also captures special syntax elements like xmlattributes. + * + * @since 7.0 + */ +@Incubating +public class SqmXmlElementExpression extends SelfRenderingSqmFunction implements JpaXmlElementExpression { + + public SqmXmlElementExpression( + SqmFunctionDescriptor descriptor, + FunctionRenderer renderer, + List> arguments, + @Nullable ReturnableType impliedResultType, + @Nullable ArgumentsValidator argumentsValidator, + FunctionReturnTypeResolver returnTypeResolver, + NodeBuilder nodeBuilder, + String name) { + super( + descriptor, + renderer, + arguments, + impliedResultType, + argumentsValidator, + returnTypeResolver, + nodeBuilder, + name + ); + } + + @Override + public SqmXmlElementExpression attribute(String attributeName, Expression expression) { + //noinspection unchecked + final List> arguments = (List>) getArguments(); + if ( arguments.size() > 1 && arguments.get( 1 ) instanceof SqmXmlAttributesExpression attributesExpression ) { + attributesExpression.attribute( attributeName, expression ); + } + else { + arguments.add( 1, new SqmXmlAttributesExpression( attributeName, expression ) ); + } + return this; + } + + @Override + public SqmXmlElementExpression content(Expression... expressions) { + return content( Arrays.asList(expressions) ); + } + + @Override + public SqmXmlElementExpression content(List> expressions) { + //noinspection unchecked + final List> arguments = (List>) getArguments(); + int contentIndex = 1; + if ( arguments.size() > contentIndex ) { + if ( arguments.get( contentIndex ) instanceof SqmXmlAttributesExpression ) { + contentIndex++; + } + while ( contentIndex < arguments.size() ) { + arguments.remove( arguments.size() - 1 ); + } + } + for ( Expression expression : expressions ) { + arguments.add( (SqmTypedNode) expression ); + } + return this; + } + + @Override + public SqmXmlElementExpression copy(SqmCopyContext context) { + final SqmXmlElementExpression existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final List> arguments = new ArrayList<>( getArguments().size() ); + for ( SqmTypedNode argument : getArguments() ) { + arguments.add( argument.copy( context ) ); + } + return context.registerCopy( + this, + new SqmXmlElementExpression( + getFunctionDescriptor(), + getFunctionRenderer(), + arguments, + getImpliedResultType(), + getArgumentsValidator(), + getReturnTypeResolver(), + nodeBuilder(), + getFunctionName() + ) + ); + } + + @Override + public void appendHqlString(StringBuilder sb) { + final List> arguments = getArguments(); + sb.append( "xmlelement(name " ); + arguments.get( 0 ).appendHqlString( sb ); + for ( int i = 1; i < arguments.size(); i++ ) { + sb.append( ',' ); + arguments.get( i ).appendHqlString( sb ); + } + sb.append( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlAttributes.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlAttributes.java new file mode 100644 index 0000000000..2df5bb775e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlAttributes.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.ast.tree.expression; + +import java.util.Map; + +import org.hibernate.sql.ast.SqlAstWalker; +import org.hibernate.sql.ast.tree.SqlAstNode; + +/** + * @since 7.0 + */ +public class XmlAttributes implements SqlAstNode { + + private final Map attributes; + + public XmlAttributes(Map attributes) { + this.attributes = attributes; + } + + public Map getAttributes() { + return attributes; + } + + @Override + public void accept(SqlAstWalker sqlTreeWalker) { + throw new UnsupportedOperationException("XmlAttributes doesn't support walking"); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlElementTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlElementTest.java new file mode 100644 index 0000000000..cd2e669d0b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlElementTest.java @@ -0,0 +1,45 @@ +/* + * 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.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 +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.XML_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsXmlelement.class) +public class XmlElementTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-xmlelement-example[] + em.createQuery( "select xmlelement(name myelement)" ).getResultList(); + //end::hql-xmlelement-example[] + } ); + } + + @Test + public void testAttributesAndContent(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-xmlelement-attributes-content-example[] + em.createQuery("select xmlelement(name `my-element`, xmlattributes(123 as attr1, '456' as `attr-2`), 'myContent', xmlelement(name empty))" ).getResultList(); + //end::hql-xmlelement-attributes-content-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 new file mode 100644 index 0000000000..f89b4ef72c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/XmlFunctionTests.java @@ -0,0 +1,208 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.hql; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.cfg.QuerySettings; +import org.hibernate.type.SqlTypes; + +import org.hibernate.testing.orm.domain.gambit.EntityOfBasics; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +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.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Tuple; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DomainModel( annotatedClasses = { + XmlFunctionTests.XmlHolder.class, + EntityOfBasics.class +}) +@ServiceRegistry(settings = @Setting(name = QuerySettings.XML_FUNCTIONS_ENABLED, value = "true")) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-18497") +public class XmlFunctionTests { + + XmlHolder entity; + + @BeforeEach + public void prepareData(SessionFactoryScope scope) { + scope.inTransaction( + em -> { + entity = new XmlHolder(); + entity.id = 1L; + entity.xml = new HashMap<>(); + entity.xml.put( "theInt", 1 ); + entity.xml.put( "theFloat", 0.1 ); + entity.xml.put( "theString", "abc" ); + entity.xml.put( "theBoolean", true ); + entity.xml.put( "theNull", null ); + entity.xml.put( "theArray", new String[] { "a", "b", "c" } ); + entity.xml.put( "theObject", new HashMap<>( entity.xml ) ); + entity.xml.put( + "theNestedObjects", + List.of( + Map.of( "id", 1, "name", "val1" ), + Map.of( "id", 2, "name", "val2" ), + Map.of( "id", 3, "name", "val3" ) + ) + ); + em.persist(entity); + + EntityOfBasics e1 = new EntityOfBasics(); + e1.setId( 1 ); + e1.setTheString( "Dog" ); + e1.setTheInteger( 0 ); + e1.setTheUuid( UUID.randomUUID() ); + EntityOfBasics e2 = new EntityOfBasics(); + e2.setId( 2 ); + e2.setTheString( "Cat" ); + e2.setTheInteger( 0 ); + + em.persist( e1 ); + em.persist( e2 ); + } + ); + } + + @AfterEach + public void cleanupData(SessionFactoryScope scope) { + scope.inTransaction( + em -> { + em.createMutationQuery( "delete from EntityOfBasics" ).executeUpdate(); + em.createMutationQuery( "delete from XmlHolder" ).executeUpdate(); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsXmlelement.class) + public void testXmlelement(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Tuple tuple = session.createQuery( + "select " + + "xmlelement(name empty), " + + "xmlelement(name `the-element`), " + + "xmlelement(name myElement, 'myContent'), " + + "xmlelement(name myElement, xmlattributes('123' as attr1)), " + + "xmlelement(name myElement, xmlattributes('123' as attr1, '456' as `attr-2`)), " + + "xmlelement(name myElement, xmlattributes('123' as attr1), 'myContent', xmlelement(name empty))", + Tuple.class + ).getSingleResult(); + assertXmlEquals( "", tuple.get( 0, String.class ) ); + assertXmlEquals( "", tuple.get( 1 , String.class ) ); + assertXmlEquals( "myContent", tuple.get( 2, String.class ) ); + assertXmlEquals( "", tuple.get( 3, String.class ) ); + assertXmlEquals( "", tuple.get( 4, String.class ) ); + assertXmlEquals( "myContent", tuple.get( 5, String.class ) ); + } + ); + } + + private void assertXmlEquals(String doc1, String doc2) { + final Document d1 = parseXml( xmlNormalize( doc1 ) ); + final Document d2 = parseXml( xmlNormalize( doc2 ) ); + normalize( d1 ); + normalize( d2 ); + assertEquals( toXml( d1 ), toXml( d2 ) ); + } + + private void normalize(Document document) { + normalize( document.getDocumentElement() ); + } + + private void normalize(Element element) { + final NodeList childNodes = element.getChildNodes(); + for ( int i = 0; i < childNodes.getLength(); i++ ) { + final Node childNode = childNodes.item( i ); + if ( childNode.getNodeType() == Node.ELEMENT_NODE ) { + normalize( (Element) childNode ); + } + else if ( childNode.getNodeType() == Node.TEXT_NODE ) { + if ( childNode.getNodeValue().isBlank() ) { + childNode.getParentNode().removeChild( childNode ); + } + else { + childNode.setNodeValue( childNode.getNodeValue().trim() ); + } + } + } + } + + private String xmlNormalize(String doc) { + final String prefix = ""; + return doc.startsWith( " xml; + } +} 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 47992a9e28..77f249928e 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 @@ -838,6 +838,12 @@ abstract public class DialectFeatureChecks { } } + public static class SupportsXmlelement implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "xmlelement" ); + } + } + public static class IsJtds implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return dialect instanceof SybaseDialect && ( (SybaseDialect) dialect ).getDriverKind() == SybaseDriverKind.JTDS;