HHH-18759 Add xmltable() set-returning function

This commit is contained in:
Christian Beikov 2024-10-31 00:02:40 +01:00
parent 854a982927
commit e1a8990358
46 changed files with 2616 additions and 19 deletions

View File

@ -2257,14 +2257,15 @@ it is necessary to enable the `hibernate.query.hql.xml_functions_enabled` config
|===
| Function | Purpose
| `xmlelement()` | Constructs an XML element from arguments
| `xmlcomment()` | Constructs an XML comment from the single argument
| `xmlforest()` | Constructs an XML forest from the arguments
| `xmlconcat()` | Concatenates multiple XML fragments to each other
| `xmlpi()` | Constructs an XML processing instruction
| `xmlquery()` | Extracts content from XML document using XQuery or XPath
| `xmlexists()` | Checks if an XQuery or XPath expression exists in an XML document
| `xmlagg()` | Aggregates XML elements by concatenation
| <<hql-xmlelement-function,`xmlelement()`>> | Constructs an XML element from arguments
| <<hql-xmlcomment-function,`xmlcomment()`>> | Constructs an XML comment from the single argument
| <<hql-xmlforest-function,`xmlforest()`>> | Constructs an XML forest from the arguments
| <<hql-xmlconcat-function,`xmlconcat()`>> | Concatenates multiple XML fragments to each other
| <<hql-xmlpi-function,`xmlpi()`>> | Constructs an XML processing instruction
| <<hql-xmlquery-function,`xmlquery()`>> | Extracts content from XML document using XQuery or XPath
| <<hql-xmlexists-function,`xmlexists()`>> | Checks if an XQuery or XPath expression exists in an XML document
| <<hql-xmlagg-function,`xmlagg()`>> | Aggregates XML elements by concatenation
| <<hql-xmltable-function,`xmltable()`>> | Turns an XML document into rows
|===
@ -2461,6 +2462,39 @@ include::{xml-example-dir-hql}/XmlAggTest.java[tags=hql-xmlagg-example]
WARNING: SAP HANA, MySQL, MariaDB and HSQLDB do not support this function.
[[hql-xmltable-function]]
===== `xmltable()`
A <<hql-from-set-returning-functions,set-returning function>>, which turns an XML document argument into rows.
Returns no rows if the document is `null` or the XPath expression resolves to no nodes.
[[hql-xmltable-bnf]]
[source, antlrv4, indent=0]
----
include::{extrasdir}/xmltable_bnf.txt[]
----
The first argument is the XPath expression. The second argument represents the XML document expression.
Columns that ought to be accessible via the `from` node alias are defined in the `columns` clause,
which can be of varying forms:
* Value attributes - denoted by a `castTarget` after the name, will cast the content of the XML node matching the XPath expression of the column
* Query attributes - denoted by the `xml` type after the name, returns the XML node matching the XPath expression of the column
* Ordinal attributes - denoted by the `for ordinality` syntax after the name, gives access to the 1-based index of the currently processed XML node
[[hql-xmltable-simple-example]]
====
[source, java, indent=0]
----
include::{xml-example-dir-hql}/XmlTableTest.java[tags=hql-xml-table-example]
----
====
The `lateral` keyword is mandatory if one of the arguments refer to a from node item of the same query level.
WARNING: H2, MySQL, MariaDB and HSQLDB do not support this function.
[[hql-user-defined-functions]]
==== Native and user-defined functions
@ -3001,7 +3035,8 @@ The following set-returning functions are available on many platforms:
| <<hql-array-unnest,`unnest()`>> | Turns an array into rows
| <<hql-from-set-returning-functions-generate-series,`generate_series()`>> | Creates a series of values as rows
| <<hql-json-table,`json_table()`>> | Turns a JSON document into rows
| <<hql-json-table-function,`json_table()`>> | Turns a JSON document into rows
| <<hql-xmltable-function,`xmltable()`>> | Turns an XML document into rows
|===
To use set returning functions defined in the database, it is required to register them in a `FunctionContributor`:

View File

@ -0,0 +1,12 @@
"xmltable(" expression "passing" expression columnsClause ")"
columnsClause
: "columns" column ("," column)*
column
: attributeName "xml" ("path" STRING_LITERAL)? defaultClause?
| attributeName "for ordinality"
| attributeName castTarget ("path" STRING_LITERAL)? defaultClause?
defaultClause
: "default" expression;

View File

@ -457,6 +457,7 @@ public class DB2LegacyDialect extends Dialect {
functionFactory.xmlexists_db2_legacy();
}
functionFactory.xmlagg();
functionFactory.xmltable_db2();
functionFactory.unnest_emulated();
if ( supportsRecursiveCTE() ) {

View File

@ -508,7 +508,7 @@ public class HANALegacyDialect extends Dialect {
functionFactory.jsonObjectAgg_hana();
}
// functionFactory.xmltable();
functionFactory.xmltable_hana();
}
// functionFactory.xmlextract();

View File

@ -334,6 +334,7 @@ public class OracleLegacyDialect extends Dialect {
functionFactory.xmlquery_oracle();
functionFactory.xmlexists();
functionFactory.xmlagg();
functionFactory.xmltable_oracle();
functionFactory.unnest_oracle();
functionFactory.generateSeries_recursive( getMaximumSeriesSize(), true, false );

View File

@ -678,6 +678,7 @@ public class PostgreSQLLegacyDialect extends Dialect {
functionFactory.xmlquery_postgresql();
functionFactory.xmlexists();
functionFactory.xmlagg();
functionFactory.xmltable( true );
if ( getVersion().isSameOrAfter( 9, 4 ) ) {
functionFactory.makeDateTimeTimestamp();

View File

@ -429,6 +429,7 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect {
functionFactory.xmlquery_sqlserver();
functionFactory.xmlexists_sqlserver();
functionFactory.xmlagg_sqlserver();
functionFactory.xmltable_sqlserver();
functionFactory.unnest_sqlserver();

View File

@ -167,6 +167,7 @@ public class SybaseASELegacyDialect extends SybaseLegacyDialect {
functionFactory.unnest_sybasease();
functionFactory.generateSeries_sybasease( getMaximumSeriesSize() );
functionFactory.xmltable_sybasease();
}
/**

View File

@ -336,6 +336,7 @@ WITH : [wW] [iI] [tT] [hH];
WITHIN : [wW] [iI] [tT] [hH] [iI] [nN];
WITHOUT : [wW] [iI] [tT] [hH] [oO] [uU] [tT];
WRAPPER : [wW] [rR] [aA] [pP] [pP] [eE] [rR];
XML : [xX] [mM] [lL];
XMLAGG : [xX] [mM] [lL] [aA] [gG] [gG];
XMLATTRIBUTES : [xX] [mM] [lL] [aA] [tT] [tT] [rR] [iI] [bB] [uU] [tT] [eE] [sS];
XMLELEMENT : [xX] [mM] [lL] [eE] [lL] [eE] [mM] [eE] [nN] [tT];
@ -343,6 +344,7 @@ XMLEXISTS : [xX] [mM] [lL] [eE] [xX] [iI] [sS] [tT] [sS];
XMLFOREST : [xX] [mM] [lL] [fF] [oO] [rR] [eE] [sS] [tT];
XMLPI : [xX] [mM] [lL] [pP] [iI];
XMLQUERY : [xX] [mM] [lL] [qQ] [uU] [eE] [rR] [yY];
XMLTABLE : [xX] [mM] [lL] [tT] [aA] [bB] [lL] [eE];
YEAR : [yY] [eE] [aA] [rR];
ZONED : [zZ] [oO] [nN] [eE] [dD];

View File

@ -1119,6 +1119,7 @@ function
setReturningFunction
: simpleSetReturningFunction
| jsonTableFunction
| xmltableFunction
;
/**
@ -1813,6 +1814,24 @@ xmlaggFunction
: XMLAGG LEFT_PAREN expression orderByClause? RIGHT_PAREN filterClause? overClause?
;
xmltableFunction
: XMLTABLE LEFT_PAREN expression PASSING expression xmltableColumnsClause RIGHT_PAREN
;
xmltableColumnsClause
: COLUMNS xmltableColumn (COMMA xmltableColumn)*
;
xmltableColumn
: identifier XML (PATH STRING_LITERAL)? xmltableDefaultClause? # XmlTableQueryColumn
| identifier FOR ORDINALITY # XmlTableOrdinalityColumn
| identifier castTarget (PATH STRING_LITERAL)? xmltableDefaultClause? # XmlTableValueColumn
;
xmltableDefaultClause
: DEFAULT expression
;
/**
* Support for "soft" keywords which may be used as identifiers
*
@ -2025,6 +2044,7 @@ xmlaggFunction
| WITHIN
| WITHOUT
| WRAPPER
| XML
| XMLAGG
| XMLATTRIBUTES
| XMLELEMENT
@ -2032,6 +2052,7 @@ xmlaggFunction
| XMLFOREST
| XMLPI
| XMLQUERY
| XMLTABLE
| YEAR
| ZONED) {
logUseOfReservedWordAsIdentifier( getCurrentToken() );

View File

@ -442,6 +442,7 @@ public class DB2Dialect extends Dialect {
functionFactory.xmlexists_db2_legacy();
}
functionFactory.xmlagg();
functionFactory.xmltable_db2();
functionFactory.unnest_emulated();
functionFactory.generateSeries_recursive( getMaximumSeriesSize(), false, true );

View File

@ -511,7 +511,7 @@ public class HANADialect extends Dialect {
functionFactory.jsonArrayAgg_hana();
functionFactory.jsonObjectAgg_hana();
// functionFactory.xmltable();
functionFactory.xmltable_hana();
// functionFactory.xmlextract();
functionFactory.generateSeries_hana( getMaximumSeriesSize() );

View File

@ -421,6 +421,7 @@ public class OracleDialect extends Dialect {
functionFactory.xmlquery_oracle();
functionFactory.xmlexists();
functionFactory.xmlagg();
functionFactory.xmltable_oracle();
functionFactory.unnest_oracle();
functionFactory.generateSeries_recursive( getMaximumSeriesSize(), true, false );

View File

@ -640,6 +640,7 @@ public class PostgreSQLDialect extends Dialect {
functionFactory.xmlquery_postgresql();
functionFactory.xmlexists();
functionFactory.xmlagg();
functionFactory.xmltable( true );
functionFactory.makeDateTimeTimestamp();
// Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions

View File

@ -446,6 +446,7 @@ public class SQLServerDialect extends AbstractTransactSQLDialect {
functionFactory.xmlquery_sqlserver();
functionFactory.xmlexists_sqlserver();
functionFactory.xmlagg_sqlserver();
functionFactory.xmltable_sqlserver();
functionFactory.unnest_sqlserver();

View File

@ -184,6 +184,7 @@ public class SybaseASEDialect extends SybaseDialect {
functionFactory.unnest_sybasease();
functionFactory.generateSeries_sybasease( getMaximumSeriesSize() );
functionFactory.xmltable_sybasease();
}
/**

View File

@ -13,12 +13,15 @@ import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.function.array.*;
import org.hibernate.dialect.function.json.*;
import org.hibernate.dialect.function.xml.DB2XmlTableFunction;
import org.hibernate.dialect.function.xml.H2XmlConcatFunction;
import org.hibernate.dialect.function.xml.H2XmlElementFunction;
import org.hibernate.dialect.function.xml.H2XmlForestFunction;
import org.hibernate.dialect.function.xml.H2XmlPiFunction;
import org.hibernate.dialect.function.xml.HANAXmlTableFunction;
import org.hibernate.dialect.function.xml.LegacyDB2XmlExistsFunction;
import org.hibernate.dialect.function.xml.LegacyDB2XmlQueryFunction;
import org.hibernate.dialect.function.xml.OracleXmlTableFunction;
import org.hibernate.dialect.function.xml.PostgreSQLXmlQueryFunction;
import org.hibernate.dialect.function.xml.SQLServerXmlAggFunction;
import org.hibernate.dialect.function.xml.SQLServerXmlConcatFunction;
@ -27,6 +30,8 @@ import org.hibernate.dialect.function.xml.SQLServerXmlExistsFunction;
import org.hibernate.dialect.function.xml.SQLServerXmlForestFunction;
import org.hibernate.dialect.function.xml.SQLServerXmlPiFunction;
import org.hibernate.dialect.function.xml.SQLServerXmlQueryFunction;
import org.hibernate.dialect.function.xml.SQLServerXmlTableFunction;
import org.hibernate.dialect.function.xml.SybaseASEXmlTableFunction;
import org.hibernate.dialect.function.xml.XmlAggFunction;
import org.hibernate.dialect.function.xml.XmlConcatFunction;
import org.hibernate.dialect.function.xml.XmlElementFunction;
@ -34,6 +39,7 @@ import org.hibernate.dialect.function.xml.XmlExistsFunction;
import org.hibernate.dialect.function.xml.XmlForestFunction;
import org.hibernate.dialect.function.xml.XmlPiFunction;
import org.hibernate.dialect.function.xml.XmlQueryFunction;
import org.hibernate.dialect.function.xml.XmlTableFunction;
import org.hibernate.query.sqm.function.SqmFunctionRegistry;
import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator;
import org.hibernate.query.sqm.produce.function.FunctionParameterType;
@ -4323,4 +4329,46 @@ public class CommonFunctionFactory {
public void jsonTable_h2(int maximumArraySize) {
functionRegistry.register( "json_table", new H2JsonTableFunction( maximumArraySize, typeConfiguration ) );
}
/**
* Standard xmltable() function
*/
public void xmltable(boolean supportsParametersInDefault) {
functionRegistry.register( "xmltable", new XmlTableFunction( supportsParametersInDefault, typeConfiguration ) );
}
/**
* Oracle xmltable() function
*/
public void xmltable_oracle() {
functionRegistry.register( "xmltable", new OracleXmlTableFunction( typeConfiguration ) );
}
/**
* DB2 xmltable() function
*/
public void xmltable_db2() {
functionRegistry.register( "xmltable", new DB2XmlTableFunction( typeConfiguration ) );
}
/**
* HANA xmltable() function
*/
public void xmltable_hana() {
functionRegistry.register( "xmltable", new HANAXmlTableFunction( typeConfiguration ) );
}
/**
* SQL Server xmltable() function
*/
public void xmltable_sqlserver() {
functionRegistry.register( "xmltable", new SQLServerXmlTableFunction( typeConfiguration ) );
}
/**
* Sybase ASE xmltable() function
*/
public void xmltable_sybasease() {
functionRegistry.register( "xmltable", new SybaseASEXmlTableFunction( typeConfiguration ) );
}
}

View File

@ -411,7 +411,7 @@ public class HANAUnnestFunction extends UnnestFunction {
sessionFactory
);
// Produce a XML string e.g. <root id="1">...</root>
// Produce an XML string e.g. <root id="1">...</root>
// which will contain the original XML as well as id column information for correlation
sqlAppender.appendSql( "trim('/>' from (select" );
char separator = ' ';
@ -424,8 +424,9 @@ public class HANAUnnestFunction extends UnnestFunction {
sqlAppender.appendDoubleQuoteEscapedString( columnInfo.name() );
separator = ',';
}
sqlAppender.appendSql( " from sys.dummy for xml('root'='no','columnstyle'='attribute','rowname'='Strings','format'='no')))||" );
sqlAppender.appendSql( "substring(" );
sqlAppender.appendSql( " from sys.dummy for xml('root'='no','columnstyle'='attribute','rowname'='" );
sqlAppender.appendSql( collectionTags.rootName() );
sqlAppender.appendSql( "','format'='no')))||substring(" );
argument.accept( walker );
sqlAppender.appendSql( ",locate('<" );
sqlAppender.appendSql( collectionTags.rootName() );

View File

@ -63,7 +63,7 @@ public class OracleJsonValueFunction extends JsonValueFunction {
}
}
static boolean isEncodedBoolean(JdbcMapping type) {
public static boolean isEncodedBoolean(JdbcMapping type) {
return type.getJdbcType().isBoolean() && type.getJdbcType().getDdlTypeCode() != SqlTypes.BOOLEAN;
}
}

View File

@ -0,0 +1,113 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.dialect.function.xml;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.metamodel.mapping.JdbcMapping;
import org.hibernate.metamodel.mapping.SelectableMapping;
import org.hibernate.metamodel.mapping.SelectablePath;
import org.hibernate.metamodel.mapping.internal.SelectableMappingImpl;
import org.hibernate.query.derived.AnonymousTupleTableGroupProducer;
import org.hibernate.query.sqm.sql.SqmToSqlAstConverter;
import org.hibernate.sql.Template;
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.expression.CastTarget;
import org.hibernate.sql.ast.tree.expression.XmlTableValueColumnDefinition;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter;
import org.hibernate.type.spi.TypeConfiguration;
import java.util.List;
/**
* DB2 xmltable function.
*/
public class DB2XmlTableFunction extends XmlTableFunction {
public DB2XmlTableFunction(TypeConfiguration typeConfiguration) {
super( false, new DB2XmlTableSetReturningFunctionTypeResolver(), typeConfiguration );
}
@Override
protected void renderXmlTable(SqlAppender sqlAppender, XmlTableArguments arguments, AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstTranslator<?> walker) {
sqlAppender.appendSql( "xmltable(" );
// DB2 doesn't like parameters for the xpath expression
walker.render( arguments.xpath(), SqlAstNodeRenderingMode.INLINE_PARAMETERS );
sqlAppender.appendSql( " passing " );
if ( !arguments.isXmlType() ) {
sqlAppender.appendSql( "xmlparse(document " );
}
// DB2 needs parameters to be casted here
walker.render( arguments.xmlDocument(), SqlAstNodeRenderingMode.NO_PLAIN_PARAMETER );
if ( !arguments.isXmlType() ) {
sqlAppender.appendSql( ")" );
}
renderColumns( sqlAppender, arguments.columnsClause(), walker );
sqlAppender.appendSql( ')' );
}
@Override
protected String determineColumnType(CastTarget castTarget, SqlAstTranslator<?> walker) {
final String typeName = super.determineColumnType( castTarget, walker );
return isBoolean( castTarget.getJdbcMapping() ) ? "varchar(5)" : typeName;
}
@Override
protected void renderXmlValueColumnDefinition(SqlAppender sqlAppender, XmlTableValueColumnDefinition definition, SqlAstTranslator<?> walker) {
sqlAppender.appendSql( definition.name() );
sqlAppender.appendSql( ' ' );
sqlAppender.appendSql( determineColumnType( definition.type(), walker ) );
// DB2 wants the default before the path
renderDefaultExpression( definition.defaultExpression(), sqlAppender, walker );
renderColumnPath( definition.name(), definition.xpath(), sqlAppender, walker );
}
static boolean isBoolean(JdbcMapping type) {
return type.getJdbcType().isBoolean();
}
private static class DB2XmlTableSetReturningFunctionTypeResolver extends XmlTableSetReturningFunctionTypeResolver {
@Override
protected void addSelectableMapping(List<SelectableMapping> selectableMappings, String name, JdbcMapping type, SqmToSqlAstConverter converter) {
if ( isBoolean( type ) ) {
//noinspection unchecked
final JdbcLiteralFormatter<Object> jdbcLiteralFormatter = type.getJdbcLiteralFormatter();
final SessionFactoryImplementor sessionFactory = converter.getCreationContext().getSessionFactory();
final Dialect dialect = sessionFactory.getJdbcServices().getDialect();
final WrapperOptions wrapperOptions = sessionFactory.getWrapperOptions();
final Object trueValue = type.convertToRelationalValue( true );
final Object falseValue = type.convertToRelationalValue( false );
final String trueFragment = jdbcLiteralFormatter.toJdbcLiteral( trueValue, dialect, wrapperOptions );
final String falseFragment = jdbcLiteralFormatter.toJdbcLiteral( falseValue, dialect, wrapperOptions );
selectableMappings.add( new SelectableMappingImpl(
"",
name,
new SelectablePath( name ),
"decode(" + Template.TEMPLATE + "." + name + ",'true'," + trueFragment + ",'false'," + falseFragment + ")",
null,
"varchar(5)",
null,
null,
null,
null,
false,
false,
false,
false,
false,
false,
type
));
}
else {
super.addSelectableMapping( selectableMappings, name, type, converter );
}
}
}
}

View File

@ -0,0 +1,454 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.dialect.function.xml;
import org.hibernate.QueryException;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.mapping.EntityValuedModelPart;
import org.hibernate.metamodel.mapping.JdbcMapping;
import org.hibernate.metamodel.mapping.JdbcMappingContainer;
import org.hibernate.metamodel.mapping.ModelPartContainer;
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
import org.hibernate.metamodel.mapping.SelectableMapping;
import org.hibernate.metamodel.mapping.SelectablePath;
import org.hibernate.metamodel.mapping.ValuedModelPart;
import org.hibernate.metamodel.mapping.internal.EmbeddedCollectionPart;
import org.hibernate.metamodel.mapping.internal.SelectableMappingImpl;
import org.hibernate.query.derived.AnonymousTupleTableGroupProducer;
import org.hibernate.query.spi.QueryEngine;
import org.hibernate.query.sqm.ComparisonOperator;
import org.hibernate.query.sqm.function.SelfRenderingSqmSetReturningFunction;
import org.hibernate.query.sqm.sql.SqmToSqlAstConverter;
import org.hibernate.query.sqm.tree.SqmTypedNode;
import org.hibernate.query.sqm.tree.expression.SqmExpression;
import org.hibernate.query.sqm.tree.expression.SqmXmlTableFunction;
import org.hibernate.spi.NavigablePath;
import org.hibernate.sql.Template;
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.internal.ColumnQualifierCollectorSqlAstWalker;
import org.hibernate.sql.ast.spi.FromClauseAccess;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.sql.ast.tree.cte.CteColumn;
import org.hibernate.sql.ast.tree.cte.CteStatement;
import org.hibernate.sql.ast.tree.cte.CteTable;
import org.hibernate.sql.ast.tree.expression.CastTarget;
import org.hibernate.sql.ast.tree.expression.ColumnReference;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.sql.ast.tree.expression.Literal;
import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression;
import org.hibernate.sql.ast.tree.expression.SqlTuple;
import org.hibernate.sql.ast.tree.expression.XmlTableQueryColumnDefinition;
import org.hibernate.sql.ast.tree.from.FunctionTableGroup;
import org.hibernate.sql.ast.tree.from.StandardTableGroup;
import org.hibernate.sql.ast.tree.from.TableGroup;
import org.hibernate.sql.ast.tree.from.TableGroupJoin;
import org.hibernate.sql.ast.tree.from.TableGroupProducer;
import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate;
import org.hibernate.sql.ast.tree.predicate.NullnessPredicate;
import org.hibernate.sql.ast.tree.select.QuerySpec;
import org.hibernate.sql.ast.tree.select.SelectStatement;
import org.hibernate.sql.results.internal.SqlSelectionImpl;
import org.hibernate.type.Type;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter;
import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry;
import org.hibernate.type.spi.TypeConfiguration;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import static org.hibernate.sql.ast.spi.AbstractSqlAstTranslator.isParameter;
/**
* HANA xmltable function.
*/
public class HANAXmlTableFunction extends XmlTableFunction {
public HANAXmlTableFunction(TypeConfiguration typeConfiguration) {
super( false, new DB2XmlTableSetReturningFunctionTypeResolver(), typeConfiguration );
}
@Override
protected <T> SelfRenderingSqmSetReturningFunction<T> generateSqmSetReturningFunctionExpression(
List<? extends SqmTypedNode<?>> arguments,
QueryEngine queryEngine) {
//noinspection unchecked
return new SqmXmlTableFunction<>(
this,
this,
getArgumentsValidator(),
getSetReturningTypeResolver(),
queryEngine.getCriteriaBuilder(),
(SqmExpression<String>) arguments.get( 0 ),
(SqmExpression<?>) arguments.get( 1 )
) {
@Override
public TableGroup convertToSqlAst(
NavigablePath navigablePath,
String identifierVariable,
boolean lateral,
boolean canUseInnerJoins,
boolean withOrdinality,
SqmToSqlAstConverter walker) {
// SAP HANA only supports table column references i.e. `TABLE_NAME.COLUMN_NAME`
// or constants as arguments to xmltable, so it's impossible to do lateral joins.
// There is a nice trick we can apply to make this work though, which is to figure out
// the table group an expression belongs to and render a special CTE returning xml/json that can be joined.
// The xml of that CTE needs to be extended by table group primary key data,
// so we can join it later.
final FunctionTableGroup functionTableGroup = (FunctionTableGroup) super.convertToSqlAst(
navigablePath,
identifierVariable,
lateral,
canUseInnerJoins,
withOrdinality,
walker
);
//noinspection unchecked
final List<SqlAstNode> sqlArguments = (List<SqlAstNode>) functionTableGroup.getPrimaryTableReference()
.getFunctionExpression()
.getArguments();
final Expression document = (Expression) sqlArguments.get( 1 );
final Set<String> qualifiers = ColumnQualifierCollectorSqlAstWalker.determineColumnQualifiers( document );
// Can only do this transformation if the argument contains a single column reference qualifier
if ( qualifiers.size() == 1 ) {
final String tableQualifier = qualifiers.iterator().next();
// Find the table group which the unnest argument refers to
final FromClauseAccess fromClauseAccess = walker.getFromClauseAccess();
final TableGroup sourceTableGroup =
fromClauseAccess.findTableGroupByIdentificationVariable( tableQualifier );
if ( sourceTableGroup != null ) {
final List<ColumnInfo> idColumns = new ArrayList<>();
addIdColumns( sourceTableGroup.getModelPart(), idColumns );
// Register a query transformer to register the CTE and rewrite the array argument
walker.registerQueryTransformer( (cteContainer, querySpec, converter) -> {
// Determine a CTE name that is available
final String baseName = "_data";
String cteName;
int index = 0;
do {
cteName = baseName + ( index++ );
} while ( cteContainer.getCteStatement( cteName ) != null );
final TableGroup parentTableGroup = querySpec.getFromClause().queryTableGroups(
tg -> tg.findTableGroupJoin( functionTableGroup ) == null ? null : tg
);
final TableGroupJoin join = parentTableGroup.findTableGroupJoin( functionTableGroup );
final Expression lhs = createExpression( tableQualifier, idColumns );
final Expression rhs = createExpression(
functionTableGroup.getPrimaryTableReference().getIdentificationVariable(),
idColumns
);
join.applyPredicate( new ComparisonPredicate( lhs, ComparisonOperator.EQUAL, rhs ) );
final String tableName = cteName;
final List<CteColumn> cteColumns = List.of(
new CteColumn( "v", document.getExpressionType().getSingleJdbcMapping() )
);
final QuerySpec cteQuery = new QuerySpec( false );
cteQuery.getFromClause().addRoot(
new StandardTableGroup(
true,
sourceTableGroup.getNavigablePath(),
(TableGroupProducer) sourceTableGroup.getModelPart(),
false,
null,
sourceTableGroup.findTableReference( tableQualifier ),
false,
null,
joinTableName -> false,
(joinTableName, tg) -> null,
null
)
);
final Expression wrapperExpression = new XmlWrapperExpression( idColumns, tableQualifier, document );
// xmltable is allergic to null values and produces no result if one occurs,
// so we must filter them out
cteQuery.applyPredicate( new NullnessPredicate( document, true ) );
cteQuery.getSelectClause().addSqlSelection( new SqlSelectionImpl( wrapperExpression ) );
cteContainer.addCteStatement( new CteStatement(
new CteTable( tableName, cteColumns ),
new SelectStatement( cteQuery )
) );
sqlArguments.set( 1, new TableColumnReferenceExpression( document, tableName, idColumns ) );
return querySpec;
} );
}
}
return functionTableGroup;
}
private Expression createExpression(String qualifier, List<ColumnInfo> idColumns) {
if ( idColumns.size() == 1 ) {
final ColumnInfo columnInfo = idColumns.get( 0 );
return new ColumnReference( qualifier, columnInfo.name(), false, null, columnInfo.jdbcMapping() );
}
else {
final ArrayList<Expression> expressions = new ArrayList<>( idColumns.size() );
for ( ColumnInfo columnInfo : idColumns ) {
expressions.add(
new ColumnReference(
qualifier,
columnInfo.name(),
false,
null,
columnInfo.jdbcMapping()
)
);
}
return new SqlTuple( expressions, null );
}
}
private void addIdColumns(ModelPartContainer modelPartContainer, List<ColumnInfo> idColumns) {
if ( modelPartContainer instanceof EntityValuedModelPart entityValuedModelPart ) {
addIdColumns( entityValuedModelPart.getEntityMappingType(), idColumns );
}
else if ( modelPartContainer instanceof PluralAttributeMapping pluralAttributeMapping ) {
addIdColumns( pluralAttributeMapping, idColumns );
}
else if ( modelPartContainer instanceof EmbeddableValuedModelPart embeddableModelPart ) {
addIdColumns( embeddableModelPart, idColumns );
}
else {
throw new QueryException( "Unsupported model part container: " + modelPartContainer );
}
}
private void addIdColumns(EmbeddableValuedModelPart embeddableModelPart, List<ColumnInfo> idColumns) {
if ( embeddableModelPart instanceof EmbeddedCollectionPart collectionPart ) {
addIdColumns( collectionPart.getCollectionAttribute(), idColumns );
}
else {
addIdColumns( embeddableModelPart.asAttributeMapping().getDeclaringType(), idColumns );
}
}
private void addIdColumns(PluralAttributeMapping pluralAttributeMapping, List<ColumnInfo> idColumns) {
final DdlTypeRegistry ddlTypeRegistry = pluralAttributeMapping.getCollectionDescriptor()
.getFactory()
.getTypeConfiguration()
.getDdlTypeRegistry();
addIdColumns( pluralAttributeMapping.getKeyDescriptor().getKeyPart(), ddlTypeRegistry, idColumns );
}
private void addIdColumns(EntityMappingType entityMappingType, List<ColumnInfo> idColumns) {
final DdlTypeRegistry ddlTypeRegistry = entityMappingType.getEntityPersister()
.getFactory()
.getTypeConfiguration()
.getDdlTypeRegistry();
addIdColumns( entityMappingType.getIdentifierMapping(), ddlTypeRegistry, idColumns );
}
private void addIdColumns(
ValuedModelPart modelPart,
DdlTypeRegistry ddlTypeRegistry,
List<ColumnInfo> idColumns) {
modelPart.forEachSelectable( (selectionIndex, selectableMapping) -> {
final JdbcMapping jdbcMapping = selectableMapping.getJdbcMapping().getSingleJdbcMapping();
idColumns.add( new ColumnInfo(
selectableMapping.getSelectionExpression(),
jdbcMapping,
ddlTypeRegistry.getTypeName(
jdbcMapping.getJdbcType().getDefaultSqlTypeCode(),
selectableMapping.toSize(),
(Type) jdbcMapping
)
) );
} );
}
};
}
record ColumnInfo(String name, JdbcMapping jdbcMapping, String ddlType) {}
static class TableColumnReferenceExpression implements SelfRenderingExpression {
private final Expression argument;
private final String tableName;
private final List<ColumnInfo> idColumns;
public TableColumnReferenceExpression(Expression argument, String tableName, List<ColumnInfo> idColumns) {
this.argument = argument;
this.tableName = tableName;
this.idColumns = idColumns;
}
@Override
public void renderToSql(
SqlAppender sqlAppender,
SqlAstTranslator<?> walker,
SessionFactoryImplementor sessionFactory) {
sqlAppender.appendSql( tableName );
sqlAppender.appendSql( ".v" );
}
@Override
public JdbcMappingContainer getExpressionType() {
return argument.getExpressionType();
}
public List<ColumnInfo> getIdColumns() {
return idColumns;
}
}
static class XmlWrapperExpression implements SelfRenderingExpression {
private final List<ColumnInfo> idColumns;
private final String tableQualifier;
private final Expression argument;
public XmlWrapperExpression(List<ColumnInfo> idColumns, String tableQualifier, Expression argument) {
this.idColumns = idColumns;
this.tableQualifier = tableQualifier;
this.argument = argument;
}
@Override
public void renderToSql(
SqlAppender sqlAppender,
SqlAstTranslator<?> walker,
SessionFactoryImplementor sessionFactory) {
// Produce an XML string e.g. <root id="1">...</root>
// which will contain the original XML as well as id column information for correlation
sqlAppender.appendSql( "'<root'" );
for ( ColumnInfo columnInfo : idColumns ) {
sqlAppender.appendSql( "||' " );
sqlAppender.appendSql( columnInfo.name() );
sqlAppender.appendSql( "=\"'||" );
sqlAppender.appendSql( tableQualifier );
sqlAppender.appendSql( '.' );
sqlAppender.appendSql( columnInfo.name() );
sqlAppender.appendSql( "||'\"'" );
}
sqlAppender.appendSql( "||'>'||" );
argument.accept( walker );
sqlAppender.appendSql( "||'</root>'" );
}
@Override
public JdbcMappingContainer getExpressionType() {
return argument.getExpressionType();
}
}
@Override
protected void renderXmlTable(SqlAppender sqlAppender, XmlTableArguments arguments, AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstTranslator<?> walker) {
sqlAppender.appendSql( "xmltable(" );
final Expression documentExpression = arguments.xmlDocument();
final String xpath = walker.getLiteralValue( arguments.xpath() );
if ( documentExpression instanceof TableColumnReferenceExpression ) {
sqlAppender.appendSingleQuoteEscapedString( "/root" + xpath );
}
else {
sqlAppender.appendSingleQuoteEscapedString( xpath );
}
sqlAppender.appendSql( " passing " );
// We have to handle the rendering of strings/literals manually here to avoid using nationalized literals,
// because HANA doesn't support that syntax in xmltable()
if ( documentExpression instanceof Literal literal ) {
sqlAppender.appendSingleQuoteEscapedString( (String) literal.getLiteralValue() );
}
else if ( isParameter( documentExpression ) ) {
sqlAppender.appendSingleQuoteEscapedString( walker.getLiteralValue( documentExpression ) );
}
else {
documentExpression.accept( walker );
}
renderColumns( sqlAppender, arguments.columnsClause(), walker );
if ( documentExpression instanceof TableColumnReferenceExpression expression ) {
for ( ColumnInfo columnInfo : expression.getIdColumns() ) {
sqlAppender.appendSql( ',' );
sqlAppender.appendSql( columnInfo.name() );
sqlAppender.appendSql( ' ' );
sqlAppender.appendSql( columnInfo.ddlType() );
sqlAppender.appendSql( " path '/root/@" );
sqlAppender.appendSql( columnInfo.name() );
sqlAppender.appendSql( '\'' );
}
}
sqlAppender.appendSql( ')' );
}
@Override
protected String determineColumnType(CastTarget castTarget, SqlAstTranslator<?> walker) {
final String typeName = super.determineColumnType( castTarget, walker );
return switch ( typeName ) {
// xmltable doesn't support tinyint. Usually it is a boolean, but if not, use "integer"
case "tinyint" -> isBoolean( castTarget.getJdbcMapping() ) ? "varchar(5)" : "integer";
// Also, smallint isn't supported
case "smallint" -> "integer";
// For boolean, use varchar since that decoding is done through a read expression
case "boolean" -> "varchar(5)";
// Float is also not supported, but double is
case "float" -> "double";
// Clobs are also not supported, so use the biggest nvarchar possible
case "clob", "nclob" -> "nvarchar(5000)";
default -> typeName;
};
}
@Override
protected void renderXmlQueryColumnDefinition(SqlAppender sqlAppender, XmlTableQueryColumnDefinition definition, SqlAstTranslator<?> walker) {
sqlAppender.appendSql( definition.name() );
sqlAppender.appendSql( ' ' );
sqlAppender.appendSql( determineColumnType( new CastTarget( definition.type() ), walker ) );
sqlAppender.appendSql( " format xml" );
renderColumnPath( definition.name(), definition.xpath(), sqlAppender, walker );
renderDefaultExpression( definition.defaultExpression(), sqlAppender, walker );
}
static boolean isBoolean(JdbcMapping type) {
return type.getJdbcType().isBoolean();
}
private static class DB2XmlTableSetReturningFunctionTypeResolver extends XmlTableSetReturningFunctionTypeResolver {
@Override
protected void addSelectableMapping(List<SelectableMapping> selectableMappings, String name, JdbcMapping type, SqmToSqlAstConverter converter) {
if ( isBoolean( type ) ) {
//noinspection unchecked
final JdbcLiteralFormatter<Object> jdbcLiteralFormatter = type.getJdbcLiteralFormatter();
final SessionFactoryImplementor sessionFactory = converter.getCreationContext().getSessionFactory();
final Dialect dialect = sessionFactory.getJdbcServices().getDialect();
final WrapperOptions wrapperOptions = sessionFactory.getWrapperOptions();
final Object trueValue = type.convertToRelationalValue( true );
final Object falseValue = type.convertToRelationalValue( false );
final String trueFragment = jdbcLiteralFormatter.toJdbcLiteral( trueValue, dialect, wrapperOptions );
final String falseFragment = jdbcLiteralFormatter.toJdbcLiteral( falseValue, dialect, wrapperOptions );
selectableMappings.add( new SelectableMappingImpl(
"",
name,
new SelectablePath( name ),
"case " + Template.TEMPLATE + "." + name + " when 'true' then " + trueFragment + " when 'false' then " + falseFragment + " end",
null,
"varchar(5)",
null,
null,
null,
null,
false,
false,
false,
false,
false,
false,
type
));
}
else {
super.addSelectableMapping( selectableMappings, name, type, converter );
}
}
}
}

View File

@ -0,0 +1,83 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.dialect.function.xml;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.metamodel.mapping.JdbcMapping;
import org.hibernate.metamodel.mapping.SelectableMapping;
import org.hibernate.metamodel.mapping.SelectablePath;
import org.hibernate.metamodel.mapping.internal.SelectableMappingImpl;
import org.hibernate.query.sqm.sql.SqmToSqlAstConverter;
import org.hibernate.sql.Template;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.tree.expression.CastTarget;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter;
import org.hibernate.type.spi.TypeConfiguration;
import java.util.List;
import static org.hibernate.dialect.function.json.OracleJsonValueFunction.isEncodedBoolean;
/**
* Oracle xmltable function.
*/
public class OracleXmlTableFunction extends XmlTableFunction {
public OracleXmlTableFunction(TypeConfiguration typeConfiguration) {
super( false, new OracleXmlTableSetReturningFunctionTypeResolver(), typeConfiguration );
}
@Override
protected String determineColumnType(CastTarget castTarget, SqlAstTranslator<?> walker) {
final String typeName = super.determineColumnType( castTarget, walker );
return switch ( typeName ) {
// clob is not supported as column type for xmltable
case "clob" -> "varchar2(" + walker.getSessionFactory().getJdbcServices().getDialect().getMaxVarcharLength() + ")";
case "number(1,0)" -> isEncodedBoolean( castTarget.getJdbcMapping() ) ? "varchar2(5)" : typeName;
default -> typeName;
};
}
private static class OracleXmlTableSetReturningFunctionTypeResolver extends XmlTableSetReturningFunctionTypeResolver {
@Override
protected void addSelectableMapping(List<SelectableMapping> selectableMappings, String name, JdbcMapping type, SqmToSqlAstConverter converter) {
if ( isEncodedBoolean( type ) ) {
//noinspection unchecked
final JdbcLiteralFormatter<Object> jdbcLiteralFormatter = type.getJdbcLiteralFormatter();
final SessionFactoryImplementor sessionFactory = converter.getCreationContext().getSessionFactory();
final Dialect dialect = sessionFactory.getJdbcServices().getDialect();
final WrapperOptions wrapperOptions = sessionFactory.getWrapperOptions();
final Object trueValue = type.convertToRelationalValue( true );
final Object falseValue = type.convertToRelationalValue( false );
final String trueFragment = jdbcLiteralFormatter.toJdbcLiteral( trueValue, dialect, wrapperOptions );
final String falseFragment = jdbcLiteralFormatter.toJdbcLiteral( falseValue, dialect, wrapperOptions );
selectableMappings.add( new SelectableMappingImpl(
"",
name,
new SelectablePath( name ),
"decode(" + Template.TEMPLATE + "." + name + ",'true'," + trueFragment + ",'false'," + falseFragment + ")",
null,
"varchar2(5)",
null,
null,
null,
null,
false,
false,
false,
false,
false,
false,
type
));
}
else {
super.addSelectableMapping( selectableMappings, name, type, converter );
}
}
}
}

View File

@ -0,0 +1,109 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.dialect.function.xml;
import org.hibernate.query.derived.AnonymousTupleTableGroupProducer;
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.expression.XmlTableColumnDefinition;
import org.hibernate.sql.ast.tree.expression.XmlTableColumnsClause;
import org.hibernate.sql.ast.tree.expression.XmlTableOrdinalityColumnDefinition;
import org.hibernate.sql.ast.tree.expression.XmlTableQueryColumnDefinition;
import org.hibernate.sql.ast.tree.expression.XmlTableValueColumnDefinition;
import org.hibernate.type.spi.TypeConfiguration;
/**
* SQL Server xmltable function.
*/
public class SQLServerXmlTableFunction extends XmlTableFunction {
public SQLServerXmlTableFunction(TypeConfiguration typeConfiguration) {
super( false, typeConfiguration );
}
@Override
protected void renderXmlTable(SqlAppender sqlAppender, XmlTableArguments arguments, AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstTranslator<?> walker) {
sqlAppender.appendSql( "(select" );
renderColumns( sqlAppender, arguments.columnsClause(), walker );
sqlAppender.appendSql( " from (select " );
if ( !arguments.isXmlType() ) {
sqlAppender.appendSql( "cast(" );
}
arguments.xmlDocument().accept( walker );
if ( !arguments.isXmlType() ) {
sqlAppender.appendSql( " as xml)" );
}
sqlAppender.appendSql( ") t0_(d) cross apply t0_.d.nodes(" );
walker.render( arguments.xpath(), SqlAstNodeRenderingMode.INLINE_PARAMETERS );
sqlAppender.appendSql( ") t1_(d))" );
}
@Override
protected void renderColumns(SqlAppender sqlAppender, XmlTableColumnsClause xmlTableColumnsClause, SqlAstTranslator<?> walker) {
char separator = ' ';
for ( XmlTableColumnDefinition columnDefinition : xmlTableColumnsClause.getColumnDefinitions() ) {
sqlAppender.appendSql( separator );
if ( columnDefinition instanceof XmlTableQueryColumnDefinition definition ) {
renderXmlQueryColumnDefinition( sqlAppender, definition, walker );
}
else if ( columnDefinition instanceof XmlTableValueColumnDefinition definition ) {
renderXmlValueColumnDefinition( sqlAppender, definition, walker );
}
else {
renderXmlOrdinalityColumnDefinition(
sqlAppender,
(XmlTableOrdinalityColumnDefinition) columnDefinition,
walker
);
}
separator = ',';
}
}
@Override
protected void renderXmlOrdinalityColumnDefinition(SqlAppender sqlAppender, XmlTableOrdinalityColumnDefinition definition, SqlAstTranslator<?> walker) {
sqlAppender.appendSql( "row_number() over (order by (select 1)) " );
sqlAppender.appendSql( definition.name() );
}
@Override
protected void renderXmlValueColumnDefinition(SqlAppender sqlAppender, XmlTableValueColumnDefinition definition, SqlAstTranslator<?> walker) {
if ( definition.defaultExpression() != null ) {
sqlAppender.appendSql( "coalesce(" );
}
sqlAppender.appendSql( "t1_.d.value('(" );
sqlAppender.appendSql( definition.xpath() == null ? definition.name() : definition.xpath() );
sqlAppender.appendSql( ")[1]'," );
sqlAppender.appendSingleQuoteEscapedString( determineColumnType( definition.type(), walker ) );
sqlAppender.appendSql( ')' );
if ( definition.defaultExpression() != null ) {
sqlAppender.appendSql( ',' );
definition.defaultExpression().accept( walker );
sqlAppender.appendSql( ')' );
}
sqlAppender.appendSql( ' ' );
sqlAppender.appendSql( definition.name() );
}
@Override
protected void renderXmlQueryColumnDefinition(SqlAppender sqlAppender, XmlTableQueryColumnDefinition definition, SqlAstTranslator<?> walker) {
if ( definition.defaultExpression() != null ) {
sqlAppender.appendSql( "coalesce(" );
}
sqlAppender.appendSql( "t1_.d.query('(" );
sqlAppender.appendSql( definition.xpath() == null ? definition.name() : definition.xpath() );
sqlAppender.appendSql( ")[1]')" );
if ( definition.defaultExpression() != null ) {
sqlAppender.appendSql( ',' );
definition.defaultExpression().accept( walker );
sqlAppender.appendSql( ')' );
}
sqlAppender.appendSql( ' ' );
sqlAppender.appendSql( definition.name() );
}
}

View File

@ -0,0 +1,208 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.dialect.function.xml;
import org.hibernate.QueryException;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.metamodel.mapping.JdbcMapping;
import org.hibernate.metamodel.mapping.SelectableMapping;
import org.hibernate.metamodel.mapping.SelectablePath;
import org.hibernate.metamodel.mapping.internal.SelectableMappingImpl;
import org.hibernate.query.derived.AnonymousTupleTableGroupProducer;
import org.hibernate.query.sqm.sql.SqmToSqlAstConverter;
import org.hibernate.sql.Template;
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
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.CastTarget;
import org.hibernate.sql.ast.tree.expression.ColumnReference;
import org.hibernate.sql.ast.tree.expression.Literal;
import org.hibernate.sql.ast.tree.expression.XmlTableColumnDefinition;
import org.hibernate.sql.ast.tree.expression.XmlTableOrdinalityColumnDefinition;
import org.hibernate.sql.ast.tree.expression.XmlTableQueryColumnDefinition;
import org.hibernate.sql.ast.tree.expression.XmlTableValueColumnDefinition;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter;
import org.hibernate.type.spi.TypeConfiguration;
import java.util.ArrayList;
import java.util.List;
/**
* Sybase ASE xmltable function.
*/
public class SybaseASEXmlTableFunction extends XmlTableFunction {
public SybaseASEXmlTableFunction(TypeConfiguration typeConfiguration) {
super( false, new SybaseASEXmlTableSetReturningFunctionTypeResolver(), typeConfiguration );
}
@Override
protected void renderXmlTable(SqlAppender sqlAppender, XmlTableArguments arguments, AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstTranslator<?> walker) {
sqlAppender.appendSql( "xmltable(" );
walker.render( arguments.xpath(), SqlAstNodeRenderingMode.INLINE_PARAMETERS );
sqlAppender.appendSql( " passing " );
walker.render( arguments.xmlDocument(), SqlAstNodeRenderingMode.INLINE_PARAMETERS );
renderColumns( sqlAppender, arguments.columnsClause(), walker );
sqlAppender.appendSql( ')' );
}
@Override
protected String determineColumnType(CastTarget castTarget, SqlAstTranslator<?> walker) {
if ( isBoolean( castTarget.getJdbcMapping() ) ) {
return "varchar(5)";
}
else {
return super.determineColumnType( castTarget, walker );
}
}
@Override
protected void renderXmlQueryColumnDefinition(SqlAppender sqlAppender, XmlTableQueryColumnDefinition definition, SqlAstTranslator<?> walker) {
// Queries don't really work, so we have to extract the ordinality instead and extract the value through a read expression
sqlAppender.appendSql( definition.name() );
sqlAppender.appendSql( " int for ordinality" );
}
@Override
protected void renderXmlValueColumnDefinition(SqlAppender sqlAppender, XmlTableValueColumnDefinition definition, SqlAstTranslator<?> walker) {
sqlAppender.appendSql( definition.name() );
sqlAppender.appendSql( ' ' );
sqlAppender.appendSql( determineColumnType( definition.type(), walker ) );
// Sybase ASE wants the default before the path
renderDefaultExpression( definition.defaultExpression(), sqlAppender, walker );
renderColumnPath( definition.name(), definition.xpath(), sqlAppender, walker );
}
@Override
protected void renderXmlOrdinalityColumnDefinition(SqlAppender sqlAppender, XmlTableOrdinalityColumnDefinition definition, SqlAstTranslator<?> walker) {
sqlAppender.appendSql( definition.name() );
sqlAppender.appendSql( " bigint for ordinality" );
}
private static class SybaseASEXmlTableSetReturningFunctionTypeResolver extends XmlTableSetReturningFunctionTypeResolver {
@Override
public SelectableMapping[] resolveFunctionReturnType(
List<? extends SqlAstNode> sqlAstNodes,
String tableIdentifierVariable,
boolean lateral,
boolean withOrdinality,
SqmToSqlAstConverter converter) {
final XmlTableArguments arguments = XmlTableArguments.extract( sqlAstNodes );
final List<SelectableMapping> selectableMappings = new ArrayList<>( arguments.columnsClause().getColumnDefinitions().size() );
addSelectableMappings( selectableMappings, arguments, converter );
return selectableMappings.toArray( new SelectableMapping[0] );
}
protected void addSelectableMappings(List<SelectableMapping> selectableMappings, XmlTableArguments arguments, SqmToSqlAstConverter converter) {
for ( XmlTableColumnDefinition columnDefinition : arguments.columnsClause().getColumnDefinitions() ) {
if ( columnDefinition instanceof XmlTableQueryColumnDefinition definition ) {
addSelectableMappings( selectableMappings, definition, arguments, converter );
}
else if ( columnDefinition instanceof XmlTableValueColumnDefinition definition ) {
addSelectableMappings( selectableMappings, definition, converter );
}
else {
final XmlTableOrdinalityColumnDefinition definition
= (XmlTableOrdinalityColumnDefinition) columnDefinition;
addSelectableMappings( selectableMappings, definition, converter );
}
}
}
protected void addSelectableMappings(List<SelectableMapping> selectableMappings, XmlTableQueryColumnDefinition definition, XmlTableArguments arguments, SqmToSqlAstConverter converter) {
// Sybase ASE can't extract XML nodes via xmltable, so we select the ordinality instead and extract
// the XML nodes via xmlextract in select item. Unfortunately, this limits XPaths to literals
// and documents to columns or literals, since that is the only form that can be encoded in read expressions
final String documentFragment;
if ( arguments.xmlDocument() instanceof Literal documentLiteral ) {
documentFragment = documentLiteral.getJdbcMapping().getJdbcLiteralFormatter().toJdbcLiteral(
documentLiteral.getLiteralValue(),
converter.getCreationContext().getSessionFactory().getJdbcServices().getDialect(),
converter.getCreationContext().getSessionFactory().getWrapperOptions()
);
}
else if ( arguments.xmlDocument() instanceof ColumnReference columnReference ) {
documentFragment = columnReference.getExpressionText();
}
else {
throw new QueryException( "Sybase ASE only supports passing a literal or column reference as XML document for xmltable() when using query columns, but got: " + arguments.xmlDocument() );
}
if ( !( arguments.xpath() instanceof Literal literal)) {
throw new QueryException( "Sybase ASE only supports passing an XPath literal to xmltable() when using query columns, but got: " + arguments.xpath() );
}
final String xpathString = (String) literal.getLiteralValue();
final String definitionPath = definition.xpath() == null ? definition.name() : definition.xpath();
selectableMappings.add( new SelectableMappingImpl(
"",
definition.name(),
new SelectablePath( definition.name() ),
"xmlextract('" + xpathString + "['||cast(" + Template.TEMPLATE + "." + definition.name() + " as varchar)||']/" + definitionPath + "'," + documentFragment + ")",
null,
null,
null,
null,
null,
null,
false,
false,
false,
false,
false,
false,
converter.getCreationContext().getTypeConfiguration().getBasicTypeRegistry()
.resolve( String.class, SqlTypes.SQLXML )
));
}
@Override
protected void addSelectableMapping(List<SelectableMapping> selectableMappings, String name, JdbcMapping type, SqmToSqlAstConverter converter) {
if ( isBoolean( type ) ) {
//noinspection unchecked
final JdbcLiteralFormatter<Object> jdbcLiteralFormatter = type.getJdbcLiteralFormatter();
final SessionFactoryImplementor sessionFactory = converter.getCreationContext().getSessionFactory();
final Dialect dialect = sessionFactory.getJdbcServices().getDialect();
final WrapperOptions wrapperOptions = sessionFactory.getWrapperOptions();
final Object trueValue = type.convertToRelationalValue( true );
final Object falseValue = type.convertToRelationalValue( false );
final String trueFragment = jdbcLiteralFormatter.toJdbcLiteral( trueValue, dialect, wrapperOptions );
final String falseFragment = jdbcLiteralFormatter.toJdbcLiteral( falseValue, dialect, wrapperOptions );
selectableMappings.add( new SelectableMappingImpl(
"",
name,
new SelectablePath( name ),
"case " + Template.TEMPLATE + "." + name + " when 'true' then " + trueFragment + " when 'false' then " + falseFragment + " end",
null,
"varchar(5)",
null,
null,
null,
null,
false,
false,
false,
false,
false,
false,
type
));
}
else {
super.addSelectableMapping( selectableMappings, name, type, converter );
}
}
}
public static boolean isBoolean(JdbcMapping type) {
return type.getJavaTypeDescriptor().getJavaTypeClass() == Boolean.class;
}
}

View File

@ -0,0 +1,233 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.dialect.function.xml;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.hibernate.dialect.function.array.DdlTypeHelper;
import org.hibernate.query.derived.AnonymousTupleTableGroupProducer;
import org.hibernate.query.spi.QueryEngine;
import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingSetReturningFunctionDescriptor;
import org.hibernate.query.sqm.function.SelfRenderingSqmSetReturningFunction;
import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator;
import org.hibernate.query.sqm.produce.function.FunctionParameterType;
import org.hibernate.query.sqm.produce.function.SetReturningFunctionTypeResolver;
import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators;
import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers;
import org.hibernate.query.sqm.tree.SqmTypedNode;
import org.hibernate.query.sqm.tree.expression.SqmExpression;
import org.hibernate.query.sqm.tree.expression.SqmXmlTableFunction;
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
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.CastTarget;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.sql.ast.tree.expression.XmlTableColumnDefinition;
import org.hibernate.sql.ast.tree.expression.XmlTableColumnsClause;
import org.hibernate.sql.ast.tree.expression.XmlTableOrdinalityColumnDefinition;
import org.hibernate.sql.ast.tree.expression.XmlTableQueryColumnDefinition;
import org.hibernate.sql.ast.tree.expression.XmlTableValueColumnDefinition;
import org.hibernate.type.spi.TypeConfiguration;
import java.util.List;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.XML;
/**
* Standard xmltable function.
*/
public class XmlTableFunction extends AbstractSqmSelfRenderingSetReturningFunctionDescriptor {
protected final boolean supportsParametersInDefault;
public XmlTableFunction(boolean supportsParametersInDefault, TypeConfiguration typeConfiguration) {
this(
supportsParametersInDefault,
new XmlTableSetReturningFunctionTypeResolver(),
typeConfiguration
);
}
protected XmlTableFunction(boolean supportsParametersInDefault, SetReturningFunctionTypeResolver setReturningFunctionTypeResolver, TypeConfiguration typeConfiguration) {
super(
"xmltable",
new ArgumentTypesValidator(
StandardArgumentsValidators.exactly( 2 ),
FunctionParameterType.STRING,
FunctionParameterType.IMPLICIT_XML
),
setReturningFunctionTypeResolver,
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, STRING, XML )
);
this.supportsParametersInDefault = supportsParametersInDefault;
}
@Override
protected <T> SelfRenderingSqmSetReturningFunction<T> generateSqmSetReturningFunctionExpression(List<? extends SqmTypedNode<?>> arguments, QueryEngine queryEngine) {
//noinspection unchecked
return new SqmXmlTableFunction<>(
this,
this,
getArgumentsValidator(),
getSetReturningTypeResolver(),
queryEngine.getCriteriaBuilder(),
(SqmExpression<String>) arguments.get( 0 ),
(SqmExpression<?>) arguments.get( 1 )
);
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> sqlAstArguments,
AnonymousTupleTableGroupProducer tupleType,
String tableIdentifierVariable,
SqlAstTranslator<?> walker) {
renderXmlTable( sqlAppender, XmlTableArguments.extract( sqlAstArguments ), tupleType, tableIdentifierVariable, walker );
}
protected void renderXmlTable(
SqlAppender sqlAppender,
XmlTableArguments arguments,
AnonymousTupleTableGroupProducer tupleType,
String tableIdentifierVariable,
SqlAstTranslator<?> walker) {
sqlAppender.appendSql( "xmltable(" );
arguments.xpath().accept( walker );
sqlAppender.appendSql( " passing " );
if ( !arguments.isXmlType() ) {
sqlAppender.appendSql( "xmlparse(document " );
}
arguments.xmlDocument().accept( walker );
if ( !arguments.isXmlType() ) {
sqlAppender.appendSql( ')' );
}
renderColumns( sqlAppender, arguments.columnsClause(), walker );
sqlAppender.appendSql( ')' );
}
protected String determineColumnType(CastTarget castTarget, SqlAstTranslator<?> walker) {
return determineColumnType( castTarget, walker.getSessionFactory().getTypeConfiguration() );
}
protected static String determineColumnType(CastTarget castTarget, TypeConfiguration typeConfiguration) {
final String columnDefinition = castTarget.getColumnDefinition();
if ( columnDefinition != null ) {
return columnDefinition;
}
else {
final String typeName = DdlTypeHelper.getTypeName(
castTarget.getJdbcMapping(),
castTarget.toSize(),
typeConfiguration
);
final int parenthesisIndex = typeName.indexOf( '(' );
if ( parenthesisIndex != -1 && typeName.charAt( parenthesisIndex + 1 ) == '$' ) {
// Remove length/precision and scale arguments if it contains unresolved variables
return typeName.substring( 0, parenthesisIndex );
}
else {
return typeName;
}
}
}
protected void renderColumns(SqlAppender sqlAppender, XmlTableColumnsClause xmlTableColumnsClause, SqlAstTranslator<?> walker) {
sqlAppender.appendSql( " columns" );
char separator = ' ';
for ( XmlTableColumnDefinition columnDefinition : xmlTableColumnsClause.getColumnDefinitions() ) {
sqlAppender.appendSql( separator );
if ( columnDefinition instanceof XmlTableQueryColumnDefinition definition ) {
renderXmlQueryColumnDefinition( sqlAppender, definition, walker );
}
else if ( columnDefinition instanceof XmlTableValueColumnDefinition definition ) {
renderXmlValueColumnDefinition( sqlAppender, definition, walker );
}
else {
renderXmlOrdinalityColumnDefinition(
sqlAppender,
(XmlTableOrdinalityColumnDefinition) columnDefinition,
walker
);
}
separator = ',';
}
}
protected void renderXmlOrdinalityColumnDefinition(SqlAppender sqlAppender, XmlTableOrdinalityColumnDefinition definition, SqlAstTranslator<?> walker) {
sqlAppender.appendSql( definition.name() );
sqlAppender.appendSql( " for ordinality" );
}
protected void renderXmlValueColumnDefinition(SqlAppender sqlAppender, XmlTableValueColumnDefinition definition, SqlAstTranslator<?> walker) {
sqlAppender.appendSql( definition.name() );
sqlAppender.appendSql( ' ' );
sqlAppender.appendSql( determineColumnType( definition.type(), walker ) );
renderColumnPath( definition.name(), definition.xpath(), sqlAppender, walker );
renderDefaultExpression( definition.defaultExpression(), sqlAppender, walker );
}
protected void renderColumnPath(String name, @Nullable String xpath, SqlAppender sqlAppender, SqlAstTranslator<?> walker) {
if ( xpath != null ) {
sqlAppender.appendSql( " path " );
sqlAppender.appendSingleQuoteEscapedString( xpath );
}
else {
// To avoid case sensitivity issues, just pass the path always
sqlAppender.appendSql( " path " );
sqlAppender.appendSingleQuoteEscapedString( name );
}
}
protected void renderDefaultExpression(@Nullable Expression expression, SqlAppender sqlAppender, SqlAstTranslator<?> walker) {
if ( expression != null ) {
sqlAppender.appendSql( " default " );
if ( supportsParametersInDefault ) {
expression.accept( walker );
}
else {
walker.render( expression, SqlAstNodeRenderingMode.INLINE_PARAMETERS );
}
}
}
protected void renderXmlQueryColumnDefinition(SqlAppender sqlAppender, XmlTableQueryColumnDefinition definition, SqlAstTranslator<?> walker) {
sqlAppender.appendSql( definition.name() );
sqlAppender.appendSql( ' ' );
sqlAppender.appendSql( determineColumnType( new CastTarget( definition.type() ), walker ) );
renderColumnPath( definition.name(), definition.xpath(), sqlAppender, walker );
renderDefaultExpression( definition.defaultExpression(), sqlAppender, walker );
}
protected record XmlTableArguments(
Expression xpath,
Expression xmlDocument,
boolean isXmlType,
XmlTableColumnsClause columnsClause
){
public static XmlTableArguments extract(List<? extends SqlAstNode> sqlAstArguments) {
final Expression xpath = (Expression) sqlAstArguments.get( 0 );
final Expression xmlDocument = (Expression) sqlAstArguments.get( 1 );
XmlTableColumnsClause columnsClause = null;
int nextIndex = 2;
if ( nextIndex < sqlAstArguments.size() ) {
final SqlAstNode node = sqlAstArguments.get( nextIndex );
if ( node instanceof XmlTableColumnsClause ) {
columnsClause = (XmlTableColumnsClause) node;
}
}
return new XmlTableArguments(
xpath,
xmlDocument,
xmlDocument.getExpressionType() != null
&& xmlDocument.getExpressionType().getSingleJdbcMapping().getJdbcType().isXml(),
columnsClause
);
}
}
}

View File

@ -0,0 +1,124 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.dialect.function.xml;
import org.hibernate.metamodel.mapping.JdbcMapping;
import org.hibernate.metamodel.mapping.SelectableMapping;
import org.hibernate.metamodel.mapping.SelectablePath;
import org.hibernate.metamodel.mapping.internal.SelectableMappingImpl;
import org.hibernate.query.derived.AnonymousTupleType;
import org.hibernate.query.sqm.produce.function.SetReturningFunctionTypeResolver;
import org.hibernate.query.sqm.sql.SqmToSqlAstConverter;
import org.hibernate.query.sqm.tree.SqmTypedNode;
import org.hibernate.query.sqm.tree.expression.SqmXmlTableFunction;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.sql.ast.tree.expression.XmlTableColumnDefinition;
import org.hibernate.sql.ast.tree.expression.XmlTableColumnsClause;
import org.hibernate.sql.ast.tree.expression.XmlTableOrdinalityColumnDefinition;
import org.hibernate.sql.ast.tree.expression.XmlTableQueryColumnDefinition;
import org.hibernate.sql.ast.tree.expression.XmlTableValueColumnDefinition;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.spi.TypeConfiguration;
import java.util.ArrayList;
import java.util.List;
/**
*
* @since 7.0
*/
public class XmlTableSetReturningFunctionTypeResolver implements SetReturningFunctionTypeResolver {
@Override
public AnonymousTupleType<?> resolveTupleType(List<? extends SqmTypedNode<?>> arguments, TypeConfiguration typeConfiguration) {
final SqmXmlTableFunction.Columns columns = (SqmXmlTableFunction.Columns) arguments.get( arguments.size() - 1 );
return columns.createTupleType();
}
@Override
public SelectableMapping[] resolveFunctionReturnType(
List<? extends SqlAstNode> arguments,
String tableIdentifierVariable,
boolean lateral,
boolean withOrdinality,
SqmToSqlAstConverter converter) {
XmlTableColumnsClause columnsClause = null;
for ( SqlAstNode argument : arguments ) {
if ( argument instanceof XmlTableColumnsClause ) {
columnsClause = (XmlTableColumnsClause) argument;
break;
}
}
assert columnsClause != null;
final List<XmlTableColumnDefinition> columnDefinitions = columnsClause.getColumnDefinitions();
final List<SelectableMapping> selectableMappings = new ArrayList<>( columnDefinitions.size() );
addSelectableMappings( selectableMappings, columnsClause, converter );
return selectableMappings.toArray( new SelectableMapping[0] );
}
protected void addSelectableMappings(List<SelectableMapping> selectableMappings, XmlTableColumnsClause columnsClause, SqmToSqlAstConverter converter) {
for ( XmlTableColumnDefinition columnDefinition : columnsClause.getColumnDefinitions() ) {
if ( columnDefinition instanceof XmlTableQueryColumnDefinition definition ) {
addSelectableMappings( selectableMappings, definition, converter );
}
else if ( columnDefinition instanceof XmlTableValueColumnDefinition definition ) {
addSelectableMappings( selectableMappings, definition, converter );
}
else {
final XmlTableOrdinalityColumnDefinition definition
= (XmlTableOrdinalityColumnDefinition) columnDefinition;
addSelectableMappings( selectableMappings, definition, converter );
}
}
}
protected void addSelectableMappings(List<SelectableMapping> selectableMappings, XmlTableOrdinalityColumnDefinition definition, SqmToSqlAstConverter converter) {
addSelectableMapping(
selectableMappings,
definition.name(),
converter.getCreationContext().getTypeConfiguration().getBasicTypeForJavaType( Long.class ),
converter );
}
protected void addSelectableMappings(List<SelectableMapping> selectableMappings, XmlTableValueColumnDefinition definition, SqmToSqlAstConverter converter) {
addSelectableMapping(
selectableMappings,
definition.name(),
definition.type().getJdbcMapping(),
converter );
}
protected void addSelectableMappings(List<SelectableMapping> selectableMappings, XmlTableQueryColumnDefinition definition, SqmToSqlAstConverter converter) {
addSelectableMapping(
selectableMappings,
definition.name(),
converter.getCreationContext().getTypeConfiguration().getBasicTypeRegistry()
.resolve( String.class, SqlTypes.SQLXML ),
converter );
}
protected void addSelectableMapping(List<SelectableMapping> selectableMappings, String name, JdbcMapping type, SqmToSqlAstConverter converter) {
selectableMappings.add( new SelectableMappingImpl(
"",
name,
new SelectablePath( name ),
null,
null,
null,
null,
null,
null,
null,
false,
false,
false,
false,
false,
false,
type
));
}
}

View File

@ -4463,6 +4463,26 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder {
@Incubating
JpaJsonTableFunction jsonTable(Expression<?> jsonDocument, Expression<String> jsonPath);
/**
* Creates a {@code xmltable} function expression to generate rows from XML elements.
*
* @since 7.0
* @see JpaSelectCriteria#from(JpaSetReturningFunction)
* @see JpaFrom#join(JpaSetReturningFunction)
*/
@Incubating
JpaXmlTableFunction xmlTable(String xpath, Expression<?> xmlDocument);
/**
* Creates a {@code xmltable} function expression to generate rows from XML elements.
*
* @since 7.0
* @see JpaSelectCriteria#from(JpaSetReturningFunction)
* @see JpaFrom#join(JpaSetReturningFunction)
*/
@Incubating
JpaXmlTableFunction xmlTable(Expression<String> xpath, Expression<?> xmlDocument);
@Override
JpaPredicate and(List<Predicate> restrictions);

View File

@ -0,0 +1,30 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.query.criteria;
import jakarta.persistence.criteria.Expression;
import org.hibernate.Incubating;
/**
* A special node for column defined for a {@code xmltable} function.
* @since 7.0
*/
@Incubating
public interface JpaXmlTableColumnNode<T> {
/**
* Specifies the default value to use if resolving the XPath expression doesn't produce results.
*
* @return {@code this} for method chaining
*/
JpaXmlTableColumnNode<T> defaultValue(T value);
/**
* Specifies the default value to use if resolving the XPath expression doesn't produce results.
*
* @return {@code this} for method chaining
*/
JpaXmlTableColumnNode<T> defaultExpression(Expression<T> expression);
}

View File

@ -0,0 +1,67 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.query.criteria;
import org.hibernate.Incubating;
/**
* A special expression for the {@code xmltable} function.
* @since 7.0
*/
@Incubating
public interface JpaXmlTableFunction {
/**
* Like {@link #queryColumn(String, String)}, but uses the column name as XPath expression.
*
* @return The {@link JpaXmlTableColumnNode} for the column
*/
JpaXmlTableColumnNode<String> queryColumn(String columnName);
/**
* Defines a string column on the result type with the given name for which the value can be obtained
* by evaluating {@code xmlquery} with the given XPath expression on the XML document.
*
* @return The {@link JpaXmlTableColumnNode} for the column
*/
JpaXmlTableColumnNode<String> queryColumn(String columnName, String xpath);
/**
* Like {@link #valueColumn(String, Class, String)} but uses the column name as XPath expression.
*
* @return The {@link JpaXmlTableColumnNode} for the column
*/
<X> JpaXmlTableColumnNode<X> valueColumn(String columnName, Class<X> type);
/**
* Like {@link #valueColumn(String, JpaCastTarget, String)} but uses the column name as XPath expression.
*
* @return The {@link JpaXmlTableColumnNode} for the column
*/
<X> JpaXmlTableColumnNode<X> valueColumn(String columnName, JpaCastTarget<X> castTarget);
/**
* Like {@link #valueColumn(String, JpaCastTarget, String)}, but converting the {@link Class}
* to {@link JpaCastTarget} via {@link HibernateCriteriaBuilder#castTarget(Class)}.
*
* @return The {@link JpaXmlTableColumnNode} for the column
*/
<X> JpaXmlTableColumnNode<X> valueColumn(String columnName, Class<X> type, String xpath);
/**
* Defines an column on the result type with the given name and type for which the value can be obtained by the given XPath path expression.
*
* @return The {@link JpaXmlTableColumnNode} for the column
*/
<X> JpaXmlTableColumnNode<X> valueColumn(String columnName, JpaCastTarget<X> castTarget, String xpath);
/**
* Defines a long column on the result type with the given name which is set to the ordinality i.e.
* the 1-based position of the processed element. Ordinality starts again at 1 within nested paths.
*
* @return {@code this} for method chaining
*/
JpaXmlTableFunction ordinalityColumn(String columnName);
}

View File

@ -3878,4 +3878,16 @@ public class HibernateCriteriaBuilderDelegate implements HibernateCriteriaBuilde
public JpaJsonTableFunction jsonTable(Expression<?> jsonDocument, Expression<String> jsonPath) {
return criteriaBuilder.jsonTable( jsonDocument, jsonPath );
}
@Incubating
@Override
public JpaXmlTableFunction xmlTable(String xpath, Expression<?> xmlDocument) {
return criteriaBuilder.xmlTable( xpath, xmlDocument );
}
@Incubating
@Override
public JpaXmlTableFunction xmlTable(Expression<String> xpath, Expression<?> xmlDocument) {
return criteriaBuilder.xmlTable( xpath, xmlDocument );
}
}

View File

@ -69,6 +69,7 @@ import org.hibernate.query.criteria.JpaJsonTableColumnsNode;
import org.hibernate.query.criteria.JpaJsonValueNode;
import org.hibernate.query.criteria.JpaRoot;
import org.hibernate.query.criteria.JpaSearchOrder;
import org.hibernate.query.criteria.JpaXmlTableColumnNode;
import org.hibernate.query.derived.AnonymousTupleType;
import org.hibernate.query.hql.HqlLogging;
import org.hibernate.query.hql.spi.DotIdentifierConsumer;
@ -3204,6 +3205,64 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
);
}
@Override
public Object visitXmltableFunction(HqlParser.XmltableFunctionContext ctx) {
checkXmlFunctionsEnabled( ctx );
final List<HqlParser.ExpressionContext> argumentsContexts = ctx.expression();
//noinspection unchecked
final SqmExpression<String> xpath = (SqmExpression<String>) argumentsContexts.get( 0 ).accept( this );
final SqmExpression<?> document = (SqmExpression<?>) argumentsContexts.get( 1 ).accept( this );
final SqmXmlTableFunction<?> xmlTable = creationContext.getNodeBuilder().xmlTable( xpath, document);
visitColumns( xmlTable, ctx.xmltableColumnsClause().xmltableColumn() );
return xmlTable;
}
private void visitColumns(SqmXmlTableFunction<?> xmlTable, List<HqlParser.XmltableColumnContext> columnContexts) {
for ( HqlParser.XmltableColumnContext columnContext : columnContexts ) {
if ( columnContext instanceof HqlParser.XmlTableQueryColumnContext queryColumnContext ) {
final String columnName = visitIdentifier( queryColumnContext.identifier() );
final TerminalNode pathNode = queryColumnContext.STRING_LITERAL();
final String xpath;
if ( pathNode == null ) {
xpath = null;
}
else {
xpath = unquoteStringLiteral( pathNode.getText() );
}
final JpaXmlTableColumnNode<String> node = xmlTable.queryColumn( columnName, xpath );
final HqlParser.XmltableDefaultClauseContext defaultClause = queryColumnContext.xmltableDefaultClause();
if ( defaultClause != null ) {
//noinspection unchecked
node.defaultExpression( (Expression<String>) defaultClause.expression().accept( this ) );
}
}
else if ( columnContext instanceof HqlParser.XmlTableValueColumnContext valueColumnContext ) {
final String columnName = visitIdentifier( valueColumnContext.identifier() );
//noinspection unchecked
final SqmCastTarget<Object> castTarget = (SqmCastTarget<Object>) visitCastTarget( valueColumnContext.castTarget() );
final TerminalNode pathNode = valueColumnContext.STRING_LITERAL();
final String xpath;
if ( pathNode == null ) {
xpath = null;
}
else {
xpath = unquoteStringLiteral( pathNode.getText() );
}
final JpaXmlTableColumnNode<Object> node = xmlTable.valueColumn( columnName, castTarget, xpath );
final HqlParser.XmltableDefaultClauseContext defaultClause = valueColumnContext.xmltableDefaultClause();
if ( defaultClause != null ) {
//noinspection unchecked
node.defaultExpression( (Expression<Object>) defaultClause.expression().accept( this ) );
}
}
else {
final HqlParser.XmlTableOrdinalityColumnContext ordinalityColumnContext
= (HqlParser.XmlTableOrdinalityColumnContext) columnContext;
xmlTable.ordinalityColumn( visitIdentifier( ordinalityColumnContext.identifier() ) );
}
}
}
private void checkXmlFunctionsEnabled(ParserRuleContext ctx) {
if ( !creationOptions.isXmlFunctionsEnabled() ) {
throw new SemanticException(

View File

@ -54,6 +54,7 @@ import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression;
import org.hibernate.query.sqm.tree.expression.SqmSetReturningFunction;
import org.hibernate.query.sqm.tree.expression.SqmTuple;
import org.hibernate.query.sqm.tree.expression.SqmXmlElementExpression;
import org.hibernate.query.sqm.tree.expression.SqmXmlTableFunction;
import org.hibernate.query.sqm.tree.from.SqmRoot;
import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement;
import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement;
@ -885,6 +886,12 @@ public interface NodeBuilder extends HibernateCriteriaBuilder, BindingContext {
@Override
SqmJsonTableFunction<?> jsonTable(Expression<?> jsonDocument, Expression<String> jsonPath);
@Override
SqmXmlTableFunction<?> xmlTable(String xpath, Expression<?> xmlDocument);
@Override
SqmXmlTableFunction<?> xmlTable(Expression<String> xpath, Expression<?> xmlDocument);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Covariant overrides

View File

@ -5956,7 +5956,7 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable {
}
@Override
public SqmJsonTableFunction<?> jsonTable(Expression<?> jsonDocument, Expression<String> jsonPath) {
public SqmJsonTableFunction<?> jsonTable(Expression<?> jsonDocument, @Nullable Expression<String> jsonPath) {
return (SqmJsonTableFunction<?>) getSetReturningFunctionDescriptor( "json_table" ).generateSqmExpression(
jsonPath == null
? asList( (SqmTypedNode<?>) jsonDocument )
@ -5964,4 +5964,17 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable {
queryEngine
);
}
@Override
public SqmXmlTableFunction<?> xmlTable(String xpath, Expression<?> xmlDocument) {
return xmlTable( value( xpath ), xmlDocument );
}
@Override
public SqmXmlTableFunction<?> xmlTable(Expression<String> xpath, Expression<?> xmlDocument) {
return (SqmXmlTableFunction<?>) getSetReturningFunctionDescriptor( "xmltable" ).generateSqmExpression(
asList( (SqmTypedNode<?>) xpath, (SqmTypedNode<?>) xmlDocument ),
queryEngine
);
}
}

View File

@ -94,7 +94,7 @@ public final class StandardFunctionArgumentTypeResolvers {
return new AbstractFunctionArgumentTypeResolver() {
@Override
public @Nullable MappingModelExpressible<?> resolveFunctionArgumentType(List<? extends SqmTypedNode<?>> arguments, int argumentIndex, SqmToSqlAstConverter converter) {
return expressibles[argumentIndex];
return argumentIndex < expressibles.length ? expressibles[argumentIndex] : null;
}
};
}
@ -103,6 +103,9 @@ public final class StandardFunctionArgumentTypeResolvers {
return new AbstractFunctionArgumentTypeResolver() {
@Override
public @Nullable MappingModelExpressible<?> resolveFunctionArgumentType(List<? extends SqmTypedNode<?>> arguments, int argumentIndex, SqmToSqlAstConverter converter) {
if ( argumentIndex >= types.length ) {
return null;
}
return getMappingModelExpressible(
converter.getCreationContext().getTypeConfiguration(),
types[argumentIndex]
@ -188,7 +191,9 @@ public final class StandardFunctionArgumentTypeResolvers {
return new AbstractFunctionArgumentTypeResolver() {
@Override
public @Nullable MappingModelExpressible<?> resolveFunctionArgumentType(List<? extends SqmTypedNode<?>> arguments, int argumentIndex, SqmToSqlAstConverter converter) {
return resolvers[argumentIndex].resolveFunctionArgumentType( arguments, argumentIndex, converter );
return argumentIndex < resolvers.length
? resolvers[argumentIndex].resolveFunctionArgumentType( arguments, argumentIndex, converter )
: null;
}
};
}

View File

@ -0,0 +1,486 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.query.sqm.tree.expression;
import jakarta.persistence.criteria.Expression;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.hibernate.internal.util.QuotingHelper;
import org.hibernate.query.criteria.JpaCastTarget;
import org.hibernate.query.criteria.JpaXmlTableColumnNode;
import org.hibernate.query.criteria.JpaXmlTableFunction;
import org.hibernate.query.derived.AnonymousTupleType;
import org.hibernate.query.sqm.NodeBuilder;
import org.hibernate.query.sqm.SemanticQueryWalker;
import org.hibernate.query.sqm.SqmExpressible;
import org.hibernate.query.sqm.function.SelfRenderingSqmSetReturningFunction;
import org.hibernate.query.sqm.function.SetReturningFunctionRenderer;
import org.hibernate.query.sqm.function.SqmSetReturningFunctionDescriptor;
import org.hibernate.query.sqm.produce.function.ArgumentsValidator;
import org.hibernate.query.sqm.produce.function.SetReturningFunctionTypeResolver;
import org.hibernate.query.sqm.sql.SqmToSqlAstConverter;
import org.hibernate.query.sqm.tree.SqmCopyContext;
import org.hibernate.query.sqm.tree.SqmTypedNode;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.sql.ast.tree.expression.CastTarget;
import org.hibernate.sql.ast.tree.expression.XmlTableColumnDefinition;
import org.hibernate.sql.ast.tree.expression.XmlTableColumnsClause;
import org.hibernate.sql.ast.tree.expression.XmlTableOrdinalityColumnDefinition;
import org.hibernate.sql.ast.tree.expression.XmlTableQueryColumnDefinition;
import org.hibernate.sql.ast.tree.expression.XmlTableValueColumnDefinition;
import org.hibernate.type.BasicType;
import org.hibernate.type.SqlTypes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @since 7.0
*/
public class SqmXmlTableFunction<T> extends SelfRenderingSqmSetReturningFunction<T> implements JpaXmlTableFunction {
private final Columns columns;
public SqmXmlTableFunction(
SqmSetReturningFunctionDescriptor descriptor,
SetReturningFunctionRenderer renderer,
@Nullable ArgumentsValidator argumentsValidator,
SetReturningFunctionTypeResolver setReturningTypeResolver,
NodeBuilder nodeBuilder,
SqmExpression<String> xpath,
SqmExpression<?> document) {
this(
descriptor,
renderer,
Arrays.asList( xpath, document, null ),
argumentsValidator,
setReturningTypeResolver,
nodeBuilder,
new ArrayList<>()
);
}
private SqmXmlTableFunction(
SqmSetReturningFunctionDescriptor descriptor,
SetReturningFunctionRenderer renderer,
List<SqmTypedNode<?>> arguments,
@Nullable ArgumentsValidator argumentsValidator,
SetReturningFunctionTypeResolver setReturningTypeResolver,
NodeBuilder nodeBuilder,
ArrayList<ColumnDefinition> columnDefinitions) {
super( descriptor, renderer, arguments, argumentsValidator, setReturningTypeResolver, nodeBuilder, "xmltable" );
this.columns = new Columns( this, columnDefinitions );
arguments.set( arguments.size() - 1, this.columns );
}
@Override
public SqmXmlTableFunction<T> copy(SqmCopyContext context) {
final SqmXmlTableFunction<T> existing = context.getCopy( this );
if ( existing != null ) {
return existing;
}
final List<? extends SqmTypedNode<?>> arguments = getArguments();
final List<SqmTypedNode<?>> argumentsCopy = new ArrayList<>( arguments.size() );
for ( int i = 0; i < arguments.size() - 1; i++ ) {
argumentsCopy.add( arguments.get( i ).copy( context ) );
}
final SqmXmlTableFunction<T> tableFunction = new SqmXmlTableFunction<>(
getFunctionDescriptor(),
getFunctionRenderer(),
argumentsCopy,
getArgumentsValidator(),
getSetReturningTypeResolver(),
nodeBuilder(),
columns.columnDefinitions
);
context.registerCopy( this, tableFunction );
tableFunction.columns.columnDefinitions.ensureCapacity( columns.columnDefinitions.size() );
for ( ColumnDefinition columnDefinition : columns.columnDefinitions ) {
tableFunction.columns.columnDefinitions.add( columnDefinition.copy( context ) );
}
return tableFunction;
}
@Override
protected List<SqlAstNode> resolveSqlAstArguments(List<? extends SqmTypedNode<?>> sqmArguments, SqmToSqlAstConverter walker) {
final List<SqlAstNode> sqlAstNodes = super.resolveSqlAstArguments( sqmArguments, walker );
// The last argument is the SqmXmlTableFunction.Columns which will convert to null, so remove that
sqlAstNodes.remove( sqlAstNodes.size() - 1 );
final List<XmlTableColumnDefinition> definitions = new ArrayList<>( columns.columnDefinitions.size() );
for ( ColumnDefinition columnDefinition : columns.columnDefinitions ) {
definitions.add( columnDefinition.convertToSqlAst( walker ) );
}
sqlAstNodes.add( new XmlTableColumnsClause( definitions ) );
return sqlAstNodes;
}
@Override
public JpaXmlTableColumnNode<String> queryColumn(String columnName) {
return queryColumn( columnName, null );
}
@Override
public JpaXmlTableColumnNode<String> queryColumn(String columnName, @Nullable String xpath) {
final QueryColumnDefinition definition = new QueryColumnDefinition(
this,
columnName,
nodeBuilder().getTypeConfiguration().getBasicTypeRegistry().resolve( String.class, SqlTypes.SQLXML ),
xpath
);
columns.addColumn( definition );
return definition;
}
@Override
public <X> JpaXmlTableColumnNode<X> valueColumn(String columnName, Class<X> type) {
return valueColumn( columnName, type, null );
}
@Override
public <X> JpaXmlTableColumnNode<X> valueColumn(String columnName, JpaCastTarget<X> castTarget) {
return valueColumn( columnName, castTarget, null );
}
@Override
public <X> JpaXmlTableColumnNode<X> valueColumn(String columnName, Class<X> type, String xpath) {
return valueColumn( columnName, nodeBuilder().castTarget( type ), xpath );
}
@Override
public <X> JpaXmlTableColumnNode<X> valueColumn(String columnName, JpaCastTarget<X> castTarget, @Nullable String xpath) {
final ValueColumnDefinition<X> definition = new ValueColumnDefinition<>(
this,
columnName,
(SqmCastTarget<X>) castTarget,
xpath
);
columns.addColumn( definition );
return definition;
}
@Override
public SqmXmlTableFunction<T> ordinalityColumn(String columnName) {
columns.addColumn( new OrdinalityColumnDefinition( columnName, nodeBuilder().getLongType() ) );
return this;
}
@Override
public void appendHqlString(StringBuilder sb) {
sb.append( "xmltable(" );
getArguments().get( 0 ).appendHqlString( sb );
sb.append( " passing " );
getArguments().get( 1 ).appendHqlString( sb );
columns.appendHqlString( sb );
sb.append( ')' );
}
private void checkTypeResolved() {
if ( isTypeResolved() ) {
throw new IllegalStateException(
"Type for xmltable function is already resolved. Mutation is not allowed anymore" );
}
}
sealed interface ColumnDefinition {
String name();
ColumnDefinition copy(SqmCopyContext context);
XmlTableColumnDefinition convertToSqlAst(SqmToSqlAstConverter walker);
void appendHqlString(StringBuilder sb);
int populateTupleType(int offset, String[] componentNames, SqmExpressible<?>[] componentTypes);
}
static final class QueryColumnDefinition implements ColumnDefinition, JpaXmlTableColumnNode<String> {
private final SqmXmlTableFunction<?> table;
private final String name;
private final BasicType<String> type;
private final @Nullable String xpath;
private @Nullable SqmExpression<String> defaultExpression;
QueryColumnDefinition(SqmXmlTableFunction<?> table, String name, BasicType<String> type, @Nullable String xpath) {
this.table = table;
this.name = name;
this.type = type;
this.xpath = xpath;
}
private QueryColumnDefinition(SqmXmlTableFunction<?> table, String name, BasicType<String> type, @Nullable String xpath, @Nullable SqmExpression<String> defaultExpression) {
this.table = table;
this.name = name;
this.type = type;
this.xpath = xpath;
this.defaultExpression = defaultExpression;
}
@Override
public ColumnDefinition copy(SqmCopyContext context) {
return new QueryColumnDefinition(
table.copy( context ),
name,
type,
xpath,
defaultExpression == null ? null : defaultExpression.copy( context )
);
}
@Override
public XmlTableColumnDefinition convertToSqlAst(SqmToSqlAstConverter walker) {
return new XmlTableQueryColumnDefinition(
name,
type,
xpath,
defaultExpression == null
? null
: (org.hibernate.sql.ast.tree.expression.Expression) defaultExpression.accept( walker )
);
}
@Override
public JpaXmlTableColumnNode<String> defaultValue(String value) {
return defaultExpression( table.nodeBuilder().value( value ) );
}
@Override
public JpaXmlTableColumnNode<String> defaultExpression(Expression<String> expression) {
table.checkTypeResolved();
this.defaultExpression = (SqmExpression<String>) expression;
return this;
}
@Override
public void appendHqlString(StringBuilder sb) {
sb.append( name );
sb.append( " xml" );
if ( xpath != null ) {
sb.append( " path " );
QuotingHelper.appendSingleQuoteEscapedString( sb, xpath );
}
if ( defaultExpression != null ) {
sb.append( " default " );
defaultExpression.appendHqlString( sb );
}
}
@Override
public int populateTupleType(int offset, String[] componentNames, SqmExpressible<?>[] componentTypes) {
componentNames[offset] = name;
componentTypes[offset] = type;
return 1;
}
@Override
public String name() {
return name;
}
}
static final class ValueColumnDefinition<X> implements ColumnDefinition, JpaXmlTableColumnNode<X> {
private final SqmXmlTableFunction<?> table;
private final String name;
private final SqmCastTarget<X> type;
private final @Nullable String xpath;
private @Nullable SqmExpression<X> defaultExpression;
ValueColumnDefinition(SqmXmlTableFunction<?> table, String name, SqmCastTarget<X> type, @Nullable String xpath) {
this.table = table;
this.name = name;
this.type = type;
this.xpath = xpath;
}
private ValueColumnDefinition(SqmXmlTableFunction<?> table, String name, SqmCastTarget<X> type, @Nullable String xpath, @Nullable SqmExpression<X> defaultExpression) {
this.table = table;
this.name = name;
this.type = type;
this.xpath = xpath;
this.defaultExpression = defaultExpression;
}
@Override
public ColumnDefinition copy(SqmCopyContext context) {
return new ValueColumnDefinition<>(
table.copy( context ),
name,
type,
xpath,
defaultExpression == null ? null : defaultExpression.copy( context )
);
}
@Override
public XmlTableColumnDefinition convertToSqlAst(SqmToSqlAstConverter walker) {
return new XmlTableValueColumnDefinition(
name,
(CastTarget) type.accept( walker ),
xpath,
defaultExpression == null
? null
: (org.hibernate.sql.ast.tree.expression.Expression) defaultExpression.accept( walker )
);
}
@Override
public JpaXmlTableColumnNode<X> defaultValue(X value) {
return defaultExpression( table.nodeBuilder().value( value ) );
}
@Override
public JpaXmlTableColumnNode<X> defaultExpression(Expression<X> expression) {
table.checkTypeResolved();
this.defaultExpression = (SqmExpression<X>) expression;
return this;
}
@Override
public void appendHqlString(StringBuilder sb) {
sb.append( name );
sb.append( ' ' );
type.appendHqlString( sb );
if ( xpath != null ) {
sb.append( " path " );
QuotingHelper.appendSingleQuoteEscapedString( sb, xpath );
}
if ( defaultExpression != null ) {
sb.append( " default " );
defaultExpression.appendHqlString( sb );
}
}
@Override
public int populateTupleType(int offset, String[] componentNames, SqmExpressible<?>[] componentTypes) {
componentNames[offset] = name;
componentTypes[offset] = type.getNodeType();
return 1;
}
@Override
public String name() {
return name;
}
}
record OrdinalityColumnDefinition(String name, BasicType<Long> type) implements ColumnDefinition {
@Override
public ColumnDefinition copy(SqmCopyContext context) {
return this;
}
@Override
public XmlTableColumnDefinition convertToSqlAst(SqmToSqlAstConverter walker) {
return new XmlTableOrdinalityColumnDefinition( name );
}
@Override
public void appendHqlString(StringBuilder sb) {
sb.append( name );
sb.append( " for ordinality" );
}
@Override
public int populateTupleType(int offset, String[] componentNames, SqmExpressible<?>[] componentTypes) {
componentNames[offset] = name;
componentTypes[offset] = type;
return 1;
}
}
public static final class Columns implements SqmTypedNode<Object> {
private final SqmXmlTableFunction<?> table;
private final Set<String> columnNames;
private final ArrayList<ColumnDefinition> columnDefinitions;
private Columns(SqmXmlTableFunction<?> table, ArrayList<ColumnDefinition> columnDefinitions) {
this.table = table;
this.columnDefinitions = columnDefinitions;
this.columnNames = new HashSet<>( columnDefinitions.size() );
for ( ColumnDefinition columnDefinition : columnDefinitions ) {
columnNames.add( columnDefinition.name() );
}
}
public AnonymousTupleType<?> createTupleType() {
if ( columnDefinitions.isEmpty() ) {
throw new IllegalArgumentException( "Couldn't determine types of columns of function 'xmltable'" );
}
final SqmExpressible<?>[] componentTypes = new SqmExpressible<?>[columnDefinitions.size()];
final String[] componentNames = new String[columnDefinitions.size()];
int offset = 0;
for ( ColumnDefinition columnDefinition : columnDefinitions ) {
offset += columnDefinition.populateTupleType( offset, componentNames, componentTypes );
}
// Sanity check
assert offset == componentTypes.length;
return new AnonymousTupleType<>( componentTypes, componentNames );
}
@Override
public Columns copy(SqmCopyContext context) {
final ArrayList<ColumnDefinition> definitions = new ArrayList<>( columnDefinitions.size() );
for ( ColumnDefinition columnDefinition : columnDefinitions ) {
definitions.add( columnDefinition.copy( context ) );
}
return new Columns( context.getCopy( table ), definitions );
}
private void addColumn(ColumnDefinition columnDefinition) {
table.checkTypeResolved();
if ( !columnNames.add( columnDefinition.name() ) ) {
throw new IllegalStateException( "Duplicate column: " + columnDefinition.name() );
}
columnDefinitions.add( columnDefinition );
}
@Override
public void appendHqlString(StringBuilder sb) {
String separator = " columns ";
for ( ColumnDefinition columnDefinition : columnDefinitions ) {
sb.append( separator );
columnDefinition.appendHqlString( sb );
separator = ", ";
}
}
@Override
public @Nullable SqmExpressible<Object> getNodeType() {
return null;
}
@Override
public NodeBuilder nodeBuilder() {
return table.nodeBuilder();
}
@Override
public <X> X accept(SemanticQueryWalker<X> walker) {
for ( ColumnDefinition columnDefinition : columnDefinitions ) {
if ( columnDefinition instanceof SqmXmlTableFunction.ValueColumnDefinition<?> definition ) {
if ( definition.defaultExpression != null ) {
definition.defaultExpression.accept( walker );
}
}
else if ( columnDefinition instanceof SqmXmlTableFunction.QueryColumnDefinition definition ) {
if ( definition.defaultExpression != null ) {
definition.defaultExpression.accept( walker );
}
}
}
// No-op since this object is going to be visible as function argument
return null;
}
}
}

View File

@ -5592,7 +5592,7 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
visitOverClause( Collections.emptyList(), getSortSpecificationsRowNumbering( selectClause, queryPart ) );
}
protected final boolean isParameter(Expression expression) {
public static final boolean isParameter(Expression expression) {
return expression instanceof JdbcParameter || expression instanceof SqmParameterInterpretation;
}

View File

@ -0,0 +1,21 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.sql.ast.tree.expression;
import org.hibernate.sql.ast.SqlAstWalker;
import org.hibernate.sql.ast.tree.SqlAstNode;
/**
* @since 7.0
*/
public sealed interface XmlTableColumnDefinition extends SqlAstNode
permits XmlTableOrdinalityColumnDefinition, XmlTableQueryColumnDefinition, XmlTableValueColumnDefinition {
@Override
default void accept(SqlAstWalker sqlTreeWalker) {
throw new UnsupportedOperationException("XmlTableColumnDefinition doesn't support walking");
}
}

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 org.hibernate.sql.ast.SqlAstWalker;
import org.hibernate.sql.ast.tree.SqlAstNode;
import java.util.List;
/**
* @since 7.0
*/
public class XmlTableColumnsClause implements SqlAstNode {
private final List<XmlTableColumnDefinition> columnDefinitions;
public XmlTableColumnsClause(List<XmlTableColumnDefinition> columnDefinitions) {
this.columnDefinitions = columnDefinitions;
}
public List<XmlTableColumnDefinition> getColumnDefinitions() {
return columnDefinitions;
}
@Override
public void accept(SqlAstWalker sqlTreeWalker) {
throw new UnsupportedOperationException("XmlTableColumnsClause doesn't support walking");
}
}

View File

@ -0,0 +1,13 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.sql.ast.tree.expression;
/**
* @since 7.0
*/
public record XmlTableOrdinalityColumnDefinition(
String name
) implements XmlTableColumnDefinition {
}

View File

@ -0,0 +1,20 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.sql.ast.tree.expression;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.hibernate.type.BasicType;
/**
* @since 7.0
*/
public record XmlTableQueryColumnDefinition(
String name,
BasicType<String> type,
@Nullable String xpath,
@Nullable Expression defaultExpression
) implements XmlTableColumnDefinition {
}

View File

@ -0,0 +1,19 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.sql.ast.tree.expression;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* @since 7.0
*/
public record XmlTableValueColumnDefinition(
String name,
CastTarget type,
@Nullable String xpath,
@Nullable Expression defaultExpression
) implements XmlTableColumnDefinition {
}

View File

@ -57,6 +57,14 @@ public class XmlArrayJdbcType extends ArrayJdbcType {
if ( string == null ) {
return null;
}
if ( javaType.getJavaType() == SQLXML.class ) {
SQLXML sqlxml = options.getSession().getJdbcCoordinator().getLogicalConnection()
.getPhysicalConnection()
.createSQLXML();
sqlxml.setString( string );
//noinspection unchecked
return (X) sqlxml;
}
return options.getSessionFactory().getFastSessionServices().getXmlFormatMapper().fromString(
string,
javaType,

View File

@ -97,6 +97,14 @@ public class XmlJdbcType implements AggregateJdbcType {
options
);
}
if ( javaType.getJavaType() == SQLXML.class ) {
SQLXML sqlxml = options.getSession().getJdbcCoordinator().getLogicalConnection()
.getPhysicalConnection()
.createSQLXML();
sqlxml.setString( string );
//noinspection unchecked
return (X) sqlxml;
}
return options.getSessionFactory().getFastSessionServices().getXmlFormatMapper().fromString(
string,
javaType,

View File

@ -0,0 +1,317 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test.function.xml;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Tuple;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.cfg.QuerySettings;
import org.hibernate.dialect.HANADialect;
import org.hibernate.dialect.OracleDialect;
import org.hibernate.dialect.SybaseASEDialect;
import org.hibernate.query.criteria.JpaFunctionRoot;
import org.hibernate.query.sqm.NodeBuilder;
import org.hibernate.query.sqm.tree.expression.SqmXmlTableFunction;
import org.hibernate.query.sqm.tree.select.SqmSelectStatement;
import org.hibernate.testing.orm.junit.DialectContext;
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.hibernate.testing.orm.junit.SkipForDialect;
import org.hibernate.type.SqlTypes;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
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 java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* @author Christian Beikov
*/
@DomainModel(annotatedClasses = XmlTableTest.XmlHolder.class)
@SessionFactory
@ServiceRegistry(settings = @Setting(name = QuerySettings.XML_FUNCTIONS_ENABLED, value = "true"))
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsXmlTable.class)
public class XmlTableTest {
private static final String XML = """
<root>
<elem>
<theInt>1</theInt>
<theFloat>0.1</theFloat>
<theString>abc</theString>
<theBoolean>true</theBoolean>
<theNull/>
<theObject>
<nested>Abc</nested>
</theObject>
</elem>
</root>
""";
@BeforeEach
public void prepareData(SessionFactoryScope scope) {
scope.inTransaction(
em -> {
XmlHolder 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);
}
);
}
@AfterEach
public void cleanupData(SessionFactoryScope scope) {
scope.inTransaction(
em -> {
em.createMutationQuery( "delete from XmlHolder" ).executeUpdate();
}
);
}
@Test
public void testSimple(SessionFactoryScope scope) {
scope.inSession( em -> {
//tag::hql-xml-table-example[]
final String query = """
select
t.theInt,
t.theFloat,
t.theString,
t.theBoolean,
t.theNull,
t.theObject,
t.theNestedString,
t.nonExisting,
t.nonExistingWithDefault
from xmltable('/root/elem' passing :xml columns
theInt Integer,
theFloat Float,
theString String,
theBoolean Boolean,
theNull String,
theObject XML,
theNestedString String path 'theObject/nested',
nonExisting String,
nonExistingWithDefault String default 'none'
) t
"""
//end::hql-xml-table-example[]
.replace( ":xml", "'" + XML + "'" );
//tag::hql-xml-table-example[]
List<Tuple> resultList = em.createQuery( query, Tuple.class )
.getResultList();
//end::hql-xml-table-example[]
assertEquals( 1, resultList.size() );
assertTupleEquals( resultList.get( 0 ) );
} );
}
@Test
@SkipForDialect(dialectClass = SybaseASEDialect.class, reason = "Sybase ASE needs a special emulation for query columns that is impossible with parameters")
public void testNodeBuilderXmlTableObject(SessionFactoryScope scope) {
scope.inSession( em -> {
final NodeBuilder cb = (NodeBuilder) em.getCriteriaBuilder();
final SqmSelectStatement<Tuple> cq = cb.createTupleQuery();
final SqmXmlTableFunction<?> xmlTable = cb.xmlTable( "/root/elem", cb.value( XML ) );
xmlTable.valueColumn( "theInt", Integer.class );
xmlTable.valueColumn( "theFloat", Float.class );
xmlTable.valueColumn( "theString", String.class );
xmlTable.valueColumn( "theBoolean", Boolean.class );
xmlTable.valueColumn( "theNull", String.class );
xmlTable.queryColumn( "theObject" );
xmlTable.valueColumn( "theNestedString", String.class, "theObject/nested" );
xmlTable.valueColumn( "nonExisting", String.class );
xmlTable.valueColumn( "nonExistingWithDefault", String.class ).defaultValue( "none" );
final JpaFunctionRoot<?> root = cq.from( xmlTable );
cq.multiselect(
root.get( "theInt" ),
root.get( "theFloat" ),
root.get( "theString" ),
root.get( "theBoolean" ),
root.get( "theNull" ),
root.get( "theObject" ),
root.get( "theNestedString" ),
root.get( "nonExisting" ),
root.get( "nonExistingWithDefault" )
);
List<Tuple> resultList = em.createQuery( cq ).getResultList();
assertEquals( 1, resultList.size() );
assertTupleEquals( resultList.get( 0 ) );
} );
}
@Test
public void testCorrelateXmlTable(SessionFactoryScope scope) {
scope.inSession( em -> {
final String query = """
select
t.theInt,
t.theFloat,
t.theString,
t.theBoolean
from XmlHolder e join lateral xmltable('/Map' passing e.xml columns
theInt Integer,
theFloat Float,
theString String,
theBoolean Boolean
) t
""";
List<Tuple> resultList = em.createQuery( query, Tuple.class ).getResultList();
assertEquals( 1, resultList.size() );
Tuple tuple = resultList.get( 0 );
assertEquals( 1, tuple.get( 0 ) );
assertEquals( 0.1F, tuple.get( 1 ) );
assertEquals( "abc", tuple.get( 2 ) );
assertEquals( true, tuple.get( 3 ) );
} );
}
private void assertTupleEquals(Tuple tuple) {
assertEquals( 1, tuple.get( 0 ) );
assertEquals( 0.1F, tuple.get( 1 ) );
assertEquals( "abc", tuple.get( 2 ) );
assertEquals( true, tuple.get( 3 ) );
if ( DialectContext.getDialect() instanceof OracleDialect
|| DialectContext.getDialect() instanceof HANADialect
|| DialectContext.getDialect() instanceof SybaseASEDialect ) {
// Some databases return null for empty tags rather than an empty string
assertNull( tuple.get( 4 ) );
}
else {
// Other DBs returns an empty string for an empty tag
assertEquals( "", tuple.get( 4 ) );
}
assertXmlEquals("<theObject><nested>Abc</nested></theObject>", tuple.get( 5, String.class ) );
assertEquals( "Abc", tuple.get( 6 ) );
assertNull( tuple.get( 7 ) );
assertEquals( "none", tuple.get( 8 ) );
}
private void assertXmlEquals(String expected, String actual) {
final Document expectedDoc = parseXml( xmlNormalize( expected ) );
final Document actualDoc = parseXml( xmlNormalize( actual ) );
normalize( expectedDoc );
normalize( actualDoc );
assertEquals( toXml( expectedDoc ).trim(), toXml( actualDoc ).trim() );
}
private void normalize(Document document) {
normalize( document.getChildNodes() );
}
private void normalize(NodeList childNodes) {
for ( int i = 0; i < childNodes.getLength(); i++ ) {
final Node childNode = childNodes.item( i );
if ( childNode.getNodeType() == Node.ELEMENT_NODE ) {
normalize( childNode.getChildNodes() );
}
else if ( childNode.getNodeType() == Node.TEXT_NODE ) {
if ( childNode.getNodeValue().isBlank() ) {
childNode.getParentNode().removeChild( childNode );
}
else {
childNode.setNodeValue( childNode.getNodeValue().trim() );
}
}
else if ( childNode.getNodeType() == Node.COMMENT_NODE ) {
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

@ -847,6 +847,12 @@ abstract public class DialectFeatureChecks {
}
}
public static class SupportsXmlTable implements DialectFeatureCheck {
public boolean apply(Dialect dialect) {
return definesSetReturningFunction( dialect, "xmltable" );
}
}
public static class SupportsArrayAgg implements DialectFeatureCheck {
public boolean apply(Dialect dialect) {
return definesFunction( dialect, "array_agg" );

View File

@ -88,6 +88,7 @@ Out-of-the-box, some common set-returning functions are already supported or emu
* `unnest()` - allows to turn an array into rows
* `generate_series()` - can be used to create a series of values as rows
* `json_table()` - turns a JSON document into rows
* `xmltable()` - turns an XML document into rows
[[cleanup]]
== Clean-up