diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index afd36f9a1f..ddf7812e2c 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -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 +| <> | Constructs an XML element from arguments +| <> | Constructs an XML comment from the single argument +| <> | Constructs an XML forest from the arguments +| <> | Concatenates multiple XML fragments to each other +| <> | Constructs an XML processing instruction +| <> | Extracts content from XML document using XQuery or XPath +| <> | Checks if an XQuery or XPath expression exists in an XML document +| <> | Aggregates XML elements by concatenation +| <> | 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 <>, 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: | <> | Turns an array into rows | <> | Creates a series of values as rows -| <> | Turns a JSON document into rows +| <> | Turns a JSON document into rows +| <> | Turns an XML document into rows |=== To use set returning functions defined in the database, it is required to register them in a `FunctionContributor`: diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmltable_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmltable_bnf.txt new file mode 100644 index 0000000000..3b649dfd05 --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmltable_bnf.txt @@ -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; \ No newline at end of file diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java index 4a7a9c70c2..4acbcc1e19 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java @@ -457,6 +457,7 @@ public class DB2LegacyDialect extends Dialect { functionFactory.xmlexists_db2_legacy(); } functionFactory.xmlagg(); + functionFactory.xmltable_db2(); functionFactory.unnest_emulated(); if ( supportsRecursiveCTE() ) { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HANALegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HANALegacyDialect.java index 43f6bdf4f1..8ce765bf10 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HANALegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HANALegacyDialect.java @@ -508,7 +508,7 @@ public class HANALegacyDialect extends Dialect { functionFactory.jsonObjectAgg_hana(); } -// functionFactory.xmltable(); + functionFactory.xmltable_hana(); } // functionFactory.xmlextract(); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java index 3c7716cd71..7446bf0674 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java @@ -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 ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java index c04740f10c..73f12a3357 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java @@ -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(); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java index a65d974a24..2d98bb6aac 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java @@ -429,6 +429,7 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect { functionFactory.xmlquery_sqlserver(); functionFactory.xmlexists_sqlserver(); functionFactory.xmlagg_sqlserver(); + functionFactory.xmltable_sqlserver(); functionFactory.unnest_sqlserver(); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java index b32a757c4b..1f91eca531 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java @@ -167,6 +167,7 @@ public class SybaseASELegacyDialect extends SybaseLegacyDialect { functionFactory.unnest_sybasease(); functionFactory.generateSeries_sybasease( getMaximumSeriesSize() ); + functionFactory.xmltable_sybasease(); } /** diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 index 8220db2ad3..f1808aab86 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 @@ -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]; diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 index f6e756c513..4281cd7cc1 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 @@ -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() ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java index b6816b389e..390880cd3b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -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 ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java index 125bd4623c..76022a70ce 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java @@ -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() ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index 2cc949d4be..47a97d6009 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -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 ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index 4878c7093a..a46cd13311 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -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 diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index 00445d53b7..16019bb4e8 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -446,6 +446,7 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { functionFactory.xmlquery_sqlserver(); functionFactory.xmlexists_sqlserver(); functionFactory.xmlagg_sqlserver(); + functionFactory.xmltable_sqlserver(); functionFactory.unnest_sqlserver(); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java index f5f66b2917..deb1f874ee 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java @@ -184,6 +184,7 @@ public class SybaseASEDialect extends SybaseDialect { functionFactory.unnest_sybasease(); functionFactory.generateSeries_sybasease( getMaximumSeriesSize() ); + functionFactory.xmltable_sybasease(); } /** diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java index 8840d1ee59..7e2b8772cf 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java @@ -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 ) ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HANAUnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HANAUnnestFunction.java index 1838768593..4573b35bf5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HANAUnnestFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HANAUnnestFunction.java @@ -411,7 +411,7 @@ public class HANAUnnestFunction extends UnnestFunction { sessionFactory ); - // Produce a XML string e.g. ... + // Produce an XML string e.g. ... // 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() ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonValueFunction.java index 15b215fe76..49dc738153 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonValueFunction.java @@ -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; } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/DB2XmlTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/DB2XmlTableFunction.java new file mode 100644 index 0000000000..f429986813 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/DB2XmlTableFunction.java @@ -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 selectableMappings, String name, JdbcMapping type, SqmToSqlAstConverter converter) { + if ( isBoolean( type ) ) { + //noinspection unchecked + final JdbcLiteralFormatter 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 ); + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/HANAXmlTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/HANAXmlTableFunction.java new file mode 100644 index 0000000000..746a36c6a4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/HANAXmlTableFunction.java @@ -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 SelfRenderingSqmSetReturningFunction generateSqmSetReturningFunctionExpression( + List> arguments, + QueryEngine queryEngine) { + //noinspection unchecked + return new SqmXmlTableFunction<>( + this, + this, + getArgumentsValidator(), + getSetReturningTypeResolver(), + queryEngine.getCriteriaBuilder(), + (SqmExpression) 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 sqlArguments = (List) functionTableGroup.getPrimaryTableReference() + .getFunctionExpression() + .getArguments(); + final Expression document = (Expression) sqlArguments.get( 1 ); + final Set 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 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 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 idColumns) { + if ( idColumns.size() == 1 ) { + final ColumnInfo columnInfo = idColumns.get( 0 ); + return new ColumnReference( qualifier, columnInfo.name(), false, null, columnInfo.jdbcMapping() ); + } + else { + final ArrayList 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 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 idColumns) { + if ( embeddableModelPart instanceof EmbeddedCollectionPart collectionPart ) { + addIdColumns( collectionPart.getCollectionAttribute(), idColumns ); + } + else { + addIdColumns( embeddableModelPart.asAttributeMapping().getDeclaringType(), idColumns ); + } + } + + private void addIdColumns(PluralAttributeMapping pluralAttributeMapping, List idColumns) { + final DdlTypeRegistry ddlTypeRegistry = pluralAttributeMapping.getCollectionDescriptor() + .getFactory() + .getTypeConfiguration() + .getDdlTypeRegistry(); + addIdColumns( pluralAttributeMapping.getKeyDescriptor().getKeyPart(), ddlTypeRegistry, idColumns ); + } + + private void addIdColumns(EntityMappingType entityMappingType, List idColumns) { + final DdlTypeRegistry ddlTypeRegistry = entityMappingType.getEntityPersister() + .getFactory() + .getTypeConfiguration() + .getDdlTypeRegistry(); + addIdColumns( entityMappingType.getIdentifierMapping(), ddlTypeRegistry, idColumns ); + } + + private void addIdColumns( + ValuedModelPart modelPart, + DdlTypeRegistry ddlTypeRegistry, + List 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 idColumns; + + public TableColumnReferenceExpression(Expression argument, String tableName, List 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 getIdColumns() { + return idColumns; + } + } + + static class XmlWrapperExpression implements SelfRenderingExpression { + private final List idColumns; + private final String tableQualifier; + private final Expression argument; + + public XmlWrapperExpression(List 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. ... + // which will contain the original XML as well as id column information for correlation + sqlAppender.appendSql( "''||" ); + argument.accept( walker ); + sqlAppender.appendSql( "||''" ); + } + + @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 selectableMappings, String name, JdbcMapping type, SqmToSqlAstConverter converter) { + if ( isBoolean( type ) ) { + //noinspection unchecked + final JdbcLiteralFormatter 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 ); + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/OracleXmlTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/OracleXmlTableFunction.java new file mode 100644 index 0000000000..c39997a149 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/OracleXmlTableFunction.java @@ -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 selectableMappings, String name, JdbcMapping type, SqmToSqlAstConverter converter) { + if ( isEncodedBoolean( type ) ) { + //noinspection unchecked + final JdbcLiteralFormatter 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 ); + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlTableFunction.java new file mode 100644 index 0000000000..f988febac1 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlTableFunction.java @@ -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() ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SybaseASEXmlTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SybaseASEXmlTableFunction.java new file mode 100644 index 0000000000..b72458e34d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SybaseASEXmlTableFunction.java @@ -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 sqlAstNodes, + String tableIdentifierVariable, + boolean lateral, + boolean withOrdinality, + SqmToSqlAstConverter converter) { + final XmlTableArguments arguments = XmlTableArguments.extract( sqlAstNodes ); + final List selectableMappings = new ArrayList<>( arguments.columnsClause().getColumnDefinitions().size() ); + addSelectableMappings( selectableMappings, arguments, converter ); + return selectableMappings.toArray( new SelectableMapping[0] ); + } + + protected void addSelectableMappings(List 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 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 selectableMappings, String name, JdbcMapping type, SqmToSqlAstConverter converter) { + if ( isBoolean( type ) ) { + //noinspection unchecked + final JdbcLiteralFormatter 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; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlTableFunction.java new file mode 100644 index 0000000000..b6e386541b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlTableFunction.java @@ -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 SelfRenderingSqmSetReturningFunction generateSqmSetReturningFunctionExpression(List> arguments, QueryEngine queryEngine) { + //noinspection unchecked + return new SqmXmlTableFunction<>( + this, + this, + getArgumentsValidator(), + getSetReturningTypeResolver(), + queryEngine.getCriteriaBuilder(), + (SqmExpression) arguments.get( 0 ), + (SqmExpression) arguments.get( 1 ) + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List 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 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 + ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlTableSetReturningFunctionTypeResolver.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlTableSetReturningFunctionTypeResolver.java new file mode 100644 index 0000000000..29b8191b90 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlTableSetReturningFunctionTypeResolver.java @@ -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> arguments, TypeConfiguration typeConfiguration) { + final SqmXmlTableFunction.Columns columns = (SqmXmlTableFunction.Columns) arguments.get( arguments.size() - 1 ); + return columns.createTupleType(); + } + + @Override + public SelectableMapping[] resolveFunctionReturnType( + List 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 columnDefinitions = columnsClause.getColumnDefinitions(); + final List selectableMappings = new ArrayList<>( columnDefinitions.size() ); + addSelectableMappings( selectableMappings, columnsClause, converter ); + return selectableMappings.toArray( new SelectableMapping[0] ); + } + + protected void addSelectableMappings(List 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 selectableMappings, XmlTableOrdinalityColumnDefinition definition, SqmToSqlAstConverter converter) { + addSelectableMapping( + selectableMappings, + definition.name(), + converter.getCreationContext().getTypeConfiguration().getBasicTypeForJavaType( Long.class ), + converter ); + } + + protected void addSelectableMappings(List selectableMappings, XmlTableValueColumnDefinition definition, SqmToSqlAstConverter converter) { + addSelectableMapping( + selectableMappings, + definition.name(), + definition.type().getJdbcMapping(), + converter ); + } + + protected void addSelectableMappings(List selectableMappings, XmlTableQueryColumnDefinition definition, SqmToSqlAstConverter converter) { + addSelectableMapping( + selectableMappings, + definition.name(), + converter.getCreationContext().getTypeConfiguration().getBasicTypeRegistry() + .resolve( String.class, SqlTypes.SQLXML ), + converter ); + } + + protected void addSelectableMapping(List 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 + )); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java index 589fbba45d..f1ac2e991a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java @@ -4463,6 +4463,26 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { @Incubating JpaJsonTableFunction jsonTable(Expression jsonDocument, Expression 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 xpath, Expression xmlDocument); + @Override JpaPredicate and(List restrictions); diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaXmlTableColumnNode.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaXmlTableColumnNode.java new file mode 100644 index 0000000000..76c0333903 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaXmlTableColumnNode.java @@ -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 { + + /** + * Specifies the default value to use if resolving the XPath expression doesn't produce results. + * + * @return {@code this} for method chaining + */ + JpaXmlTableColumnNode 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 defaultExpression(Expression expression); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaXmlTableFunction.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaXmlTableFunction.java new file mode 100644 index 0000000000..2d2f9fa237 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaXmlTableFunction.java @@ -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 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 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 + */ + JpaXmlTableColumnNode valueColumn(String columnName, Class type); + + /** + * Like {@link #valueColumn(String, JpaCastTarget, String)} but uses the column name as XPath expression. + * + * @return The {@link JpaXmlTableColumnNode} for the column + */ + JpaXmlTableColumnNode valueColumn(String columnName, JpaCastTarget 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 + */ + JpaXmlTableColumnNode valueColumn(String columnName, Class 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 + */ + JpaXmlTableColumnNode valueColumn(String columnName, JpaCastTarget 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); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java index f76a7d531f..2e499346fc 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java @@ -3878,4 +3878,16 @@ public class HibernateCriteriaBuilderDelegate implements HibernateCriteriaBuilde public JpaJsonTableFunction jsonTable(Expression jsonDocument, Expression 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 xpath, Expression xmlDocument) { + return criteriaBuilder.xmlTable( xpath, xmlDocument ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index fffe6c51b3..a930b02b45 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -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 extends HqlParserBaseVisitor implem ); } + @Override + public Object visitXmltableFunction(HqlParser.XmltableFunctionContext ctx) { + checkXmlFunctionsEnabled( ctx ); + final List argumentsContexts = ctx.expression(); + //noinspection unchecked + final SqmExpression xpath = (SqmExpression) 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 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 node = xmlTable.queryColumn( columnName, xpath ); + final HqlParser.XmltableDefaultClauseContext defaultClause = queryColumnContext.xmltableDefaultClause(); + if ( defaultClause != null ) { + //noinspection unchecked + node.defaultExpression( (Expression) defaultClause.expression().accept( this ) ); + } + } + else if ( columnContext instanceof HqlParser.XmlTableValueColumnContext valueColumnContext ) { + final String columnName = visitIdentifier( valueColumnContext.identifier() ); + //noinspection unchecked + final SqmCastTarget castTarget = (SqmCastTarget) visitCastTarget( valueColumnContext.castTarget() ); + final TerminalNode pathNode = valueColumnContext.STRING_LITERAL(); + final String xpath; + if ( pathNode == null ) { + xpath = null; + } + else { + xpath = unquoteStringLiteral( pathNode.getText() ); + } + final JpaXmlTableColumnNode node = xmlTable.valueColumn( columnName, castTarget, xpath ); + final HqlParser.XmltableDefaultClauseContext defaultClause = valueColumnContext.xmltableDefaultClause(); + if ( defaultClause != null ) { + //noinspection unchecked + node.defaultExpression( (Expression) 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( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java index 960ab2241d..1aceebe5b4 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java @@ -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 jsonPath); + @Override + SqmXmlTableFunction xmlTable(String xpath, Expression xmlDocument); + + @Override + SqmXmlTableFunction xmlTable(Expression xpath, Expression xmlDocument); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Covariant overrides diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index 099740265d..33d639f7d1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java @@ -5956,7 +5956,7 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable { } @Override - public SqmJsonTableFunction jsonTable(Expression jsonDocument, Expression jsonPath) { + public SqmJsonTableFunction jsonTable(Expression jsonDocument, @Nullable Expression 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 xpath, Expression xmlDocument) { + return (SqmXmlTableFunction) getSetReturningFunctionDescriptor( "xmltable" ).generateSqmExpression( + asList( (SqmTypedNode) xpath, (SqmTypedNode) xmlDocument ), + queryEngine + ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/StandardFunctionArgumentTypeResolvers.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/StandardFunctionArgumentTypeResolvers.java index 8895c5b4dd..8b6949fcc5 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/StandardFunctionArgumentTypeResolvers.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/StandardFunctionArgumentTypeResolvers.java @@ -94,7 +94,7 @@ public final class StandardFunctionArgumentTypeResolvers { return new AbstractFunctionArgumentTypeResolver() { @Override public @Nullable MappingModelExpressible resolveFunctionArgumentType(List> 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> 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> arguments, int argumentIndex, SqmToSqlAstConverter converter) { - return resolvers[argumentIndex].resolveFunctionArgumentType( arguments, argumentIndex, converter ); + return argumentIndex < resolvers.length + ? resolvers[argumentIndex].resolveFunctionArgumentType( arguments, argumentIndex, converter ) + : null; } }; } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmXmlTableFunction.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmXmlTableFunction.java new file mode 100644 index 0000000000..464b467101 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmXmlTableFunction.java @@ -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 extends SelfRenderingSqmSetReturningFunction implements JpaXmlTableFunction { + + private final Columns columns; + + public SqmXmlTableFunction( + SqmSetReturningFunctionDescriptor descriptor, + SetReturningFunctionRenderer renderer, + @Nullable ArgumentsValidator argumentsValidator, + SetReturningFunctionTypeResolver setReturningTypeResolver, + NodeBuilder nodeBuilder, + SqmExpression xpath, + SqmExpression document) { + this( + descriptor, + renderer, + Arrays.asList( xpath, document, null ), + argumentsValidator, + setReturningTypeResolver, + nodeBuilder, + new ArrayList<>() + ); + } + + private SqmXmlTableFunction( + SqmSetReturningFunctionDescriptor descriptor, + SetReturningFunctionRenderer renderer, + List> arguments, + @Nullable ArgumentsValidator argumentsValidator, + SetReturningFunctionTypeResolver setReturningTypeResolver, + NodeBuilder nodeBuilder, + ArrayList 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 copy(SqmCopyContext context) { + final SqmXmlTableFunction existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final List> arguments = getArguments(); + final List> argumentsCopy = new ArrayList<>( arguments.size() ); + for ( int i = 0; i < arguments.size() - 1; i++ ) { + argumentsCopy.add( arguments.get( i ).copy( context ) ); + } + final SqmXmlTableFunction 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 resolveSqlAstArguments(List> sqmArguments, SqmToSqlAstConverter walker) { + final List 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 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 queryColumn(String columnName) { + return queryColumn( columnName, null ); + } + + @Override + public JpaXmlTableColumnNode 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 JpaXmlTableColumnNode valueColumn(String columnName, Class type) { + return valueColumn( columnName, type, null ); + } + + @Override + public JpaXmlTableColumnNode valueColumn(String columnName, JpaCastTarget castTarget) { + return valueColumn( columnName, castTarget, null ); + } + + @Override + public JpaXmlTableColumnNode valueColumn(String columnName, Class type, String xpath) { + return valueColumn( columnName, nodeBuilder().castTarget( type ), xpath ); + } + + @Override + public JpaXmlTableColumnNode valueColumn(String columnName, JpaCastTarget castTarget, @Nullable String xpath) { + final ValueColumnDefinition definition = new ValueColumnDefinition<>( + this, + columnName, + (SqmCastTarget) castTarget, + xpath + ); + columns.addColumn( definition ); + return definition; + } + + @Override + public SqmXmlTableFunction 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 { + private final SqmXmlTableFunction table; + private final String name; + private final BasicType type; + private final @Nullable String xpath; + private @Nullable SqmExpression defaultExpression; + + QueryColumnDefinition(SqmXmlTableFunction table, String name, BasicType type, @Nullable String xpath) { + this.table = table; + this.name = name; + this.type = type; + this.xpath = xpath; + } + + private QueryColumnDefinition(SqmXmlTableFunction table, String name, BasicType type, @Nullable String xpath, @Nullable SqmExpression 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 defaultValue(String value) { + return defaultExpression( table.nodeBuilder().value( value ) ); + } + + @Override + public JpaXmlTableColumnNode defaultExpression(Expression expression) { + table.checkTypeResolved(); + this.defaultExpression = (SqmExpression) 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 implements ColumnDefinition, JpaXmlTableColumnNode { + private final SqmXmlTableFunction table; + private final String name; + private final SqmCastTarget type; + private final @Nullable String xpath; + private @Nullable SqmExpression defaultExpression; + + ValueColumnDefinition(SqmXmlTableFunction table, String name, SqmCastTarget type, @Nullable String xpath) { + this.table = table; + this.name = name; + this.type = type; + this.xpath = xpath; + } + + private ValueColumnDefinition(SqmXmlTableFunction table, String name, SqmCastTarget type, @Nullable String xpath, @Nullable SqmExpression 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 defaultValue(X value) { + return defaultExpression( table.nodeBuilder().value( value ) ); + } + + @Override + public JpaXmlTableColumnNode defaultExpression(Expression expression) { + table.checkTypeResolved(); + this.defaultExpression = (SqmExpression) 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 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 { + + private final SqmXmlTableFunction table; + private final Set columnNames; + private final ArrayList columnDefinitions; + + private Columns(SqmXmlTableFunction table, ArrayList 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 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 getNodeType() { + return null; + } + + @Override + public NodeBuilder nodeBuilder() { + return table.nodeBuilder(); + } + + @Override + public X accept(SemanticQueryWalker 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; + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index 6ef4287db8..b81cb0e3af 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java @@ -5592,7 +5592,7 @@ public abstract class AbstractSqlAstTranslator 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; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlTableColumnDefinition.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlTableColumnDefinition.java new file mode 100644 index 0000000000..d53e2930ca --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlTableColumnDefinition.java @@ -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"); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlTableColumnsClause.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlTableColumnsClause.java new file mode 100644 index 0000000000..0e64291f9b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlTableColumnsClause.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.ast.tree.expression; + +import 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 columnDefinitions; + + public XmlTableColumnsClause(List columnDefinitions) { + this.columnDefinitions = columnDefinitions; + } + + public List getColumnDefinitions() { + return columnDefinitions; + } + + @Override + public void accept(SqlAstWalker sqlTreeWalker) { + throw new UnsupportedOperationException("XmlTableColumnsClause doesn't support walking"); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlTableOrdinalityColumnDefinition.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlTableOrdinalityColumnDefinition.java new file mode 100644 index 0000000000..6b787911ce --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlTableOrdinalityColumnDefinition.java @@ -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 { +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlTableQueryColumnDefinition.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlTableQueryColumnDefinition.java new file mode 100644 index 0000000000..53c2200baf --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlTableQueryColumnDefinition.java @@ -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 type, + @Nullable String xpath, + @Nullable Expression defaultExpression +) implements XmlTableColumnDefinition { + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlTableValueColumnDefinition.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlTableValueColumnDefinition.java new file mode 100644 index 0000000000..7dce28c0ca --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlTableValueColumnDefinition.java @@ -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 { + +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlArrayJdbcType.java index dbcb316bd8..0b480fe680 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlArrayJdbcType.java @@ -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, diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlJdbcType.java index 58dd303d09..647e238cbb 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlJdbcType.java @@ -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, diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlTableTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlTableTest.java new file mode 100644 index 0000000000..b8265c7f21 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlTableTest.java @@ -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 = """ + + + 1 + 0.1 + abc + true + + + Abc + + + + """; + + @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 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 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 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 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("Abc", 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 = ""; + return doc.startsWith( " xml; + } + +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index 1db67ccca3..32ba62a309 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -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" ); diff --git a/release-announcement.adoc b/release-announcement.adoc index daeaa23375..56c46e0658 100644 --- a/release-announcement.adoc +++ b/release-announcement.adoc @@ -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