HHH-18497 Add xmlconcat function

This commit is contained in:
Christian Beikov 2024-09-21 00:44:40 +02:00
parent 1abfd4eea6
commit a37ae66a2e
26 changed files with 354 additions and 1 deletions

View File

@ -2173,7 +2173,8 @@ it is necessary to enable the `hibernate.query.hql.xml_functions_enabled` config
| `xmlelement()` | Constructs an XML element from arguments | `xmlelement()` | Constructs an XML element from arguments
| `xmlcomment()` | Constructs an XML comment from the single argument | `xmlcomment()` | Constructs an XML comment from the single argument
| `xmlforest()` | Constructs an XML forest from thearguments | `xmlforest()` | Constructs an XML forest from the arguments
| `xmlconcat()` | Concatenates multiple XML fragments to each other
|=== |===
@ -2250,6 +2251,21 @@ include::{xml-example-dir-hql}/XmlForestTest.java[tags=hql-xmlforest-example]
WARNING: SAP HANA, MySQL, MariaDB and HSQLDB do not support this function. WARNING: SAP HANA, MySQL, MariaDB and HSQLDB do not support this function.
[[hql-xmlconcat-function]]
===== `xmlconcat()`
Concatenates multiple XML fragments to each other.
[[hql-xmlconcat-example]]
====
[source, java, indent=0]
----
include::{xml-example-dir-hql}/XmlConcatTest.java[tags=hql-xmlconcat-example]
----
====
WARNING: SAP HANA, MySQL, MariaDB 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

@ -444,6 +444,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
functionFactory.xmlelement(); functionFactory.xmlelement();
functionFactory.xmlcomment(); functionFactory.xmlcomment();
functionFactory.xmlforest(); functionFactory.xmlforest();
functionFactory.xmlconcat();
} }
@Override @Override

View File

@ -421,6 +421,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
functionFactory.xmlelement_h2(); functionFactory.xmlelement_h2();
functionFactory.xmlcomment(); functionFactory.xmlcomment();
functionFactory.xmlforest_h2(); functionFactory.xmlforest_h2();
functionFactory.xmlconcat_h2();
} }
else { else {
functionFactory.listagg_groupConcat(); functionFactory.listagg_groupConcat();

View File

@ -329,6 +329,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
functionFactory.xmlelement(); functionFactory.xmlelement();
functionFactory.xmlcomment(); functionFactory.xmlcomment();
functionFactory.xmlforest(); functionFactory.xmlforest();
functionFactory.xmlconcat();
} }
@Override @Override

View File

@ -672,6 +672,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
functionFactory.xmlelement(); functionFactory.xmlelement();
functionFactory.xmlcomment(); functionFactory.xmlcomment();
functionFactory.xmlforest(); functionFactory.xmlforest();
functionFactory.xmlconcat();
if ( getVersion().isSameOrAfter( 9, 4 ) ) { if ( getVersion().isSameOrAfter( 9, 4 ) ) {
functionFactory.makeDateTimeTimestamp(); functionFactory.makeDateTimeTimestamp();

View File

@ -416,6 +416,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
functionFactory.xmlelement_sqlserver(); functionFactory.xmlelement_sqlserver();
functionFactory.xmlcomment_sqlserver(); functionFactory.xmlcomment_sqlserver();
functionFactory.xmlforest_sqlserver(); functionFactory.xmlforest_sqlserver();
functionFactory.xmlconcat_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

@ -429,6 +429,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
functionFactory.xmlelement(); functionFactory.xmlelement();
functionFactory.xmlcomment(); functionFactory.xmlcomment();
functionFactory.xmlforest(); functionFactory.xmlforest();
functionFactory.xmlconcat();
} }
@Override @Override

View File

@ -356,6 +356,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
functionFactory.xmlelement_h2(); functionFactory.xmlelement_h2();
functionFactory.xmlcomment(); functionFactory.xmlcomment();
functionFactory.xmlforest_h2(); functionFactory.xmlforest_h2();
functionFactory.xmlconcat_h2();
} }
/** /**

View File

@ -419,6 +419,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
functionFactory.xmlelement(); functionFactory.xmlelement();
functionFactory.xmlcomment(); functionFactory.xmlcomment();
functionFactory.xmlforest(); functionFactory.xmlforest();
functionFactory.xmlconcat();
} }
@Override @Override

View File

@ -633,6 +633,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
functionFactory.xmlelement(); functionFactory.xmlelement();
functionFactory.xmlcomment(); functionFactory.xmlcomment();
functionFactory.xmlforest(); functionFactory.xmlforest();
functionFactory.xmlconcat();
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

@ -434,6 +434,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
functionFactory.xmlelement_sqlserver(); functionFactory.xmlelement_sqlserver();
functionFactory.xmlcomment_sqlserver(); functionFactory.xmlcomment_sqlserver();
functionFactory.xmlforest_sqlserver(); functionFactory.xmlforest_sqlserver();
functionFactory.xmlconcat_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,10 +153,13 @@
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.H2XmlConcatFunction;
import org.hibernate.dialect.function.xml.H2XmlElementFunction; import org.hibernate.dialect.function.xml.H2XmlElementFunction;
import org.hibernate.dialect.function.xml.H2XmlForestFunction; import org.hibernate.dialect.function.xml.H2XmlForestFunction;
import org.hibernate.dialect.function.xml.SQLServerXmlConcatFunction;
import org.hibernate.dialect.function.xml.SQLServerXmlElementFunction; import org.hibernate.dialect.function.xml.SQLServerXmlElementFunction;
import org.hibernate.dialect.function.xml.SQLServerXmlForestFunction; import org.hibernate.dialect.function.xml.SQLServerXmlForestFunction;
import org.hibernate.dialect.function.xml.XmlConcatFunction;
import org.hibernate.dialect.function.xml.XmlElementFunction; import org.hibernate.dialect.function.xml.XmlElementFunction;
import org.hibernate.dialect.function.xml.XmlForestFunction; import org.hibernate.dialect.function.xml.XmlForestFunction;
import org.hibernate.query.sqm.function.SqmFunctionRegistry; import org.hibernate.query.sqm.function.SqmFunctionRegistry;
@ -4167,4 +4170,25 @@ public void xmlforest_h2() {
public void xmlforest_sqlserver() { public void xmlforest_sqlserver() {
functionRegistry.register( "xmlforest", new SQLServerXmlForestFunction( typeConfiguration ) ); functionRegistry.register( "xmlforest", new SQLServerXmlForestFunction( typeConfiguration ) );
} }
/**
* Standard xmlconcat() function
*/
public void xmlconcat() {
functionRegistry.register( "xmlconcat", new XmlConcatFunction( typeConfiguration ) );
}
/**
* H2 xmlconcat() function
*/
public void xmlconcat_h2() {
functionRegistry.register( "xmlconcat", new H2XmlConcatFunction( typeConfiguration ) );
}
/**
* SQL Server xmlconcat() function
*/
public void xmlconcat_sqlserver() {
functionRegistry.register( "xmlconcat", new SQLServerXmlConcatFunction( typeConfiguration ) );
}
} }

View File

@ -0,0 +1,39 @@
/*
* 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 org.hibernate.query.ReturnableType;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.type.spi.TypeConfiguration;
/**
* H2 xmlconcat function.
*/
public class H2XmlConcatFunction extends XmlConcatFunction {
public H2XmlConcatFunction(TypeConfiguration typeConfiguration) {
super( typeConfiguration );
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> sqlAstArguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
String separator = "";
sqlAppender.appendSql( '(' );
for ( SqlAstNode sqlAstArgument : sqlAstArguments ) {
sqlAppender.appendSql( separator );
sqlAstArgument.accept( walker );
separator = "||";
}
sqlAppender.appendSql( ')' );
}
}

View File

@ -0,0 +1,41 @@
/*
* 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 org.hibernate.query.ReturnableType;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.type.spi.TypeConfiguration;
/**
* SQL Server xmlconcat function.
*/
public class SQLServerXmlConcatFunction extends XmlConcatFunction {
public SQLServerXmlConcatFunction(TypeConfiguration typeConfiguration) {
super( typeConfiguration );
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> sqlAstArguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
sqlAppender.appendSql( "cast" );
char separator = '(';
for ( SqlAstNode sqlAstArgument : sqlAstArguments ) {
sqlAppender.appendSql( separator );
sqlAppender.appendSql( "cast(" );
sqlAstArgument.accept( walker );
sqlAppender.appendSql( " as nvarchar(max))" );
separator = '+';
}
sqlAppender.appendSql( " as xml)" );
}
}

View File

@ -0,0 +1,58 @@
/*
* 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 org.hibernate.query.ReturnableType;
import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor;
import org.hibernate.query.sqm.function.FunctionKind;
import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator;
import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators;
import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers;
import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.spi.TypeConfiguration;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.XML;
/**
* Standard XmlConcatFunction function.
*/
public class XmlConcatFunction extends AbstractSqmSelfRenderingFunctionDescriptor {
public XmlConcatFunction(TypeConfiguration typeConfiguration) {
super(
"xmlconcat",
FunctionKind.NORMAL,
StandardArgumentsValidators.composite(
new ArgumentTypesValidator( StandardArgumentsValidators.min( 1 ), XML )
),
StandardFunctionReturnTypeResolvers.invariant(
typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.SQLXML )
),
StandardFunctionArgumentTypeResolvers.impliedOrInvariant( typeConfiguration, XML )
);
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> sqlAstArguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
sqlAppender.appendSql( "xmlconcat" );
char separator = '(';
for ( SqlAstNode sqlAstArgument : sqlAstArguments ) {
sqlAppender.appendSql( separator );
sqlAstArgument.accept( walker );
separator = ',';
}
sqlAppender.appendSql( ')' );
}
}

View File

@ -4067,6 +4067,7 @@ default <E> JpaPredicate collectionOverlapsNullable(Collection<E> collection1, E
* Creates an XML forest from the given XML element expressions. * Creates an XML forest from the given XML element expressions.
* *
* @since 7.0 * @since 7.0
* @see #named(Expression, String)
*/ */
@Incubating @Incubating
JpaExpression<String> xmlforest(Expression<?>... elements); JpaExpression<String> xmlforest(Expression<?>... elements);
@ -4075,10 +4076,27 @@ default <E> JpaPredicate collectionOverlapsNullable(Collection<E> collection1, E
* Creates an XML forest from the given XML element expressions. * Creates an XML forest from the given XML element expressions.
* *
* @since 7.0 * @since 7.0
* @see #named(Expression, String)
*/ */
@Incubating @Incubating
JpaExpression<String> xmlforest(List<? extends Expression<?>> elements); JpaExpression<String> xmlforest(List<? extends Expression<?>> elements);
/**
* Concatenates the given XML element expressions.
*
* @since 7.0
*/
@Incubating
JpaExpression<String> xmlconcat(Expression<?>... elements);
/**
* Concatenates the given XML element expressions.
*
* @since 7.0
*/
@Incubating
JpaExpression<String> xmlconcat(List<? extends Expression<?>> elements);
/** /**
* Creates a named expression. The name is important for the result of the expression, * Creates a named expression. The name is important for the result of the expression,
* e.g. when building an {@code xmlforest}, the name acts as the XML element name. * e.g. when building an {@code xmlforest}, the name acts as the XML element name.

View File

@ -3657,6 +3657,18 @@ public JpaExpression<String> xmlforest(List<? extends Expression<?>> elements) {
return criteriaBuilder.xmlforest( elements ); return criteriaBuilder.xmlforest( elements );
} }
@Override
@Incubating
public JpaExpression<String> xmlconcat(Expression<?>... elements) {
return criteriaBuilder.xmlconcat( elements );
}
@Override
@Incubating
public JpaExpression<String> xmlconcat(List<? extends Expression<?>> elements) {
return criteriaBuilder.xmlconcat( elements );
}
@Override @Override
@Incubating @Incubating
public <T> JpaExpression<T> named(Expression<T> expression, String name) { public <T> JpaExpression<T> named(Expression<T> expression, String name) {

View File

@ -765,6 +765,12 @@ <T> SqmJsonValueExpression<T> jsonValue(
@Override @Override
SqmExpression<String> xmlforest(Expression<?>... elements); SqmExpression<String> xmlforest(Expression<?>... elements);
@Override
SqmExpression<String> xmlconcat(Expression<?>... elements);
@Override
SqmExpression<String> xmlconcat(List<? extends Expression<?>> elements);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Covariant overrides // Covariant overrides

View File

@ -5732,4 +5732,18 @@ public SqmExpression<String> xmlforest(List<? extends Expression<?>> elements) {
queryEngine queryEngine
); );
} }
@Override
public SqmExpression<String> xmlconcat(Expression<?>... elements) {
return xmlconcat( Arrays.asList( elements ) );
}
@Override
public SqmExpression<String> xmlconcat(List<? extends Expression<?>> elements) {
return getFunctionDescriptor( "xmlforest" ).generateSqmExpression(
(List<? extends SqmTypedNode<?>>) elements,
null,
queryEngine
);
}
} }

View File

@ -251,6 +251,8 @@ private static boolean isCompatible(FunctionParameterType type, JdbcType jdbcTyp
case SPATIAL -> jdbcType.isSpatial(); case SPATIAL -> jdbcType.isSpatial();
case JSON -> jdbcType.isJson(); case JSON -> jdbcType.isJson();
case IMPLICIT_JSON -> jdbcType.isImplicitJson(); case IMPLICIT_JSON -> jdbcType.isImplicitJson();
case XML -> jdbcType.isXml();
case IMPLICIT_XML -> jdbcType.isImplicitXml();
default -> true; // TODO: should we throw here? default -> true; // TODO: should we throw here?
}; };
} }

View File

@ -98,6 +98,18 @@ public enum FunctionParameterType {
* @since 7.0 * @since 7.0
*/ */
IMPLICIT_JSON, IMPLICIT_JSON,
/**
* Indicates that the argument should be a XML type
* @see org.hibernate.type.SqlTypes#isXmlType(int)
* @since 7.0
*/
XML,
/**
* Indicates that the argument should be a XML or String type
* @see org.hibernate.type.SqlTypes#isImplicitXmlType(int)
* @since 7.0
*/
IMPLICIT_XML,
/** /**
* Indicates a parameter that accepts any type, except untyped expressions like {@code null} literals * Indicates a parameter that accepts any type, except untyped expressions like {@code null} literals
*/ */

View File

@ -1021,4 +1021,36 @@ public static boolean isImplicitJsonType(int typeCode) {
return isCharacterOrClobType( typeCode ); return isCharacterOrClobType( typeCode );
} }
} }
/**
* Does the typecode represent a XML type.
*
* @param typeCode - a JDBC type code
* @since 7.0
*/
public static boolean isXmlType(int typeCode) {
switch ( typeCode ) {
case SQLXML:
case XML_ARRAY:
return true;
default:
return false;
}
}
/**
* Does the typecode represent an XML type or a type that can be implicitly cast to XML.
*
* @param typeCode - a JDBC type code
* @since 7.0
*/
public static boolean isImplicitXmlType(int typeCode) {
switch ( typeCode ) {
case SQLXML:
case XML_ARRAY:
return true;
default:
return isCharacterOrClobType( typeCode );
}
}
} }

View File

@ -435,6 +435,16 @@ default boolean isImplicitJson() {
return isImplicitJsonType( getDefaultSqlTypeCode() ); return isImplicitJsonType( getDefaultSqlTypeCode() );
} }
@Incubating
default boolean isXml() {
return isXmlType( getDefaultSqlTypeCode() );
}
@Incubating
default boolean isImplicitXml() {
return isImplicitXmlType( getDefaultSqlTypeCode() );
}
@Incubating @Incubating
default boolean isBoolean() { default boolean isBoolean() {
return getDefaultSqlTypeCode() == BOOLEAN; return getDefaultSqlTypeCode() == BOOLEAN;

View File

@ -0,0 +1,36 @@
/*
* 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.SupportsXmlconcat.class)
public class XmlConcatTest {
@Test
public void testSimple(SessionFactoryScope scope) {
scope.inSession( em -> {
//tag::hql-xmlconcat-example[]
em.createQuery( "select xmlconcat(xmlelement(name e1, 123), xmlelement(name e2, 'text'))" ).getResultList();
//end::hql-xmlconcat-example[]
} );
}
}

View File

@ -170,6 +170,23 @@ public void testXmlforest(SessionFactoryScope scope) {
); );
} }
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsXmlconcat.class)
public void testXmlconcat(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
Tuple tuple = session.createQuery(
"select xmlconcat(xmlelement(name e1, 123), xmlelement(name e2, 'text'))," +
"xmlconcat(xmlelement(name id, e.id), xmlelement(name theString, e.theString)) " +
"from EntityOfBasics e where e.id = 1",
Tuple.class
).getSingleResult();
assertXmlEquals( "<r><e1>123</e1><e2>text</e2></r>", "<r>" + tuple.get( 0, String.class ) + "</r>" );
assertXmlEquals( "<r><id>1</id><theString>Dog</theString></r>", "<r>" + tuple.get( 1, String.class ) + "</r>" );
}
);
}
private void assertXmlEquals(String expected, String actual) { private void assertXmlEquals(String expected, String actual) {
final Document expectedDoc = parseXml( xmlNormalize( expected ) ); final Document expectedDoc = parseXml( xmlNormalize( expected ) );
final Document actualDoc = parseXml( xmlNormalize( actual ) ); final Document actualDoc = parseXml( xmlNormalize( actual ) );

View File

@ -856,6 +856,12 @@ public boolean apply(Dialect dialect) {
} }
} }
public static class SupportsXmlconcat implements DialectFeatureCheck {
public boolean apply(Dialect dialect) {
return definesFunction( dialect, "xmlconcat" );
}
}
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;