HHH-18497 Add xmlelement function

This commit is contained in:
Christian Beikov 2024-09-20 19:35:22 +02:00
parent 7ff0567383
commit 4baba673cb
39 changed files with 1113 additions and 2 deletions

View File

@ -7,6 +7,7 @@
:example-dir-hql: {core-project-dir}/src/test/java/org/hibernate/orm/test/hql :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 :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 :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 :extrasdir: extras
This chapter describes Hibernate Query Language (HQL) and Jakarta Persistence Query Language (JPQL). 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. 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]] [[hql-user-defined-functions]]
==== Native and user-defined functions ==== Native and user-defined functions

View File

@ -0,0 +1,5 @@
"xmlelement(name " identifier xmlattributes? ("," expressionOrPredicate)* ")"
xmlattributes
: "xmlattributes(" expressionOrPredicate " as " identifier ("," expressionOrPredicate " as " identifier)* ")"
;

View File

@ -440,6 +440,8 @@ public class DB2LegacyDialect extends Dialect {
functionFactory.jsonObjectAgg_db2(); functionFactory.jsonObjectAgg_db2();
} }
} }
functionFactory.xmlelement();
} }
@Override @Override

View File

@ -417,6 +417,8 @@ public class H2LegacyDialect extends Dialect {
// Use group_concat until 2.x as listagg was buggy // Use group_concat until 2.x as listagg was buggy
functionFactory.listagg_groupConcat(); functionFactory.listagg_groupConcat();
} }
functionFactory.xmlelement_h2();
} }
else { else {
functionFactory.listagg_groupConcat(); functionFactory.listagg_groupConcat();

View File

@ -325,6 +325,8 @@ public class OracleLegacyDialect extends Dialect {
functionFactory.jsonArrayAppend_oracle(); functionFactory.jsonArrayAppend_oracle();
functionFactory.jsonArrayInsert_oracle(); functionFactory.jsonArrayInsert_oracle();
} }
functionFactory.xmlelement();
} }
@Override @Override

View File

@ -669,6 +669,8 @@ public class PostgreSQLLegacyDialect extends Dialect {
functionFactory.jsonArrayAppend_postgresql( getVersion().isSameOrAfter( 13 ) ); functionFactory.jsonArrayAppend_postgresql( getVersion().isSameOrAfter( 13 ) );
functionFactory.jsonArrayInsert_postgresql(); functionFactory.jsonArrayInsert_postgresql();
functionFactory.xmlelement();
if ( getVersion().isSameOrAfter( 9, 4 ) ) { if ( getVersion().isSameOrAfter( 9, 4 ) ) {
functionFactory.makeDateTimeTimestamp(); functionFactory.makeDateTimeTimestamp();
// Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions

View File

@ -413,6 +413,7 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect {
functionFactory.jsonArrayAppend_sqlserver( getVersion().isSameOrAfter( 16 ) ); functionFactory.jsonArrayAppend_sqlserver( getVersion().isSameOrAfter( 16 ) );
functionFactory.jsonArrayInsert_sqlserver(); functionFactory.jsonArrayInsert_sqlserver();
} }
functionFactory.xmlelement_sqlserver();
if ( getVersion().isSameOrAfter( 14 ) ) { if ( getVersion().isSameOrAfter( 14 ) ) {
functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); functionFactory.listagg_stringAggWithinGroup( "varchar(max)" );
functionFactory.jsonArrayAgg_sqlserver( getVersion().isSameOrAfter( 16 ) ); functionFactory.jsonArrayAgg_sqlserver( getVersion().isSameOrAfter( 16 ) );

View File

@ -258,6 +258,7 @@ MINELEMENT : [mM] [iI] [nN] [eE] [lL] [eE] [mM] [eE] [nN] [tT];
MININDEX : [mM] [iI] [nN] [iI] [nN] [dD] [eE] [xX]; MININDEX : [mM] [iI] [nN] [iI] [nN] [dD] [eE] [xX];
MINUTE : [mM] [iI] [nN] [uU] [tT] [eE]; MINUTE : [mM] [iI] [nN] [uU] [tT] [eE];
MONTH : [mM] [oO] [nN] [tT] [hH]; MONTH : [mM] [oO] [nN] [tT] [hH];
NAME : [nN] [aA] [mM] [eE];
NANOSECOND : [nN] [aA] [nN] [oO] [sS] [eE] [cC] [oO] [nN] [dD]; NANOSECOND : [nN] [aA] [nN] [oO] [sS] [eE] [cC] [oO] [nN] [dD];
NEW : [nN] [eE] [wW]; NEW : [nN] [eE] [wW];
NEXT : [nN] [eE] [xX] [tT]; NEXT : [nN] [eE] [xX] [tT];
@ -329,6 +330,8 @@ WITH : [wW] [iI] [tT] [hH];
WITHIN : [wW] [iI] [tT] [hH] [iI] [nN]; WITHIN : [wW] [iI] [tT] [hH] [iI] [nN];
WITHOUT : [wW] [iI] [tT] [hH] [oO] [uU] [tT]; WITHOUT : [wW] [iI] [tT] [hH] [oO] [uU] [tT];
WRAPPER : [wW] [rR] [aA] [pP] [pP] [eE] [rR]; 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]; YEAR : [yY] [eE] [aA] [rR];
ZONED : [zZ] [oO] [nN] [eE] [dD]; ZONED : [zZ] [oO] [nN] [eE] [dD];

View File

@ -1110,6 +1110,7 @@ function
| jpaNonstandardFunction | jpaNonstandardFunction
| columnFunction | columnFunction
| jsonFunction | jsonFunction
| xmlFunction
| genericFunction | genericFunction
; ;
@ -1716,6 +1717,24 @@ jsonUniqueKeysClause
: (WITH|WITHOUT) UNIQUE KEYS : (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 * Support for "soft" keywords which may be used as identifiers
* *
@ -1847,6 +1866,7 @@ jsonUniqueKeysClause
| MININDEX | MININDEX
| MINUTE | MINUTE
| MONTH | MONTH
| NAME
| NANOSECOND | NANOSECOND
| NATURALID | NATURALID
| NEW | NEW
@ -1921,6 +1941,8 @@ jsonUniqueKeysClause
| WITHIN | WITHIN
| WITHOUT | WITHOUT
| WRAPPER | WRAPPER
| XMLATTRIBUTES
| XMLELEMENT
| YEAR | YEAR
| ZONED) { | ZONED) {
logUseOfReservedWordAsIdentifier( getCurrentToken() ); logUseOfReservedWordAsIdentifier( getCurrentToken() );

View File

@ -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.DEFAULT_NULL_ORDERING;
import static org.hibernate.cfg.QuerySettings.JSON_FUNCTIONS_ENABLED; import static org.hibernate.cfg.QuerySettings.JSON_FUNCTIONS_ENABLED;
import static org.hibernate.cfg.QuerySettings.PORTABLE_INTEGER_DIVISION; 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.engine.config.spi.StandardConverters.BOOLEAN;
import static org.hibernate.internal.CoreLogging.messageLogger; import static org.hibernate.internal.CoreLogging.messageLogger;
import static org.hibernate.internal.log.DeprecationLogger.DEPRECATION_LOGGER; 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 portableIntegerDivisionEnabled;
private final boolean jsonFunctionsEnabled; private final boolean jsonFunctionsEnabled;
private final boolean xmlFunctionsEnabled;
private final int queryStatisticsMaxSize; private final int queryStatisticsMaxSize;
@ -614,6 +616,10 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
JSON_FUNCTIONS_ENABLED, JSON_FUNCTIONS_ENABLED,
configurationSettings configurationSettings
); );
this.xmlFunctionsEnabled = getBoolean(
XML_FUNCTIONS_ENABLED,
configurationSettings
);
this.queryStatisticsMaxSize = getInt( this.queryStatisticsMaxSize = getInt(
QUERY_STATISTICS_MAX_SIZE, QUERY_STATISTICS_MAX_SIZE,
@ -1244,6 +1250,11 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
return jsonFunctionsEnabled; return jsonFunctionsEnabled;
} }
@Override
public boolean isXmlFunctionsEnabled() {
return xmlFunctionsEnabled;
}
@Override @Override
public boolean isPortableIntegerDivisionEnabled() { public boolean isPortableIntegerDivisionEnabled() {
return portableIntegerDivisionEnabled; return portableIntegerDivisionEnabled;

View File

@ -433,6 +433,11 @@ public class AbstractDelegatingSessionFactoryOptions implements SessionFactoryOp
return delegate.isJsonFunctionsEnabled(); return delegate.isJsonFunctionsEnabled();
} }
@Override
public boolean isXmlFunctionsEnabled() {
return delegate.isXmlFunctionsEnabled();
}
@Override @Override
public boolean isPortableIntegerDivisionEnabled() { public boolean isPortableIntegerDivisionEnabled() {
return delegate.isPortableIntegerDivisionEnabled(); return delegate.isPortableIntegerDivisionEnabled();

View File

@ -285,6 +285,14 @@ public interface SessionFactoryOptions extends QueryEngineOptions {
return false; return false;
} }
/**
* @see org.hibernate.cfg.AvailableSettings#XML_FUNCTIONS_ENABLED
*/
@Override
default boolean isXmlFunctionsEnabled() {
return false;
}
/** /**
* @see org.hibernate.cfg.AvailableSettings#PORTABLE_INTEGER_DIVISION * @see org.hibernate.cfg.AvailableSettings#PORTABLE_INTEGER_DIVISION
*/ */

View File

@ -24,6 +24,16 @@ public interface QuerySettings {
*/ */
@Incubating @Incubating
String JSON_FUNCTIONS_ENABLED = "hibernate.query.hql.json_functions_enabled"; 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 * Specifies that division of two integers should produce an integer on all
* databases. By default, integer division in HQL can produce a non-integer * databases. By default, integer division in HQL can produce a non-integer

View File

@ -425,6 +425,8 @@ public class DB2Dialect extends Dialect {
functionFactory.jsonArrayAgg_db2(); functionFactory.jsonArrayAgg_db2();
functionFactory.jsonObjectAgg_db2(); functionFactory.jsonObjectAgg_db2();
} }
functionFactory.xmlelement();
} }
@Override @Override

View File

@ -352,6 +352,8 @@ public class H2Dialect extends Dialect {
functionFactory.jsonArrayAgg_h2(); functionFactory.jsonArrayAgg_h2();
functionFactory.jsonObjectAgg_h2(); functionFactory.jsonObjectAgg_h2();
} }
functionFactory.xmlelement_h2();
} }
/** /**

View File

@ -415,6 +415,8 @@ public class OracleDialect extends Dialect {
functionFactory.jsonMergepatch_oracle(); functionFactory.jsonMergepatch_oracle();
functionFactory.jsonArrayAppend_oracle(); functionFactory.jsonArrayAppend_oracle();
functionFactory.jsonArrayInsert_oracle(); functionFactory.jsonArrayInsert_oracle();
functionFactory.xmlelement();
} }
@Override @Override

View File

@ -630,6 +630,8 @@ public class PostgreSQLDialect extends Dialect {
functionFactory.jsonArrayAppend_postgresql( getVersion().isSameOrAfter( 13 ) ); functionFactory.jsonArrayAppend_postgresql( getVersion().isSameOrAfter( 13 ) );
functionFactory.jsonArrayInsert_postgresql(); functionFactory.jsonArrayInsert_postgresql();
functionFactory.xmlelement();
functionFactory.makeDateTimeTimestamp(); functionFactory.makeDateTimeTimestamp();
// Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions
functionFactory.inverseDistributionOrderedSetAggregates(); functionFactory.inverseDistributionOrderedSetAggregates();

View File

@ -431,6 +431,7 @@ public class SQLServerDialect extends AbstractTransactSQLDialect {
functionFactory.jsonArrayAppend_sqlserver( getVersion().isSameOrAfter( 16 ) ); functionFactory.jsonArrayAppend_sqlserver( getVersion().isSameOrAfter( 16 ) );
functionFactory.jsonArrayInsert_sqlserver(); functionFactory.jsonArrayInsert_sqlserver();
} }
functionFactory.xmlelement_sqlserver();
if ( getVersion().isSameOrAfter( 14 ) ) { if ( getVersion().isSameOrAfter( 14 ) ) {
functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); functionFactory.listagg_stringAggWithinGroup( "varchar(max)" );
functionFactory.jsonArrayAgg_sqlserver( getVersion().isSameOrAfter( 16 ) ); functionFactory.jsonArrayAgg_sqlserver( getVersion().isSameOrAfter( 16 ) );

View File

@ -153,6 +153,9 @@ import org.hibernate.dialect.function.json.SQLServerJsonRemoveFunction;
import org.hibernate.dialect.function.json.SQLServerJsonReplaceFunction; import org.hibernate.dialect.function.json.SQLServerJsonReplaceFunction;
import org.hibernate.dialect.function.json.SQLServerJsonSetFunction; import org.hibernate.dialect.function.json.SQLServerJsonSetFunction;
import org.hibernate.dialect.function.json.SQLServerJsonValueFunction; 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.function.SqmFunctionRegistry;
import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator;
import org.hibernate.query.sqm.produce.function.FunctionParameterType; import org.hibernate.query.sqm.produce.function.FunctionParameterType;
@ -4097,4 +4100,25 @@ public class CommonFunctionFactory {
public void jsonArrayInsert_sqlserver() { public void jsonArrayInsert_sqlserver() {
functionRegistry.register( "json_array_insert", new SQLServerJsonArrayInsertFunction( typeConfiguration ) ); 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 ) );
}
} }

View File

@ -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<String, Expression> 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)" );
}
}

View File

@ -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<String, Expression> 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)" );
}
}

View File

@ -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<? extends SqmTypedNode<?>> arguments,
String functionName,
TypeConfiguration typeConfiguration) {
//noinspection unchecked
final String elementName = ( (SqmLiteral<String>) 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<String, SqmExpression<?>> attributes = attributesExpression.getAttributes();
for ( Map.Entry<String, SqmExpression<?>> 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 <T> SelfRenderingSqmFunction<T> generateSqmFunctionExpression(
List<? extends SqmTypedNode<?>> arguments,
ReturnableType<T> impliedResultType,
QueryEngine queryEngine) {
//noinspection unchecked
return (SelfRenderingSqmFunction<T>) new SqmXmlElementExpression(
this,
this,
arguments,
(ReturnableType<String>) impliedResultType,
getArgumentsValidator(),
getReturnTypeResolver(),
queryEngine.getCriteriaBuilder(),
getName()
);
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> 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<String, Expression> 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<Expression> content) {
static XmlElementArguments extract(List<? extends SqlAstNode> arguments) {
final Literal elementName = (Literal) arguments.get( 0 );
final XmlAttributes attributes;
final List<Expression> 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<Expression>) arguments.subList( index, arguments.size() );
return new XmlElementArguments( (String) elementName.getLiteralValue(), attributes, content );
}
}
}

View File

@ -4047,6 +4047,14 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder {
@Incubating @Incubating
JpaExpression<String> jsonMergepatch(String document, Expression<?> patch); JpaExpression<String> jsonMergepatch(String document, Expression<?> patch);
/**
* Creates an XML element with the given element name.
*
* @since 7.0
*/
@Incubating
JpaXmlElementExpression xmlelement(String elementName);
@Override @Override
JpaPredicate and(List<Predicate> restrictions); JpaPredicate and(List<Predicate> restrictions);

View File

@ -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<String> {
/**
* 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<? extends Expression<?>> 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);
}

View File

@ -57,6 +57,7 @@ import org.hibernate.query.criteria.JpaSubQuery;
import org.hibernate.query.criteria.JpaValues; import org.hibernate.query.criteria.JpaValues;
import org.hibernate.query.criteria.JpaWindow; import org.hibernate.query.criteria.JpaWindow;
import org.hibernate.query.criteria.JpaWindowFrame; import org.hibernate.query.criteria.JpaWindowFrame;
import org.hibernate.query.criteria.JpaXmlElementExpression;
import org.hibernate.query.sqm.TemporalUnit; import org.hibernate.query.sqm.TemporalUnit;
import jakarta.persistence.Tuple; import jakarta.persistence.Tuple;
@ -3631,4 +3632,10 @@ public class HibernateCriteriaBuilderDelegate implements HibernateCriteriaBuilde
public JpaExpression<String> jsonMergepatch(String document, Expression<?> patch) { public JpaExpression<String> jsonMergepatch(String document, Expression<?> patch) {
return criteriaBuilder.jsonMergepatch( document, patch ); return criteriaBuilder.jsonMergepatch( document, patch );
} }
@Override
@Incubating
public JpaXmlElementExpression xmlelement(String elementName) {
return criteriaBuilder.xmlelement( elementName );
}
} }

View File

@ -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.SqmTrimSpecification;
import org.hibernate.query.sqm.tree.expression.SqmTuple; import org.hibernate.query.sqm.tree.expression.SqmTuple;
import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation; 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.SqmAttributeJoin;
import org.hibernate.query.sqm.tree.from.SqmCrossJoin; import org.hibernate.query.sqm.tree.from.SqmCrossJoin;
import org.hibernate.query.sqm.tree.from.SqmCteJoin; import org.hibernate.query.sqm.tree.from.SqmCteJoin;
@ -229,6 +230,7 @@ import org.hibernate.type.spi.TypeConfiguration;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.metamodel.Bindable; import jakarta.persistence.metamodel.Bindable;
import jakarta.persistence.metamodel.SingularAttribute; import jakarta.persistence.metamodel.SingularAttribute;
@ -2982,6 +2984,36 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> 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<HqlParser.ExpressionOrPredicateContext> expressions = attributeCtx.expressionOrPredicate();
final List<HqlParser.IdentifierContext> 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 @Override
public SqmPredicate visitIncludesPredicate(HqlParser.IncludesPredicateContext ctx) { public SqmPredicate visitIncludesPredicate(HqlParser.IncludesPredicateContext ctx) {
final boolean negated = ctx.NOT() != null; final boolean negated = ctx.NOT() != null;

View File

@ -31,6 +31,13 @@ public interface SqmCreationOptions {
return false; return false;
} }
/**
* @see org.hibernate.cfg.AvailableSettings#XML_FUNCTIONS_ENABLED
*/
default boolean isXmlFunctionsEnabled() {
return false;
}
/** /**
* @see org.hibernate.cfg.AvailableSettings#PORTABLE_INTEGER_DIVISION * @see org.hibernate.cfg.AvailableSettings#PORTABLE_INTEGER_DIVISION
*/ */

View File

@ -81,6 +81,11 @@ public interface QueryEngineOptions {
*/ */
boolean isJsonFunctionsEnabled(); boolean isJsonFunctionsEnabled();
/**
* @see org.hibernate.cfg.AvailableSettings#XML_FUNCTIONS_ENABLED
*/
boolean isXmlFunctionsEnabled();
/** /**
* @see org.hibernate.cfg.AvailableSettings#PORTABLE_INTEGER_DIVISION * @see org.hibernate.cfg.AvailableSettings#PORTABLE_INTEGER_DIVISION
*/ */

View File

@ -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.SqmJsonValueExpression;
import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression; import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression;
import org.hibernate.query.sqm.tree.expression.SqmTuple; 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.from.SqmRoot;
import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement;
import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement;
@ -749,6 +750,9 @@ public interface NodeBuilder extends HibernateCriteriaBuilder, BindingContext {
@Override @Override
SqmExpression<String> jsonMergepatch(Expression<?> document, Expression<?> patch); SqmExpression<String> jsonMergepatch(Expression<?> document, Expression<?> patch);
@Override
SqmXmlElementExpression xmlelement(String elementName);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Covariant overrides // Covariant overrides

View File

@ -27,6 +27,11 @@ public class SqmCreationOptionsStandard implements SqmCreationOptions {
return queryEngineOptions.isJsonFunctionsEnabled(); return queryEngineOptions.isJsonFunctionsEnabled();
} }
@Override
public boolean isXmlFunctionsEnabled() {
return queryEngineOptions.isXmlFunctionsEnabled();
}
@Override @Override
public boolean isPortableIntegerDivisionEnabled() { public boolean isPortableIntegerDivisionEnabled() {
return queryEngineOptions.isPortableIntegerDivisionEnabled(); return queryEngineOptions.isPortableIntegerDivisionEnabled();

View File

@ -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.SqmUnaryOperation;
import org.hibernate.query.sqm.tree.expression.SqmWindow; import org.hibernate.query.sqm.tree.expression.SqmWindow;
import org.hibernate.query.sqm.tree.expression.SqmWindowFrame; 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.expression.ValueBindJpaCriteriaParameter;
import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.from.SqmRoot;
import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement;
@ -218,6 +219,7 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable {
private transient BasicType<Integer> integerType; private transient BasicType<Integer> integerType;
private transient BasicType<Long> longType; private transient BasicType<Long> longType;
private transient BasicType<Character> characterType; private transient BasicType<Character> characterType;
private transient BasicType<String> stringType;
private transient FunctionReturnTypeResolver sumReturnTypeResolver; private transient FunctionReturnTypeResolver sumReturnTypeResolver;
private transient FunctionReturnTypeResolver avgReturnTypeResolver; private transient FunctionReturnTypeResolver avgReturnTypeResolver;
private final transient Map<Class<? extends HibernateCriteriaBuilder>, HibernateCriteriaBuilder> extensions; private final transient Map<Class<? extends HibernateCriteriaBuilder>, HibernateCriteriaBuilder> extensions;
@ -311,6 +313,16 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable {
return characterType; return characterType;
} }
public BasicType<String> getStringType() {
final BasicType<String> stringType = this.stringType;
if ( stringType == null ) {
return this.stringType =
getTypeConfiguration().getBasicTypeRegistry()
.resolve( StandardBasicTypes.STRING );
}
return stringType;
}
public FunctionReturnTypeResolver getSumReturnTypeResolver() { public FunctionReturnTypeResolver getSumReturnTypeResolver() {
final FunctionReturnTypeResolver resolver = sumReturnTypeResolver; final FunctionReturnTypeResolver resolver = sumReturnTypeResolver;
if ( resolver == null ) { if ( resolver == null ) {
@ -5664,4 +5676,15 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable {
queryEngine queryEngine
); );
} }
@Override
public SqmXmlElementExpression xmlelement(String elementName) {
final List<SqmTypedNode<?>> arguments = new ArrayList<>( 3 );
arguments.add( new SqmLiteral<>( elementName, getStringType(), this ) );
return (SqmXmlElementExpression) getFunctionDescriptor( "xmlelement" ).<String>generateSqmExpression(
arguments,
null,
queryEngine
);
}
} }

View File

@ -35,7 +35,7 @@ public enum SqmJsonNullBehavior implements SqmTypedNode<Object> {
@Override @Override
public NodeBuilder nodeBuilder() { public NodeBuilder nodeBuilder() {
return null; throw new UnsupportedOperationException();
} }
@Override @Override

View File

@ -35,7 +35,7 @@ public enum SqmJsonObjectAggUniqueKeysBehavior implements SqmTypedNode<Object> {
@Override @Override
public NodeBuilder nodeBuilder() { public NodeBuilder nodeBuilder() {
return null; throw new UnsupportedOperationException();
} }
@Override @Override

View File

@ -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<Object> {
private final Map<String, SqmExpression<?>> attributes;
public SqmXmlAttributesExpression(String attributeName, Expression<?> expression) {
final Map<String, SqmExpression<?>> attributes = new LinkedHashMap<>();
attributes.put( attributeName, (SqmExpression<?>) expression );
this.attributes = attributes;
}
private SqmXmlAttributesExpression(Map<String, SqmExpression<?>> attributes) {
this.attributes = attributes;
}
public void attribute(String attributeName, Expression<?> expression) {
attributes.put( attributeName, (SqmExpression<?>) expression );
}
public Map<String, SqmExpression<?>> getAttributes() {
return attributes;
}
@Override
public @Nullable SqmExpressible<Object> getNodeType() {
return null;
}
@Override
public NodeBuilder nodeBuilder() {
throw new UnsupportedOperationException();
}
@Override
public <X> X accept(SemanticQueryWalker<X> walker) {
final Map<String, org.hibernate.sql.ast.tree.expression.Expression> attributes = new LinkedHashMap<>();
for ( Map.Entry<String, SqmExpression<?>> 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<String, SqmExpression<?>> attributes = new LinkedHashMap<>();
for ( Map.Entry<String, SqmExpression<?>> 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<String, SqmExpression<?>> entry : attributes.entrySet() ) {
sb.append( separator );
entry.getValue().appendHqlString( sb );
sb.append( " as " );
sb.append( entry.getKey() );
separator = ", ";
}
sb.append( ')' );
}
}

View File

@ -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<String> implements JpaXmlElementExpression {
public SqmXmlElementExpression(
SqmFunctionDescriptor descriptor,
FunctionRenderer renderer,
List<? extends SqmTypedNode<?>> arguments,
@Nullable ReturnableType<String> 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<SqmTypedNode<?>> arguments = (List<SqmTypedNode<?>>) 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<? extends Expression<?>> expressions) {
//noinspection unchecked
final List<SqmTypedNode<?>> arguments = (List<SqmTypedNode<?>>) 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<SqmTypedNode<?>> 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<? extends SqmTypedNode<?>> 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( ')' );
}
}

View File

@ -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<String, Expression> attributes;
public XmlAttributes(Map<String, Expression> attributes) {
this.attributes = attributes;
}
public Map<String, Expression> getAttributes() {
return attributes;
}
@Override
public void accept(SqlAstWalker sqlTreeWalker) {
throw new UnsupportedOperationException("XmlAttributes doesn't support walking");
}
}

View File

@ -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[]
} );
}
}

View File

@ -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( "<empty/>", tuple.get( 0, String.class ) );
assertXmlEquals( "<the-element/>", tuple.get( 1 , String.class ) );
assertXmlEquals( "<myElement>myContent</myElement>", tuple.get( 2, String.class ) );
assertXmlEquals( "<myElement attr1=\"123\"/>", tuple.get( 3, String.class ) );
assertXmlEquals( "<myElement attr1=\"123\" attr-2=\"456\"/>", tuple.get( 4, String.class ) );
assertXmlEquals( "<myElement attr1=\"123\">myContent<empty/></myElement>", 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 = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
return doc.startsWith( "<?xml" ) ? doc : prefix + doc;
}
private static Document parseXml(String document) {
final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
final DocumentBuilder db = dbf.newDocumentBuilder();
return db.parse( new InputSource( new StringReader( document ) ) );
}
catch (ParserConfigurationException | IOException | SAXException e) {
throw new RuntimeException( e );
}
}
private static String toXml(Document document) {
final TransformerFactory tf = TransformerFactory.newInstance();
try {
final Transformer transformer = tf.newTransformer();
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.setOutputProperty( OutputKeys.INDENT, "yes" );
final StringWriter writer = new StringWriter();
transformer.transform( new DOMSource( document ), new StreamResult( writer ) );
return writer.toString();
}
catch (TransformerException e) {
throw new RuntimeException( e );
}
}
@Entity(name = "XmlHolder")
public static class XmlHolder {
@Id
Long id;
@JdbcTypeCode(SqlTypes.SQLXML)
Map<String, Object> xml;
}
}

View File

@ -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 static class IsJtds implements DialectFeatureCheck {
public boolean apply(Dialect dialect) { public boolean apply(Dialect dialect) {
return dialect instanceof SybaseDialect && ( (SybaseDialect) dialect ).getDriverKind() == SybaseDriverKind.JTDS; return dialect instanceof SybaseDialect && ( (SybaseDialect) dialect ).getDriverKind() == SybaseDriverKind.JTDS;