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 455fb039bfd6..b9689d0548c6 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -7,6 +7,7 @@ :example-dir-hql: {core-project-dir}/src/test/java/org/hibernate/orm/test/hql :array-example-dir-hql: {core-project-dir}/src/test/java/org/hibernate/orm/test/function/array :json-example-dir-hql: {core-project-dir}/src/test/java/org/hibernate/orm/test/function/json +:xml-example-dir-hql: {core-project-dir}/src/test/java/org/hibernate/orm/test/function/xml :extrasdir: extras This chapter describes Hibernate Query Language (HQL) and Jakarta Persistence Query Language (JPQL). @@ -2158,6 +2159,222 @@ include::{json-example-dir-hql}/JsonArrayInsertTest.java[tags=hql-json-array-ins WARNING: SAP HANA, DB2, H2 and HSQLDB do not support this function. +[[hql-functions-xml]] +==== Functions for dealing with XML + +The following functions deal with SQL XML types, which are not supported on every database. + +NOTE: The following functions are incubating/tech-preview and to use them in HQL, +it is necessary to enable the `hibernate.query.hql.xml_functions_enabled` configuration setting. + +[[hql-xml-functions]] +|=== +| Function | Purpose + +| `xmlelement()` | Constructs an XML element from arguments +| `xmlcomment()` | Constructs an XML comment from the single argument +| `xmlforest()` | Constructs an XML forest from the arguments +| `xmlconcat()` | Concatenates multiple XML fragments to each other +| `xmlpi()` | Constructs an XML processing instruction +| `xmlquery()` | Extracts content from XML document using XQuery or XPath +| `xmlexists()` | Checks if an XQuery or XPath expression exists in an XML document +| `xmlagg()` | Aggregates XML elements by concatenation +|=== + + +[[hql-xmlelement-function]] +===== `xmlelement()` + +Constructs an XML element from the arguments. + +[[hql-xmlelement-bnf]] +[source, antlrv4, indent=0] +---- +include::{extrasdir}/xmlelement_bnf.txt[] +---- + +The identifier represents the XML element name and can be quoted by using backticks. + +[[hql-xmlelement-example]] +==== +[source, java, indent=0] +---- +include::{xml-example-dir-hql}/XmlElementTest.java[tags=hql-xmlelement-example] +---- +==== + +XML element attributes can be defined by using the `xmlattributes` function as second argument. +All following arguments represent the XML content. + +[[hql-xmlelement-attributes-content-example]] +==== +[source, java, indent=0] +---- +include::{xml-example-dir-hql}/XmlElementTest.java[tags=hql-xmlelement-attributes-content-example] +---- +==== + +WARNING: SAP HANA, MySQL, MariaDB and HSQLDB do not support this function. + +[[hql-xmlcomment-function]] +===== `xmlcomment()` + +Constructs an XML comment from the single string argument. + +[[hql-xmlcomment-example]] +==== +[source, java, indent=0] +---- +include::{xml-example-dir-hql}/XmlCommentTest.java[tags=hql-xmlcomment-example] +---- +==== + +WARNING: SAP HANA, MySQL, MariaDB and HSQLDB do not support this function. + +[[hql-xmlforest-function]] +===== `xmlforest()` + +Constructs an XML forest from the arguments. A forest is a sequence of XML elements. + +[[hql-xmlforest-bnf]] +[source, antlrv4, indent=0] +---- +include::{extrasdir}/xmlforest_bnf.txt[] +---- + +The optional name specifies the XML element name to use for the content as produced by the expression. +The name can be omitted if a path expression is passed, in which case the last attribute name is used as element name. + +[[hql-xmlforest-example]] +==== +[source, java, indent=0] +---- +include::{xml-example-dir-hql}/XmlForestTest.java[tags=hql-xmlforest-example] +---- +==== + +WARNING: SAP HANA, MySQL, MariaDB and HSQLDB do not support this function. + +[[hql-xmlconcat-function]] +===== `xmlconcat()` + +Concatenates multiple XML fragments to each other. + +[[hql-xmlconcat-example]] +==== +[source, java, indent=0] +---- +include::{xml-example-dir-hql}/XmlConcatTest.java[tags=hql-xmlconcat-example] +---- +==== + +WARNING: SAP HANA, MySQL, MariaDB and HSQLDB do not support this function. + +[[hql-xmlpi-function]] +===== `xmlpi()` + +Constructs an XML processing instruction from the arguments. + +[[hql-xmlpi-bnf]] +[source, antlrv4, indent=0] +---- +include::{extrasdir}/xmlpi_bnf.txt[] +---- + +The identifier represents the XML processing instruction name and can be quoted by using backticks. + +[[hql-xmlpi-example]] +==== +[source, java, indent=0] +---- +include::{xml-example-dir-hql}/XmlPiTest.java[tags=hql-xmlpi-example] +---- +==== + +The optional second argument represents the processing instruction content. + +[[hql-xmlpi-content-example]] +==== +[source, java, indent=0] +---- +include::{xml-example-dir-hql}/XmlPiTest.java[tags=hql-xmlpi-content-example] +---- +==== + +WARNING: SAP HANA, MySQL, MariaDB and HSQLDB do not support this function. + +[[hql-xmlquery-function]] +===== `xmlquery()` + +Extracts content from an XML document using XQuery or XPath. + +[[hql-xmlquery-bnf]] +[source, antlrv4, indent=0] +---- +include::{extrasdir}/xmlquery_bnf.txt[] +---- + +The first argument represents the XQuery or XPath expression. +The second argument after the `passing` keyword represents the XML document. + +[[hql-xmlquery-example]] +==== +[source, java, indent=0] +---- +include::{xml-example-dir-hql}/XmlQueryTest.java[tags=hql-xmlquery-example] +---- +==== + +WARNING: SAP HANA, MySQL, MariaDB and HSQLDB do not support this function. + +[[hql-xmlexists-function]] +===== `xmlexists()` + +Checks if an XQuery or XPath expression exists in an XML document. + +[[hql-xmlexists-bnf]] +[source, antlrv4, indent=0] +---- +include::{extrasdir}/xmlexists_bnf.txt[] +---- + +The first argument represents the XQuery or XPath expression. +The second argument after the `passing` keyword represents the XML document. + +[[hql-xmlexists-example]] +==== +[source, java, indent=0] +---- +include::{xml-example-dir-hql}/XmlExistsTest.java[tags=hql-xmlexists-example] +---- +==== + +WARNING: SAP HANA, MySQL, MariaDB and HSQLDB do not support this function. + +[[hql-xmlagg-function]] +===== `xmlagg()` + +Aggregates XML elements by concatenation. + +[[hql-xmlexists-bnf]] +[source, antlrv4, indent=0] +---- +include::{extrasdir}/xmlagg_bnf.txt[] +---- + +This aggregate function is similar to an <> +since it allows to specify the order in which elements are aggregated, but uses a special syntax. + +[[hql-xmlagg-example]] +==== +[source, java, indent=0] +---- +include::{xml-example-dir-hql}/XmlAggTest.java[tags=hql-xmlagg-example] +---- +==== + +WARNING: SAP HANA, MySQL, MariaDB and HSQLDB do not support this function. + [[hql-user-defined-functions]] ==== Native and user-defined functions diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlagg_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlagg_bnf.txt new file mode 100644 index 000000000000..ea6637bd7c28 --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlagg_bnf.txt @@ -0,0 +1 @@ +"xmlagg(" expression orderByClause? ")" filterClause? overClause? \ No newline at end of file diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlelement_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlelement_bnf.txt new file mode 100644 index 000000000000..4e71635823d0 --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlelement_bnf.txt @@ -0,0 +1,5 @@ +"xmlelement(name " identifier xmlattributes? ("," expressionOrPredicate)* ")" + +xmlattributes + : "xmlattributes(" expressionOrPredicate " as " identifier ("," expressionOrPredicate " as " identifier)* ")" + ; \ No newline at end of file diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlexists_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlexists_bnf.txt new file mode 100644 index 000000000000..e7af7019dad1 --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlexists_bnf.txt @@ -0,0 +1 @@ +"xmlexists(" expression "passing" expression ")" \ No newline at end of file diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlforest_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlforest_bnf.txt new file mode 100644 index 000000000000..93ecd4617bd6 --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlforest_bnf.txt @@ -0,0 +1 @@ +"xmlforest(expressionOrPredicate ("as" identifier)? ("," expressionOrPredicate ("as" identifier)?)* ")" \ No newline at end of file diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlpi_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlpi_bnf.txt new file mode 100644 index 000000000000..50f84ab20ca8 --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlpi_bnf.txt @@ -0,0 +1 @@ +"xmlpi(name " identifier ("," expressionOrPredicate)? ")" \ No newline at end of file diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlquery_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlquery_bnf.txt new file mode 100644 index 000000000000..0f496f2d48f4 --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/xmlquery_bnf.txt @@ -0,0 +1 @@ +"xmlquery(" expression "passing" 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 276adb29bf9c..7fd2861e7dd1 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java @@ -440,6 +440,21 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonObjectAgg_db2(); } } + + functionFactory.xmlelement(); + functionFactory.xmlcomment(); + functionFactory.xmlforest(); + functionFactory.xmlconcat(); + functionFactory.xmlpi(); + if ( getDB2Version().isSameOrAfter( 11 ) ) { + functionFactory.xmlquery_db2(); + functionFactory.xmlexists(); + } + else { + functionFactory.xmlquery_db2_legacy(); + functionFactory.xmlexists_db2_legacy(); + } + functionFactory.xmlagg(); } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java index 8cc4b20d2927..e00ee7e4d70e 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java @@ -417,6 +417,12 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio // Use group_concat until 2.x as listagg was buggy functionFactory.listagg_groupConcat(); } + + functionFactory.xmlelement_h2(); + functionFactory.xmlcomment(); + functionFactory.xmlforest_h2(); + functionFactory.xmlconcat_h2(); + functionFactory.xmlpi_h2(); } else { functionFactory.listagg_groupConcat(); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java index 2e66da81973a..d7dd7bcc4864 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java @@ -325,6 +325,15 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArrayAppend_oracle(); functionFactory.jsonArrayInsert_oracle(); } + + functionFactory.xmlelement(); + functionFactory.xmlcomment(); + functionFactory.xmlforest(); + functionFactory.xmlconcat(); + functionFactory.xmlpi(); + functionFactory.xmlquery_oracle(); + functionFactory.xmlexists(); + functionFactory.xmlagg(); } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java index 6dc5e338ff1b..b2209a66bc61 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java @@ -669,6 +669,15 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArrayAppend_postgresql( getVersion().isSameOrAfter( 13 ) ); functionFactory.jsonArrayInsert_postgresql(); + functionFactory.xmlelement(); + functionFactory.xmlcomment(); + functionFactory.xmlforest(); + functionFactory.xmlconcat(); + functionFactory.xmlpi(); + functionFactory.xmlquery_postgresql(); + functionFactory.xmlexists(); + functionFactory.xmlagg(); + if ( getVersion().isSameOrAfter( 9, 4 ) ) { functionFactory.makeDateTimeTimestamp(); // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java index dfed524539fb..d95e8e468d92 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java @@ -413,6 +413,14 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArrayAppend_sqlserver( getVersion().isSameOrAfter( 16 ) ); functionFactory.jsonArrayInsert_sqlserver(); } + functionFactory.xmlelement_sqlserver(); + functionFactory.xmlcomment_sqlserver(); + functionFactory.xmlforest_sqlserver(); + functionFactory.xmlconcat_sqlserver(); + functionFactory.xmlpi_sqlserver(); + functionFactory.xmlquery_sqlserver(); + functionFactory.xmlexists_sqlserver(); + functionFactory.xmlagg_sqlserver(); if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); functionFactory.jsonArrayAgg_sqlserver( getVersion().isSameOrAfter( 16 ) ); diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 index 1737e80444a2..ea55107b2246 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 @@ -258,6 +258,7 @@ MINELEMENT : [mM] [iI] [nN] [eE] [lL] [eE] [mM] [eE] [nN] [tT]; MININDEX : [mM] [iI] [nN] [iI] [nN] [dD] [eE] [xX]; MINUTE : [mM] [iI] [nN] [uU] [tT] [eE]; MONTH : [mM] [oO] [nN] [tT] [hH]; +NAME : [nN] [aA] [mM] [eE]; NANOSECOND : [nN] [aA] [nN] [oO] [sS] [eE] [cC] [oO] [nN] [dD]; NEW : [nN] [eE] [wW]; NEXT : [nN] [eE] [xX] [tT]; @@ -329,6 +330,13 @@ 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]; +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]; +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]; 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 dfcc5aa1a526..a4e867ed2ce5 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 @@ -1110,6 +1110,7 @@ function | jpaNonstandardFunction | columnFunction | jsonFunction + | xmlFunction | genericFunction ; @@ -1716,6 +1717,64 @@ jsonUniqueKeysClause : (WITH|WITHOUT) UNIQUE KEYS ; +xmlFunction + : xmlelementFunction + | xmlforestFunction + | xmlpiFunction + | xmlqueryFunction + | xmlexistsFunction + | xmlaggFunction + ; + +/** + * The 'xmlelement()' function + */ +xmlelementFunction + : XMLELEMENT LEFT_PAREN NAME identifier (COMMA xmlattributesFunction)? (COMMA expressionOrPredicate)* RIGHT_PAREN + ; + +/** + * The 'xmlattributes()' function + */ +xmlattributesFunction + : XMLATTRIBUTES LEFT_PAREN expressionOrPredicate AS identifier (COMMA expressionOrPredicate AS identifier)* RIGHT_PAREN + ; + +/** + * The 'xmlforest()' function + */ +xmlforestFunction + : XMLFOREST LEFT_PAREN expressionOrPredicate (AS identifier)? (COMMA expressionOrPredicate (AS identifier)?)* RIGHT_PAREN + ; + +/** + * The 'xmlpi()' function + */ +xmlpiFunction + : XMLPI LEFT_PAREN NAME identifier (COMMA expression)? RIGHT_PAREN + ; + +/** + * The 'xmlquery()' function + */ +xmlqueryFunction + : XMLQUERY LEFT_PAREN expression PASSING expression RIGHT_PAREN + ; + +/** + * The 'xmlexists()' function + */ +xmlexistsFunction + : XMLEXISTS LEFT_PAREN expression PASSING expression RIGHT_PAREN + ; + +/** + * The 'xmlexists()' function + */ +xmlaggFunction + : XMLAGG LEFT_PAREN expression orderByClause? RIGHT_PAREN filterClause? overClause? + ; + /** * Support for "soft" keywords which may be used as identifiers * @@ -1847,6 +1906,7 @@ jsonUniqueKeysClause | MININDEX | MINUTE | MONTH + | NAME | NANOSECOND | NATURALID | NEW @@ -1921,6 +1981,13 @@ jsonUniqueKeysClause | WITHIN | WITHOUT | WRAPPER + | XMLAGG + | XMLATTRIBUTES + | XMLELEMENT + | XMLEXISTS + | XMLFOREST + | XMLPI + | XMLQUERY | YEAR | ZONED) { logUseOfReservedWordAsIdentifier( getCurrentToken() ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java index ea6f1e21a723..ba40caa8b537 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java @@ -131,6 +131,7 @@ import static org.hibernate.cfg.QuerySettings.DEFAULT_NULL_ORDERING; import static org.hibernate.cfg.QuerySettings.JSON_FUNCTIONS_ENABLED; import static org.hibernate.cfg.QuerySettings.PORTABLE_INTEGER_DIVISION; +import static org.hibernate.cfg.QuerySettings.XML_FUNCTIONS_ENABLED; import static org.hibernate.engine.config.spi.StandardConverters.BOOLEAN; import static org.hibernate.internal.CoreLogging.messageLogger; import static org.hibernate.internal.log.DeprecationLogger.DEPRECATION_LOGGER; @@ -276,6 +277,7 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions { private final boolean portableIntegerDivisionEnabled; private final boolean jsonFunctionsEnabled; + private final boolean xmlFunctionsEnabled; private final int queryStatisticsMaxSize; @@ -614,6 +616,10 @@ else if ( jdbcTimeZoneValue != null ) { JSON_FUNCTIONS_ENABLED, configurationSettings ); + this.xmlFunctionsEnabled = getBoolean( + XML_FUNCTIONS_ENABLED, + configurationSettings + ); this.queryStatisticsMaxSize = getInt( QUERY_STATISTICS_MAX_SIZE, @@ -1244,6 +1250,11 @@ public boolean isJsonFunctionsEnabled() { return jsonFunctionsEnabled; } + @Override + public boolean isXmlFunctionsEnabled() { + return xmlFunctionsEnabled; + } + @Override public boolean isPortableIntegerDivisionEnabled() { return portableIntegerDivisionEnabled; diff --git a/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java b/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java index 47e1bce73280..85461af84dc7 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java @@ -433,6 +433,11 @@ public boolean isJsonFunctionsEnabled() { return delegate.isJsonFunctionsEnabled(); } + @Override + public boolean isXmlFunctionsEnabled() { + return delegate.isXmlFunctionsEnabled(); + } + @Override public boolean isPortableIntegerDivisionEnabled() { return delegate.isPortableIntegerDivisionEnabled(); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java b/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java index 7967c85b1a1a..2939955db708 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java @@ -285,6 +285,14 @@ default boolean isJsonFunctionsEnabled() { return false; } + /** + * @see org.hibernate.cfg.AvailableSettings#XML_FUNCTIONS_ENABLED + */ + @Override + default boolean isXmlFunctionsEnabled() { + return false; + } + /** * @see org.hibernate.cfg.AvailableSettings#PORTABLE_INTEGER_DIVISION */ diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java b/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java index e135523cf51e..71e529c80e1c 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java @@ -24,6 +24,16 @@ public interface QuerySettings { */ @Incubating String JSON_FUNCTIONS_ENABLED = "hibernate.query.hql.json_functions_enabled"; + + /** + * Boolean setting to control if the use of tech preview XML functions in HQL is enabled. + * By default, this is {@code false} i.e. disabled since the functions are still incubating. + * + * @since 7.0 + */ + @Incubating + String XML_FUNCTIONS_ENABLED = "hibernate.query.hql.xml_functions_enabled"; + /** * Specifies that division of two integers should produce an integer on all * databases. By default, integer division in HQL can produce a non-integer diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java index 023a550fbe07..1cab2faf3837 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -425,6 +425,21 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArrayAgg_db2(); functionFactory.jsonObjectAgg_db2(); } + + functionFactory.xmlelement(); + functionFactory.xmlcomment(); + functionFactory.xmlforest(); + functionFactory.xmlconcat(); + functionFactory.xmlpi(); + if ( getDB2Version().isSameOrAfter( 11 ) ) { + functionFactory.xmlquery_db2(); + functionFactory.xmlexists(); + } + else { + functionFactory.xmlquery_db2_legacy(); + functionFactory.xmlexists_db2_legacy(); + } + functionFactory.xmlagg(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index d8465a0dc242..cf7ffe2eb8cb 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -352,6 +352,12 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArrayAgg_h2(); functionFactory.jsonObjectAgg_h2(); } + + functionFactory.xmlelement_h2(); + functionFactory.xmlcomment(); + functionFactory.xmlforest_h2(); + functionFactory.xmlconcat_h2(); + functionFactory.xmlpi_h2(); } /** diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index 5574e03d4588..c64f10b4fe3f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -415,6 +415,15 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonMergepatch_oracle(); functionFactory.jsonArrayAppend_oracle(); functionFactory.jsonArrayInsert_oracle(); + + functionFactory.xmlelement(); + functionFactory.xmlcomment(); + functionFactory.xmlforest(); + functionFactory.xmlconcat(); + functionFactory.xmlpi(); + functionFactory.xmlquery_oracle(); + functionFactory.xmlexists(); + functionFactory.xmlagg(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index e7900ad3734e..016ba056f87d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -630,6 +630,15 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArrayAppend_postgresql( getVersion().isSameOrAfter( 13 ) ); functionFactory.jsonArrayInsert_postgresql(); + functionFactory.xmlelement(); + functionFactory.xmlcomment(); + functionFactory.xmlforest(); + functionFactory.xmlconcat(); + functionFactory.xmlpi(); + functionFactory.xmlquery_postgresql(); + functionFactory.xmlexists(); + functionFactory.xmlagg(); + functionFactory.makeDateTimeTimestamp(); // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions functionFactory.inverseDistributionOrderedSetAggregates(); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index a18eef147324..d00622b88699 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -431,6 +431,14 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArrayAppend_sqlserver( getVersion().isSameOrAfter( 16 ) ); functionFactory.jsonArrayInsert_sqlserver(); } + functionFactory.xmlelement_sqlserver(); + functionFactory.xmlcomment_sqlserver(); + functionFactory.xmlforest_sqlserver(); + functionFactory.xmlconcat_sqlserver(); + functionFactory.xmlpi_sqlserver(); + functionFactory.xmlquery_sqlserver(); + functionFactory.xmlexists_sqlserver(); + functionFactory.xmlagg_sqlserver(); if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); functionFactory.jsonArrayAgg_sqlserver( getVersion().isSameOrAfter( 16 ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java index a2d1114e30eb..11752fc442ec 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java @@ -153,6 +153,27 @@ import org.hibernate.dialect.function.json.SQLServerJsonReplaceFunction; import org.hibernate.dialect.function.json.SQLServerJsonSetFunction; import org.hibernate.dialect.function.json.SQLServerJsonValueFunction; +import org.hibernate.dialect.function.xml.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.LegacyDB2XmlExistsFunction; +import org.hibernate.dialect.function.xml.LegacyDB2XmlQueryFunction; +import org.hibernate.dialect.function.xml.PostgreSQLXmlQueryFunction; +import org.hibernate.dialect.function.xml.SQLServerXmlAggFunction; +import org.hibernate.dialect.function.xml.SQLServerXmlConcatFunction; +import org.hibernate.dialect.function.xml.SQLServerXmlElementFunction; +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.XmlAggFunction; +import org.hibernate.dialect.function.xml.XmlConcatFunction; +import org.hibernate.dialect.function.xml.XmlElementFunction; +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.query.sqm.function.SqmFunctionRegistry; import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; import org.hibernate.query.sqm.produce.function.FunctionParameterType; @@ -4097,4 +4118,180 @@ public void jsonArrayInsert_oracle() { public void jsonArrayInsert_sqlserver() { functionRegistry.register( "json_array_insert", new SQLServerJsonArrayInsertFunction( typeConfiguration ) ); } + + /** + * Standard xmlelement() function + */ + public void xmlelement() { + functionRegistry.register( "xmlelement", new XmlElementFunction( typeConfiguration ) ); + } + + /** + * H2 xmlelement() function + */ + public void xmlelement_h2() { + functionRegistry.register( "xmlelement", new H2XmlElementFunction( typeConfiguration ) ); + } + + /** + * SQL Server xmlelement() function + */ + public void xmlelement_sqlserver() { + functionRegistry.register( "xmlelement", new SQLServerXmlElementFunction( typeConfiguration ) ); + } + + /** + * Standard xmlcomment() function + */ + public void xmlcomment() { + functionRegistry.namedDescriptorBuilder( "xmlcomment" ) + .setExactArgumentCount( 1 ) + .setParameterTypes( STRING ) + .setInvariantType( typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.SQLXML ) ) + .register(); + } + + /** + * SQL Server xmlcomment() function + */ + public void xmlcomment_sqlserver() { + functionRegistry.patternDescriptorBuilder( "xmlcomment", "cast(('') AS xml)" ) + .setExactArgumentCount( 1 ) + .setParameterTypes( STRING ) + .setInvariantType( typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.SQLXML ) ) + .register(); + } + + /** + * Standard xmlforest() function + */ + public void xmlforest() { + functionRegistry.register( "xmlforest", new XmlForestFunction( typeConfiguration ) ); + } + + /** + * H2 xmlforest() function + */ + public void xmlforest_h2() { + functionRegistry.register( "xmlforest", new H2XmlForestFunction( typeConfiguration ) ); + } + + /** + * SQL Server xmlforest() function + */ + public void xmlforest_sqlserver() { + functionRegistry.register( "xmlforest", new SQLServerXmlForestFunction( typeConfiguration ) ); + } + + /** + * Standard xmlconcat() function + */ + public void xmlconcat() { + functionRegistry.register( "xmlconcat", new XmlConcatFunction( typeConfiguration ) ); + } + + /** + * H2 xmlconcat() function + */ + public void xmlconcat_h2() { + functionRegistry.register( "xmlconcat", new H2XmlConcatFunction( typeConfiguration ) ); + } + + /** + * SQL Server xmlconcat() function + */ + public void xmlconcat_sqlserver() { + functionRegistry.register( "xmlconcat", new SQLServerXmlConcatFunction( typeConfiguration ) ); + } + + /** + * Standard xmlpi() function + */ + public void xmlpi() { + functionRegistry.register( "xmlpi", new XmlPiFunction( typeConfiguration ) ); + } + + /** + * H2 xmlpi() function + */ + public void xmlpi_h2() { + functionRegistry.register( "xmlpi", new H2XmlPiFunction( typeConfiguration ) ); + } + + /** + * SQL Server xmlpi() function + */ + public void xmlpi_sqlserver() { + functionRegistry.register( "xmlpi", new SQLServerXmlPiFunction( typeConfiguration ) ); + } + + /** + * Oracle xmlquery() function + */ + public void xmlquery_oracle() { + functionRegistry.register( "xmlquery", new XmlQueryFunction( true, typeConfiguration ) ); + } + + /** + * DB2 xmlquery() function + */ + public void xmlquery_db2() { + functionRegistry.register( "xmlquery", new XmlQueryFunction( false, typeConfiguration ) ); + } + + /** + * DB2 10.5 xmlquery() function + */ + public void xmlquery_db2_legacy() { + functionRegistry.register( "xmlquery", new LegacyDB2XmlQueryFunction( typeConfiguration ) ); + } + + /** + * PostgreSQL xmlquery() function + */ + public void xmlquery_postgresql() { + functionRegistry.register( "xmlquery", new PostgreSQLXmlQueryFunction( typeConfiguration ) ); + } + + /** + * SQL Server xmlquery() function + */ + public void xmlquery_sqlserver() { + functionRegistry.register( "xmlquery", new SQLServerXmlQueryFunction( typeConfiguration ) ); + } + + /** + * Standard xmlexists() function + */ + public void xmlexists() { + functionRegistry.register( "xmlexists", new XmlExistsFunction( typeConfiguration ) ); + } + + /** + * SQL Server xmlexists() function + */ + public void xmlexists_sqlserver() { + functionRegistry.register( "xmlexists", new SQLServerXmlExistsFunction( typeConfiguration ) ); + } + + /** + * DB2 10.5 xmlexists() function + */ + public void xmlexists_db2_legacy() { + functionRegistry.register( "xmlexists", new LegacyDB2XmlExistsFunction( typeConfiguration ) ); + } + + /** + * Standard xmlagg() function + */ + public void xmlagg() { + functionRegistry.register( "xmlagg", new XmlAggFunction( typeConfiguration ) ); + } + + /** + * SQL Server xmlagg() function + */ + public void xmlagg_sqlserver() { + functionRegistry.register( "xmlagg", new SQLServerXmlAggFunction( typeConfiguration ) ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/ExpressionTypeHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/ExpressionTypeHelper.java index b4445bf1c5df..fffc698305a6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/ExpressionTypeHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/ExpressionTypeHelper.java @@ -35,6 +35,13 @@ public static boolean isJson(SqlAstNode node) { && expressionType.getSingleJdbcMapping().getJdbcType().isJson(); } + public static boolean isXml(SqlAstNode node) { + final Expression expression = (Expression) node; + final JdbcMappingContainer expressionType = expression.getExpressionType(); + return expressionType.getJdbcTypeCount() == 1 + && expressionType.getSingleJdbcMapping().getJdbcType().isXml(); + } + public static JdbcType getSingleJdbcType(SqlAstNode node) { final Expression expression = (Expression) node; final JdbcMappingContainer expressionType = expression.getExpressionType(); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/H2XmlConcatFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/H2XmlConcatFunction.java new file mode 100644 index 000000000000..e74283093658 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/H2XmlConcatFunction.java @@ -0,0 +1,39 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.List; + +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * H2 xmlconcat function. + */ +public class H2XmlConcatFunction extends XmlConcatFunction { + + public H2XmlConcatFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + String separator = ""; + sqlAppender.appendSql( '(' ); + for ( SqlAstNode sqlAstArgument : sqlAstArguments ) { + sqlAppender.appendSql( separator ); + sqlAstArgument.accept( walker ); + separator = "||"; + } + sqlAppender.appendSql( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/H2XmlElementFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/H2XmlElementFunction.java new file mode 100644 index 000000000000..a293a0a7c588 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/H2XmlElementFunction.java @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.Map; + +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * H2 xmlelement function. + */ +public class H2XmlElementFunction extends XmlElementFunction { + + public H2XmlElementFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + XmlElementArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( "xmlnode(" ); + sqlAppender.appendSingleQuoteEscapedString( arguments.elementName() ); + if ( arguments.attributes() != null ) { + String separator = ","; + for ( Map.Entry entry : arguments.attributes().getAttributes().entrySet() ) { + sqlAppender.appendSql( separator ); + sqlAppender.appendSql( "xmlattr(" ); + sqlAppender.appendSingleQuoteEscapedString( entry.getKey() ); + sqlAppender.appendSql( ',' ); + entry.getValue().accept( walker ); + sqlAppender.appendSql( ')' ); + separator = "||"; + } + } + else { + sqlAppender.appendSql( ",null" ); + } + if ( !arguments.content().isEmpty() ) { + String separator = ","; + for ( Expression expression : arguments.content() ) { + sqlAppender.appendSql( separator ); + expression.accept( walker ); + separator = "||"; + } + } + else { + sqlAppender.appendSql( ",null" ); + } + sqlAppender.appendSql( ",false)" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/H2XmlForestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/H2XmlForestFunction.java new file mode 100644 index 000000000000..167b8071d2f6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/H2XmlForestFunction.java @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.List; + +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.AliasedExpression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * H2 xmlforest function. + */ +public class H2XmlForestFunction extends XmlForestFunction { + + public H2XmlForestFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + String separator = ""; + sqlAppender.appendSql( '(' ); + for ( SqlAstNode sqlAstArgument : sqlAstArguments ) { + final AliasedExpression expression = (AliasedExpression) sqlAstArgument; + sqlAppender.appendSql( separator ); + sqlAppender.appendSql( "xmlnode(" ); + sqlAppender.appendSingleQuoteEscapedString( expression.getAlias() ); + sqlAppender.appendSql( ",null," ); + expression.getExpression().accept( walker ); + sqlAppender.appendSql( ",false)" ); + separator = "||"; + } + sqlAppender.appendSql( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/H2XmlPiFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/H2XmlPiFunction.java new file mode 100644 index 000000000000..34250555fca1 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/H2XmlPiFunction.java @@ -0,0 +1,44 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.List; + +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * H2 xmlpi function. + */ +public class H2XmlPiFunction extends XmlPiFunction { + + public H2XmlPiFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( "' 1 ) { + sqlAppender.appendSql( " '||" ); + sqlAstArguments.get( 1 ).accept( walker ); + sqlAppender.appendSql( "||'?>'" ); + } + else { + sqlAppender.appendSql( "?>'" ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/LegacyDB2XmlExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/LegacyDB2XmlExistsFunction.java new file mode 100644 index 000000000000..8210a4cd9fc3 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/LegacyDB2XmlExistsFunction.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.List; + +import org.hibernate.dialect.function.json.ExpressionTypeHelper; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * DB2 10.5 xmlexists function. + */ +public class LegacyDB2XmlExistsFunction extends XmlExistsFunction { + + public LegacyDB2XmlExistsFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final String xquery = walker.getLiteralValue( (Expression) sqlAstArguments.get( 0 ) ); + final Expression xmlDocument = (Expression) sqlAstArguments.get( 1 ); + final boolean needsCast = !ExpressionTypeHelper.isXml( xmlDocument ); + sqlAppender.appendSql( "xmlexists(" ); + sqlAppender.appendSingleQuoteEscapedString( "$d" + xquery ); + sqlAppender.appendSql( " passing " ); + if ( needsCast ) { + sqlAppender.appendSql( "xmlparse(document " ); + } + sqlAstArguments.get( 1 ).accept( walker ); + if ( needsCast ) { + sqlAppender.appendSql( ')' ); + } + sqlAppender.appendSql( " as \"d\")" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/LegacyDB2XmlQueryFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/LegacyDB2XmlQueryFunction.java new file mode 100644 index 000000000000..b3931a5da9ed --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/LegacyDB2XmlQueryFunction.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.List; + +import org.hibernate.dialect.function.json.ExpressionTypeHelper; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * DB2 10.5 xmlquery function. + */ +public class LegacyDB2XmlQueryFunction extends XmlQueryFunction { + + public LegacyDB2XmlQueryFunction(TypeConfiguration typeConfiguration) { + super( false, typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final String xquery = walker.getLiteralValue( (Expression) sqlAstArguments.get( 0 ) ); + final Expression xmlDocument = (Expression) sqlAstArguments.get( 1 ); + final boolean needsCast = !ExpressionTypeHelper.isXml( xmlDocument ); + sqlAppender.appendSql( "xmlquery(" ); + sqlAppender.appendSingleQuoteEscapedString( "$d" + xquery ); + sqlAppender.appendSql( " passing " ); + if ( needsCast ) { + sqlAppender.appendSql( "xmlparse(document " ); + } + sqlAstArguments.get( 1 ).accept( walker ); + if ( needsCast ) { + sqlAppender.appendSql( ')' ); + } + sqlAppender.appendSql( " as \"d\")" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/PostgreSQLXmlQueryFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/PostgreSQLXmlQueryFunction.java new file mode 100644 index 000000000000..733b634686bf --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/PostgreSQLXmlQueryFunction.java @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.List; + +import org.hibernate.dialect.function.json.ExpressionTypeHelper; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * PostgreSQL xmlquery function. + */ +public class PostgreSQLXmlQueryFunction extends XmlQueryFunction { + + public PostgreSQLXmlQueryFunction(TypeConfiguration typeConfiguration) { + super( false, typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression xmlDocument = (Expression) sqlAstArguments.get( 1 ); + final boolean needsCast = !ExpressionTypeHelper.isXml( xmlDocument ); + sqlAppender.appendSql( "(select xmlagg(v) from unnest(xpath(" ); + sqlAstArguments.get( 0 ).accept( walker ); + sqlAppender.appendSql( ',' ); + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + sqlAstArguments.get( 1 ).accept( walker ); + if ( needsCast ) { + sqlAppender.appendSql( " as xml)" ); + } + sqlAppender.appendSql( ")) t(v))" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlAggFunction.java new file mode 100644 index 000000000000..125740f7232e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlAggFunction.java @@ -0,0 +1,244 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.hibernate.dialect.function.json.ExpressionTypeHelper; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.function.SelfRenderingFunctionSqlAstExpression; +import org.hibernate.query.sqm.function.SelfRenderingOrderedSetAggregateFunctionSqlAstExpression; +import org.hibernate.query.sqm.function.SelfRenderingSqmOrderedSetAggregateFunction; +import org.hibernate.query.sqm.spi.SqmCreationHelper; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.predicate.SqmPredicate; +import org.hibernate.query.sqm.tree.select.SqmOrderByClause; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.SqlAstJoinType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.AbstractSqlAstWalker; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Distinct; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.FunctionExpression; +import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression; +import org.hibernate.sql.ast.tree.from.FunctionTableGroup; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupJoin; +import org.hibernate.sql.ast.tree.from.TableReferenceJoin; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.spi.TypeConfiguration; + + +/** + * SQL Server xmlagg function. + */ +public class SQLServerXmlAggFunction extends XmlAggFunction { + + public SQLServerXmlAggFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public SelfRenderingSqmOrderedSetAggregateFunction generateSqmOrderedSetAggregateFunctionExpression( + List> arguments, + SqmPredicate filter, + SqmOrderByClause withinGroupClause, + ReturnableType impliedResultType, + QueryEngine queryEngine) { + return new SelfRenderingSqmOrderedSetAggregateFunction<>( + this, + this, + arguments, + filter, + withinGroupClause, + impliedResultType, + getArgumentsValidator(), + getReturnTypeResolver(), + queryEngine.getCriteriaBuilder(), + getName() + ) { + @Override + public Expression convertToSqlAst(SqmToSqlAstConverter walker) { + // SQL Server can't aggregate an argument that contains a subquery, + // which is a bummer because xmlelement and xmlforest implementations require subqueries, + // but we can apply a trick to make this still work. + // Essentially, we try to move the subquery into the from clause and mark it as lateral. + // Then we can replace the original expression with that new table reference. + final SelfRenderingOrderedSetAggregateFunctionSqlAstExpression expression = (SelfRenderingOrderedSetAggregateFunctionSqlAstExpression) super.convertToSqlAst( walker ); + final Expression xml = (Expression) expression.getArguments().get( 0 ); + final Set qualifiers = ColumnQualifierCollectorSqlAstWalker.determineColumnQualifiers( xml ); + // If the argument contains a subquery, we will receive the column qualifiers that are used + if ( !qualifiers.isEmpty() ) { + // Register a query transformer to register the lateral table group join + walker.registerQueryTransformer( (cteContainer, querySpec, converter) -> { + // Find the table group which the subquery refers to + final TableGroup tableGroup = querySpec.getFromClause().queryTableGroups( + tg -> { + final String primaryVariable = tg.getPrimaryTableReference() + .getIdentificationVariable(); + if ( qualifiers.contains( primaryVariable ) ) { + return tg; + } + for ( TableReferenceJoin tableReferenceJoin : tg.getTableReferenceJoins() ) { + final String variable = tableReferenceJoin.getJoinedTableReference() + .getIdentificationVariable(); + if ( qualifiers.contains( variable ) ) { + return tg; + } + } + return null; + } + ); + if ( tableGroup != null ) { + // Generate the lateral subquery + final String alias = "gen" + SqmCreationHelper.acquireUniqueAlias(); + final FunctionTableGroup lateralGroup = new FunctionTableGroup( + new NavigablePath( "generated", alias ), + null, + new SelfRenderingFunctionSqlAstExpression( + "helper", + (sqlAppender, sqlAstArguments, returnType, walker1) -> { + sqlAppender.appendSql( "(select " ); + xml.accept( walker1 ); + sqlAppender.appendSql( " v)" ); + }, + List.of(), + null, + null + ), + alias, + List.of("v"), + true, + true, + null + ); + tableGroup.addTableGroupJoin( + new TableGroupJoin( + lateralGroup.getNavigablePath(), + SqlAstJoinType.INNER, + lateralGroup + ) + ); + // Replace the original expression that contains a subquery with a simple column reference, + // that points to the newly created lateral table group + //noinspection unchecked + ( (List) expression.getArguments() ).set( + 0, + new ColumnReference( + lateralGroup.getPrimaryTableReference(), + "v", + expression.getJdbcMapping() + ) + ); + } + return querySpec; + } ); + } + return expression; + } + }; + } + + static class ColumnQualifierCollectorSqlAstWalker extends AbstractSqlAstWalker { + + private static final Set POTENTIAL_SUBQUERY_FUNCTIONS = Set.of( + "xmlelement", + "xmlforest" + ); + private final Set columnQualifiers = new HashSet<>(); + private boolean potentialSubquery; + + public static Set determineColumnQualifiers(SqlAstNode node) { + final ColumnQualifierCollectorSqlAstWalker walker = new ColumnQualifierCollectorSqlAstWalker(); + node.accept( walker ); + return walker.potentialSubquery ? walker.columnQualifiers : Set.of(); + } + + @Override + public void visitSelfRenderingExpression(SelfRenderingExpression expression) { + if ( expression instanceof FunctionExpression functionExpression + && POTENTIAL_SUBQUERY_FUNCTIONS.contains( functionExpression.getFunctionName() ) ) { + potentialSubquery = true; + } + super.visitSelfRenderingExpression( expression ); + } + + @Override + public void visitColumnReference(ColumnReference columnReference) { + if ( columnReference.getQualifier() != null ) { + columnQualifiers.add( columnReference.getQualifier() ); + } + } + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + ReturnableType returnType, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null && !translator.supportsFilterClause(); + sqlAppender.appendSql( "cast(string_agg(" ); + final SqlAstNode firstArg = sqlAstArguments.get( 0 ); + final Expression arg; + if ( firstArg instanceof Distinct ) { + sqlAppender.appendSql( "distinct " ); + arg = ( (Distinct) firstArg ).getExpression(); + } + else { + arg = (Expression) firstArg; + } + final boolean needsCast = ExpressionTypeHelper.isXml( arg ); + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + if ( caseWrapper ) { + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + sqlAppender.appendSql( " then " ); + arg.accept( translator ); + sqlAppender.appendSql( " else null end" ); + translator.getCurrentClauseStack().pop(); + } + else { + arg.accept( translator ); + } + if ( needsCast ) { + sqlAppender.appendSql( " as nvarchar(max))" ); + } + sqlAppender.appendSql( ",'')" ); + if ( withinGroup != null && !withinGroup.isEmpty() ) { + translator.getCurrentClauseStack().push( Clause.WITHIN_GROUP ); + sqlAppender.appendSql( " within group (order by " ); + withinGroup.get( 0 ).accept( translator ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( translator ); + } + sqlAppender.appendSql( ')' ); + translator.getCurrentClauseStack().pop(); + } + if ( !caseWrapper && filter != null ) { + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( " filter (where " ); + filter.accept( translator ); + sqlAppender.appendSql( ')' ); + translator.getCurrentClauseStack().pop(); + } + sqlAppender.appendSql( " as xml)" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlConcatFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlConcatFunction.java new file mode 100644 index 000000000000..150a7aaa5974 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlConcatFunction.java @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.List; + +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server xmlconcat function. + */ +public class SQLServerXmlConcatFunction extends XmlConcatFunction { + + public SQLServerXmlConcatFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( "cast" ); + char separator = '('; + for ( SqlAstNode sqlAstArgument : sqlAstArguments ) { + sqlAppender.appendSql( separator ); + sqlAppender.appendSql( "cast(" ); + sqlAstArgument.accept( walker ); + sqlAppender.appendSql( " as nvarchar(max))" ); + separator = '+'; + } + sqlAppender.appendSql( " as xml)" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlElementFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlElementFunction.java new file mode 100644 index 000000000000..44dcca18980c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlElementFunction.java @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.Map; + +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server xmlelement function. + */ +public class SQLServerXmlElementFunction extends XmlElementFunction { + + public SQLServerXmlElementFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + XmlElementArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( "(select 1 tag,null parent" ); + final String aliasPrefix = " [" + arguments.elementName() + "!1"; + if ( arguments.attributes() != null ) { + for ( Map.Entry entry : arguments.attributes().getAttributes().entrySet() ) { + sqlAppender.appendSql( ',' ); + entry.getValue().accept( walker ); + sqlAppender.appendSql( aliasPrefix ); + sqlAppender.appendSql( '!' ); + sqlAppender.appendSql( entry.getKey() ); + sqlAppender.appendSql( ']' ); + } + } + else if ( arguments.content().isEmpty() ) { + sqlAppender.appendSql( ",null" ); + sqlAppender.appendSql( aliasPrefix ); + sqlAppender.appendSql( ']' ); + } + if ( !arguments.content().isEmpty() ) { + for ( Expression expression : arguments.content() ) { + sqlAppender.appendSql( ',' ); + expression.accept( walker ); + sqlAppender.appendSql( aliasPrefix ); + sqlAppender.appendSql( ']' ); + } + } + sqlAppender.appendSql( " for xml explicit,type)" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlExistsFunction.java new file mode 100644 index 000000000000..2cbbecf8f8b4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlExistsFunction.java @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.List; + +import org.hibernate.dialect.function.json.ExpressionTypeHelper; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server xmlexists function. + */ +public class SQLServerXmlExistsFunction extends XmlExistsFunction { + + public SQLServerXmlExistsFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression xmlDocument = (Expression) sqlAstArguments.get( 1 ); + final boolean needsCast = !ExpressionTypeHelper.isXml( xmlDocument ); + sqlAppender.appendSql( '(' ); + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + sqlAstArguments.get( 1 ).accept( walker ); + if ( needsCast ) { + sqlAppender.appendSql( " as xml)" ); + } + sqlAppender.appendSql( ".exist(" ); + sqlAstArguments.get( 0 ).accept( walker ); + sqlAppender.appendSql( ")=1)" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlForestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlForestFunction.java new file mode 100644 index 000000000000..742582db2d21 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlForestFunction.java @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.List; + +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.AliasedExpression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server xmlforest function. + */ +public class SQLServerXmlForestFunction extends XmlForestFunction { + + public SQLServerXmlForestFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( "cast" ); + char separator = '('; + for ( SqlAstNode sqlAstArgument : sqlAstArguments ) { + final AliasedExpression expression = (AliasedExpression) sqlAstArgument; + sqlAppender.appendSql( separator ); + sqlAppender.appendSql( "cast((select 1 tag,null parent," ); + expression.getExpression().accept( walker ); + sqlAppender.appendSql( " [" ); + sqlAppender.appendSql( expression.getAlias() ); + sqlAppender.appendSql( "!1] for xml explicit,type) as nvarchar(max))" ); + separator = '+'; + } + sqlAppender.appendSql( " as xml)" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlPiFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlPiFunction.java new file mode 100644 index 000000000000..e56e70584914 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlPiFunction.java @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.List; + +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server xmlpi function. + */ +public class SQLServerXmlPiFunction extends XmlPiFunction { + + public SQLServerXmlPiFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( "cast(' 1 ) { + sqlAppender.appendSql( " '+" ); + sqlAstArguments.get( 1 ).accept( walker ); + sqlAppender.appendSql( "+'?>'" ); + } + else { + sqlAppender.appendSql( "?>'" ); + } + sqlAppender.appendSql( " as xml)" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlQueryFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlQueryFunction.java new file mode 100644 index 000000000000..82f332fadde6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlQueryFunction.java @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.List; + +import org.hibernate.dialect.function.json.ExpressionTypeHelper; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server xmlquery function. + */ +public class SQLServerXmlQueryFunction extends XmlQueryFunction { + + public SQLServerXmlQueryFunction(TypeConfiguration typeConfiguration) { + super( false, typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression xmlDocument = (Expression) sqlAstArguments.get( 1 ); + final boolean needsCast = !ExpressionTypeHelper.isXml( xmlDocument ); + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + sqlAstArguments.get( 1 ).accept( walker ); + if ( needsCast ) { + sqlAppender.appendSql( " as xml)" ); + } + sqlAppender.appendSql( ".query(" ); + sqlAstArguments.get( 0 ).accept( walker ); + sqlAppender.appendSql( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlAggFunction.java new file mode 100644 index 000000000000..0b2c35d8da83 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlAggFunction.java @@ -0,0 +1,126 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.Collections; +import java.util.List; + +import org.hibernate.dialect.function.json.ExpressionTypeHelper; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.sql.ast.Clause; +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.Distinct; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.XML; + +/** + * Standard xmlagg function. + */ +public class XmlAggFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public XmlAggFunction(TypeConfiguration typeConfiguration) { + super( + "xmlagg", + FunctionKind.ORDERED_SET_AGGREGATE, + StandardArgumentsValidators.composite( + new ArgumentTypesValidator( null, XML ) + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.SQLXML ) + ), + StandardFunctionArgumentTypeResolvers.impliedOrInvariant( typeConfiguration, XML ) + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + render( sqlAppender, sqlAstArguments, null, Collections.emptyList(), returnType, walker ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + ReturnableType returnType, + SqlAstTranslator walker) { + render( sqlAppender, sqlAstArguments, filter, Collections.emptyList(), returnType, walker ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + ReturnableType returnType, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null && !translator.supportsFilterClause(); + sqlAppender.appendSql( "xmlagg(" ); + final SqlAstNode firstArg = sqlAstArguments.get( 0 ); + final Expression arg; + if ( firstArg instanceof Distinct ) { + sqlAppender.appendSql( "distinct " ); + arg = ( (Distinct) firstArg ).getExpression(); + } + else { + arg = (Expression) firstArg; + } + final boolean needsCast = !ExpressionTypeHelper.isXml( arg ); + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + if ( caseWrapper ) { + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + sqlAppender.appendSql( " then " ); + arg.accept( translator ); + sqlAppender.appendSql( " else null end" ); + translator.getCurrentClauseStack().pop(); + } + else { + arg.accept( translator ); + } + if ( needsCast ) { + sqlAppender.appendSql( " as xml)" ); + } + if ( withinGroup != null && !withinGroup.isEmpty() ) { + translator.getCurrentClauseStack().push( Clause.WITHIN_GROUP ); + sqlAppender.appendSql( " order by " ); + withinGroup.get( 0 ).accept( translator ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( translator ); + } + translator.getCurrentClauseStack().pop(); + } + sqlAppender.appendSql( ')' ); + if ( !caseWrapper && filter != null ) { + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( " filter (where " ); + filter.accept( translator ); + sqlAppender.appendSql( ')' ); + translator.getCurrentClauseStack().pop(); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlConcatFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlConcatFunction.java new file mode 100644 index 000000000000..2e0ef74b83bf --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlConcatFunction.java @@ -0,0 +1,58 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.List; + +import org.hibernate.query.ReturnableType; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.XML; + +/** + * Standard xmlconcat function. + */ +public class XmlConcatFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public XmlConcatFunction(TypeConfiguration typeConfiguration) { + super( + "xmlconcat", + FunctionKind.NORMAL, + StandardArgumentsValidators.composite( + new ArgumentTypesValidator( StandardArgumentsValidators.min( 2 ), XML ) + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.SQLXML ) + ), + StandardFunctionArgumentTypeResolvers.impliedOrInvariant( typeConfiguration, XML ) + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( "xmlconcat" ); + char separator = '('; + for ( SqlAstNode sqlAstArgument : sqlAstArguments ) { + sqlAppender.appendSql( separator ); + sqlAstArgument.accept( walker ); + separator = ','; + } + sqlAppender.appendSql( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlElementFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlElementFunction.java new file mode 100644 index 000000000000..826ae0206575 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlElementFunction.java @@ -0,0 +1,191 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.List; +import java.util.Map; + +import org.hibernate.query.ReturnableType; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.function.SelfRenderingSqmFunction; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.ArgumentsValidator; +import org.hibernate.query.sqm.produce.function.FunctionArgumentException; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.expression.SqmExpression; +import org.hibernate.query.sqm.tree.expression.SqmLiteral; +import org.hibernate.query.sqm.tree.expression.SqmXmlAttributesExpression; +import org.hibernate.query.sqm.tree.expression.SqmXmlElementExpression; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.sql.ast.tree.expression.XmlAttributes; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import static java.lang.Character.isLetter; +import static java.lang.Character.isLetterOrDigit; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING; + +/** + * Standard xmlelement function. + */ +public class XmlElementFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public XmlElementFunction(TypeConfiguration typeConfiguration) { + super( + "xmlelement", + FunctionKind.NORMAL, + StandardArgumentsValidators.composite( + new ArgumentTypesValidator( StandardArgumentsValidators.min( 1 ), STRING ), + new ArgumentsValidator() { + @Override + public void validate( + List> arguments, + String functionName, + TypeConfiguration typeConfiguration) { + //noinspection unchecked + final String elementName = ( (SqmLiteral) arguments.get( 0 ) ).getLiteralValue(); + if ( !isValidXmlName( elementName ) ) { + throw new FunctionArgumentException( + String.format( + "Invalid XML element name passed to 'xmlelement()': %s", + elementName + ) + ); + } + if ( arguments.size() > 1 + && arguments.get( 1 ) instanceof SqmXmlAttributesExpression attributesExpression ) { + final Map> attributes = attributesExpression.getAttributes(); + for ( Map.Entry> entry : attributes.entrySet() ) { + if ( !isValidXmlName( entry.getKey() ) ) { + throw new FunctionArgumentException( + String.format( + "Invalid XML attribute name passed to 'xmlattributes()': %s", + entry.getKey() + ) + ); + } + } + } + } + + private static boolean isValidXmlName(String name) { + if ( name.isEmpty() + || !isValidXmlNameStart( name.charAt( 0 ) ) + || name.regionMatches( true, 0, "xml", 0, 3 ) ) { + return false; + } + for ( int i = 1; i < name.length(); i++ ) { + if ( !isValidXmlNameChar( name.charAt( i ) ) ) { + return false; + } + } + return true; + } + + private static boolean isValidXmlNameStart(char c) { + return isLetter( c ) || c == '_' || c == ':'; + } + + private static boolean isValidXmlNameChar(char c) { + return isLetterOrDigit( c ) || c == '_' || c == ':' || c == '-' || c == '.'; + } + + } + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.SQLXML ) + ), + null + ); + } + + @Override + protected SelfRenderingSqmFunction generateSqmFunctionExpression( + List> arguments, + ReturnableType impliedResultType, + QueryEngine queryEngine) { + //noinspection unchecked + return (SelfRenderingSqmFunction) new SqmXmlElementExpression( + this, + this, + arguments, + (ReturnableType) impliedResultType, + getArgumentsValidator(), + getReturnTypeResolver(), + queryEngine.getCriteriaBuilder(), + getName() + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + render( sqlAppender, XmlElementArguments.extract( sqlAstArguments ), returnType, walker ); + } + + protected void render( + SqlAppender sqlAppender, + XmlElementArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( "xmlelement(name " ); + sqlAppender.appendDoubleQuoteEscapedString( arguments.elementName() ); + if ( arguments.attributes() != null ) { + sqlAppender.appendSql( ",xmlattributes" ); + char separator = '('; + for ( Map.Entry entry : arguments.attributes().getAttributes().entrySet() ) { + sqlAppender.appendSql( separator ); + entry.getValue().accept( walker ); + sqlAppender.appendSql( " as " ); + sqlAppender.appendDoubleQuoteEscapedString( entry.getKey() ); + separator = ','; + } + sqlAppender.appendSql( ')' ); + } + if ( !arguments.content().isEmpty() ) { + for ( Expression expression : arguments.content() ) { + sqlAppender.appendSql( ',' ); + expression.accept( walker ); + } + } + sqlAppender.appendSql( ')' ); + } + + protected record XmlElementArguments( + String elementName, + @Nullable XmlAttributes attributes, + List content) { + static XmlElementArguments extract(List arguments) { + final Literal elementName = (Literal) arguments.get( 0 ); + final XmlAttributes attributes; + final List content; + + int index = 1; + if ( arguments.size() > index && arguments.get( index ) instanceof XmlAttributes ) { + attributes = (XmlAttributes) arguments.get( index ); + index++; + } + else { + attributes = null; + } + //noinspection unchecked + content = (List) arguments.subList( index, arguments.size() ); + return new XmlElementArguments( (String) elementName.getLiteralValue(), attributes, content ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlExistsFunction.java new file mode 100644 index 000000000000..080c34e99117 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlExistsFunction.java @@ -0,0 +1,71 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.List; + +import org.hibernate.dialect.function.json.ExpressionTypeHelper; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.IMPLICIT_XML; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.XML; + +/** + * Standard xmlexists function. + */ +public class XmlExistsFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public XmlExistsFunction(TypeConfiguration typeConfiguration) { + super( + "xmlexists", + FunctionKind.NORMAL, + StandardArgumentsValidators.composite( + new ArgumentTypesValidator( null, STRING, IMPLICIT_XML ) + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().getRegisteredType( Boolean.class ) + ), + StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, STRING, XML ) + ); + } + + @Override + public boolean isPredicate() { + return true; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression xmlDocument = (Expression) sqlAstArguments.get( 1 ); + final boolean needsCast = !ExpressionTypeHelper.isXml( xmlDocument ); + sqlAppender.appendSql( "xmlexists(" ); + sqlAstArguments.get( 0 ).accept( walker ); + sqlAppender.appendSql( " passing " ); + if ( needsCast ) { + sqlAppender.appendSql( "xmlparse(document " ); + } + sqlAstArguments.get( 1 ).accept( walker ); + if ( needsCast ) { + sqlAppender.appendSql( ')' ); + } + sqlAppender.appendSql( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlForestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlForestFunction.java new file mode 100644 index 000000000000..7a081c533081 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlForestFunction.java @@ -0,0 +1,119 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.List; + +import org.hibernate.query.ReturnableType; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentsValidator; +import org.hibernate.query.sqm.produce.function.FunctionArgumentException; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.expression.SqmNamedExpression; +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.AliasedExpression; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import static java.lang.Character.isLetter; +import static java.lang.Character.isLetterOrDigit; + +/** + * Standard xmlforest function. + */ +public class XmlForestFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public XmlForestFunction(TypeConfiguration typeConfiguration) { + super( + "xmlforest", + FunctionKind.NORMAL, + StandardArgumentsValidators.composite( + StandardArgumentsValidators.min( 1 ), + new ArgumentsValidator() { + @Override + public void validate( + List> arguments, + String functionName, + TypeConfiguration typeConfiguration) { + for ( int i = 0; i < arguments.size(); i++ ) { + SqmTypedNode argument = arguments.get( i ); + if ( !( argument instanceof SqmNamedExpression namedExpression ) ) { + throw new FunctionArgumentException( + String.format( + "Parameter %d of function 'xmlforest()' is not named", + i + ) + ); + } + if ( !isValidXmlName( namedExpression.getName() ) ) { + throw new FunctionArgumentException( + String.format( + "Invalid XML element name passed to 'xmlforest()': %s", + namedExpression.getName() + ) + ); + } + } + } + + private static boolean isValidXmlName(String name) { + if ( name.isEmpty() + || !isValidXmlNameStart( name.charAt( 0 ) ) + || name.regionMatches( true, 0, "xml", 0, 3 ) ) { + return false; + } + for ( int i = 1; i < name.length(); i++ ) { + if ( !isValidXmlNameChar( name.charAt( i ) ) ) { + return false; + } + } + return true; + } + + private static boolean isValidXmlNameStart(char c) { + return isLetter( c ) || c == '_' || c == ':'; + } + + private static boolean isValidXmlNameChar(char c) { + return isLetterOrDigit( c ) || c == '_' || c == ':' || c == '-' || c == '.'; + } + + } + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.SQLXML ) + ), + null + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( "xmlforest" ); + char separator = '('; + for ( SqlAstNode sqlAstArgument : sqlAstArguments ) { + sqlAppender.appendSql( separator ); + if ( sqlAstArgument instanceof AliasedExpression aliasedExpression ) { + aliasedExpression.getExpression().accept( walker ); + sqlAppender.appendSql( " as " ); + sqlAppender.appendDoubleQuoteEscapedString( aliasedExpression.getAlias() ); + } + else { + sqlAstArgument.accept( walker ); + } + separator = ','; + } + sqlAppender.appendSql( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlPiFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlPiFunction.java new file mode 100644 index 000000000000..813c1a8cac7f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlPiFunction.java @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.List; + +import org.hibernate.query.ReturnableType; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ANY; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING; + +/** + * Standard xmlpi function. + */ +public class XmlPiFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public XmlPiFunction(TypeConfiguration typeConfiguration) { + super( + "xmlpi", + FunctionKind.NORMAL, + StandardArgumentsValidators.composite( + new ArgumentTypesValidator( StandardArgumentsValidators.between( 1, 2 ), STRING, STRING ) + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.SQLXML ) + ), + StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, ANY, STRING ) + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( "xmlpi(name " ); + final Literal literal = (Literal) sqlAstArguments.get( 0 ); + sqlAppender.appendDoubleQuoteEscapedString( (String) literal.getLiteralValue() ); + if ( sqlAstArguments.size() > 1 ) { + sqlAppender.appendSql( ',' ); + sqlAstArguments.get( 1 ).accept( walker ); + } + sqlAppender.appendSql( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlQueryFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlQueryFunction.java new file mode 100644 index 000000000000..a53a0f47f534 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlQueryFunction.java @@ -0,0 +1,73 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.xml; + +import java.util.List; + +import org.hibernate.dialect.function.json.ExpressionTypeHelper; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.IMPLICIT_XML; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.XML; + +/** + * Standard xmlquery function. + */ +public class XmlQueryFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + private final boolean returningContent; + + public XmlQueryFunction(boolean returningContent, TypeConfiguration typeConfiguration) { + super( + "xmlquery", + FunctionKind.NORMAL, + StandardArgumentsValidators.composite( + new ArgumentTypesValidator( null, STRING, IMPLICIT_XML ) + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.SQLXML ) + ), + StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, STRING, XML ) + ); + this.returningContent = returningContent; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression xmlDocument = (Expression) sqlAstArguments.get( 1 ); + final boolean needsCast = !ExpressionTypeHelper.isXml( xmlDocument ); + sqlAppender.appendSql( "xmlquery(" ); + sqlAstArguments.get( 0 ).accept( walker ); + sqlAppender.appendSql( " passing " ); + if ( needsCast ) { + sqlAppender.appendSql( "xmlparse(document " ); + } + sqlAstArguments.get( 1 ).accept( walker ); + if ( needsCast ) { + sqlAppender.appendSql( ')' ); + } + if ( returningContent ) { + sqlAppender.appendSql( " returning content" ); + } + sqlAppender.appendSql( ')' ); + } +} 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 73d78e8a779c..62219014c9d0 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java @@ -4047,6 +4047,152 @@ default JpaPredicate collectionOverlapsNullable(Collection collection1, E @Incubating JpaExpression jsonMergepatch(String document, Expression patch); + /** + * Creates an XML element with the given element name. + * + * @since 7.0 + */ + @Incubating + JpaXmlElementExpression xmlelement(String elementName); + + /** + * Creates an XML comment with the given argument as content. + * + * @since 7.0 + */ + @Incubating + JpaExpression xmlcomment(String comment); + + /** + * Creates an XML forest from the given XML element expressions. + * + * @since 7.0 + * @see #named(Expression, String) + */ + @Incubating + JpaExpression xmlforest(Expression... elements); + + /** + * Creates an XML forest from the given XML element expressions. + * + * @since 7.0 + * @see #named(Expression, String) + */ + @Incubating + JpaExpression xmlforest(List> elements); + + /** + * Concatenates the given XML element expressions. + * + * @since 7.0 + */ + @Incubating + JpaExpression xmlconcat(Expression... elements); + + /** + * Concatenates the given XML element expressions. + * + * @since 7.0 + */ + @Incubating + JpaExpression xmlconcat(List> elements); + + /** + * Creates an XML processing with the given name. + * + * @since 7.0 + */ + @Incubating + JpaExpression xmlpi(String elementName); + + /** + * Creates an XML processing with the given name and content. + * + * @since 7.0 + */ + @Incubating + JpaExpression xmlpi(String elementName, Expression content); + + /** + * Queries the given XML document with the given XPath or XQuery query. + * + * @since 7.0 + */ + @Incubating + JpaExpression xmlquery(String query, Expression xmlDocument); + + /** + * Queries the given XML document with the given XPath or XQuery query. + * + * @since 7.0 + */ + @Incubating + JpaExpression xmlquery(Expression query, Expression xmlDocument); + + /** + * Checks if the given XPath or XQuery query exists in the given XML document. + * + * @since 7.0 + */ + @Incubating + JpaExpression xmlexists(String query, Expression xmlDocument); + + /** + * Checks if the given XPath or XQuery query exists in the given XML document. + * + * @since 7.0 + */ + @Incubating + JpaExpression xmlexists(Expression query, Expression xmlDocument); + + /** + * @see #xmlagg(JpaOrder, JpaPredicate, JpaWindow, Expression) + */ + @Incubating + JpaExpression xmlagg(JpaOrder order, Expression argument); + + /** + * @see #xmlagg(JpaOrder, JpaPredicate, JpaWindow, Expression) + */ + @Incubating + JpaExpression xmlagg(JpaOrder order, JpaPredicate filter, Expression argument); + + /** + * @see #xmlagg(JpaOrder, JpaPredicate, JpaWindow, Expression) + */ + @Incubating + JpaExpression xmlagg(JpaOrder order, JpaWindow window, Expression argument); + + /** + * Create a {@code xmlagg} ordered set-aggregate function expression. + * + * @param order order by clause used in within group + * @param filter optional filter clause + * @param window optional window over which to apply the function + * @param argument values to join + * + * @return ordered set-aggregate expression + * + * @see #functionWithinGroup(String, Class, JpaOrder, JpaPredicate, JpaWindow, Expression...) + */ + @Incubating + JpaExpression xmlagg( + JpaOrder order, + JpaPredicate filter, + JpaWindow window, + Expression argument); + + /** + * Creates a named expression. The name is important for the result of the expression, + * e.g. when building an {@code xmlforest}, the name acts as the XML element name. + * + * @since 7.0 + * @see #xmlforest(Expression[]) + * @see #xmlforest(List) + */ + @Incubating + JpaExpression named(Expression expression, String name); + @Override JpaPredicate and(List restrictions); diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaXmlElementExpression.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaXmlElementExpression.java new file mode 100644 index 000000000000..c44c68fe7cab --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaXmlElementExpression.java @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.criteria; + +import java.util.List; + +import org.hibernate.Incubating; + +import jakarta.persistence.criteria.Expression; + +/** + * A special expression for the {@code xmlelement} function. + * @since 7.0 + */ +@Incubating +public interface JpaXmlElementExpression extends JpaExpression { + + /** + * Passes the given {@link Expression} as value for the XML attribute with the given name. + * + * @return {@code this} for method chaining + */ + JpaXmlElementExpression attribute(String attributeName, Expression expression); + + /** + * Passes the given {@link Expression}s as value for the XML content of this element. + * + * @return {@code this} for method chaining + */ + JpaXmlElementExpression content(List> expressions); + + /** + * Passes the given {@link Expression}s as value for the XML content of this element. + * + * @return {@code this} for method chaining + */ + JpaXmlElementExpression content(Expression... expressions); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java index d025c6bacbb6..00c00aea0053 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java @@ -57,6 +57,7 @@ import org.hibernate.query.criteria.JpaValues; import org.hibernate.query.criteria.JpaWindow; import org.hibernate.query.criteria.JpaWindowFrame; +import org.hibernate.query.criteria.JpaXmlElementExpression; import org.hibernate.query.sqm.TemporalUnit; import jakarta.persistence.Tuple; @@ -3631,4 +3632,106 @@ public JpaExpression jsonMergepatch(Expression document, String patch public JpaExpression jsonMergepatch(String document, Expression patch) { return criteriaBuilder.jsonMergepatch( document, patch ); } + + @Override + @Incubating + public JpaXmlElementExpression xmlelement(String elementName) { + return criteriaBuilder.xmlelement( elementName ); + } + + @Override + @Incubating + public JpaExpression xmlcomment(String comment) { + return criteriaBuilder.xmlcomment( comment ); + } + + @Override + @Incubating + public JpaExpression xmlforest(Expression... elements) { + return criteriaBuilder.xmlforest( elements ); + } + + @Override + @Incubating + public JpaExpression xmlforest(List> elements) { + return criteriaBuilder.xmlforest( elements ); + } + + @Override + @Incubating + public JpaExpression xmlconcat(Expression... elements) { + return criteriaBuilder.xmlconcat( elements ); + } + + @Override + @Incubating + public JpaExpression xmlconcat(List> elements) { + return criteriaBuilder.xmlconcat( elements ); + } + + @Override + @Incubating + public JpaExpression xmlpi(String elementName) { + return criteriaBuilder.xmlpi( elementName ); + } + + @Override + @Incubating + public JpaExpression xmlpi(String elementName, Expression content) { + return criteriaBuilder.xmlpi( elementName, content ); + } + + @Override + @Incubating + public JpaExpression xmlquery(String query, Expression xmlDocument) { + return criteriaBuilder.xmlquery( query, xmlDocument ); + } + + @Override + @Incubating + public JpaExpression xmlquery(Expression query, Expression xmlDocument) { + return criteriaBuilder.xmlquery( query, xmlDocument ); + } + + @Override + @Incubating + public JpaExpression xmlexists(String query, Expression xmlDocument) { + return criteriaBuilder.xmlexists( query, xmlDocument ); + } + + @Override + @Incubating + public JpaExpression xmlexists(Expression query, Expression xmlDocument) { + return criteriaBuilder.xmlexists( query, xmlDocument ); + } + + @Override + @Incubating + public JpaExpression xmlagg(JpaOrder order, Expression argument) { + return criteriaBuilder.xmlagg( order, argument ); + } + + @Override + @Incubating + public JpaExpression xmlagg(JpaOrder order, JpaPredicate filter, Expression argument) { + return criteriaBuilder.xmlagg( order, filter, argument ); + } + + @Override + @Incubating + public JpaExpression xmlagg(JpaOrder order, JpaWindow window, Expression argument) { + return criteriaBuilder.xmlagg( order, window, argument ); + } + + @Override + @Incubating + public JpaExpression xmlagg(JpaOrder order, JpaPredicate filter, JpaWindow window, Expression argument) { + return criteriaBuilder.xmlagg( order, filter, window, argument ); + } + + @Override + @Incubating + public JpaExpression named(Expression expression, String name) { + return criteriaBuilder.named( expression, name ); + } } 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 e87e69c81f9d..b9761e532c71 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 @@ -153,6 +153,7 @@ import org.hibernate.query.sqm.tree.expression.SqmLiteral; import org.hibernate.query.sqm.tree.expression.SqmLiteralEntityType; import org.hibernate.query.sqm.tree.expression.SqmLiteralNull; +import org.hibernate.query.sqm.tree.expression.SqmNamedExpression; import org.hibernate.query.sqm.tree.expression.SqmNamedParameter; import org.hibernate.query.sqm.tree.expression.SqmOver; import org.hibernate.query.sqm.tree.expression.SqmOverflow; @@ -165,6 +166,7 @@ import org.hibernate.query.sqm.tree.expression.SqmTrimSpecification; import org.hibernate.query.sqm.tree.expression.SqmTuple; import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation; +import org.hibernate.query.sqm.tree.expression.SqmXmlElementExpression; import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; import org.hibernate.query.sqm.tree.from.SqmCrossJoin; import org.hibernate.query.sqm.tree.from.SqmCteJoin; @@ -229,6 +231,7 @@ import org.jboss.logging.Logger; +import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.metamodel.Bindable; import jakarta.persistence.metamodel.SingularAttribute; @@ -2982,6 +2985,110 @@ private void checkJsonFunctionsEnabled(ParserRuleContext ctx) { } } + @Override + public SqmExpression visitXmlelementFunction(HqlParser.XmlelementFunctionContext ctx) { + checkXmlFunctionsEnabled( ctx ); + final String elementName = visitIdentifier( ctx.identifier() ); + final SqmXmlElementExpression xmlelement = creationContext.getNodeBuilder().xmlelement( elementName ); + final HqlParser.XmlattributesFunctionContext attributeCtx = ctx.xmlattributesFunction(); + if ( attributeCtx != null ) { + final List expressions = attributeCtx.expressionOrPredicate(); + final List attributeNames = attributeCtx.identifier(); + for ( int i = 0; i < expressions.size(); i++ ) { + xmlelement.attribute( + visitIdentifier( attributeNames.get( i ) ), + (Expression) expressions.get( i ).accept( this ) + ); + } + } + xmlelement.content( visitExpressions( ctx ) ); + return xmlelement; + } + + @Override + public SqmExpression visitXmlforestFunction(HqlParser.XmlforestFunctionContext ctx) { + checkXmlFunctionsEnabled( ctx ); + final ArrayList> elementExpressions = new ArrayList<>( ctx.getChildCount() >> 1 ); + for ( int i = 2; i < ctx.getChildCount(); i++ ) { + if ( ctx.getChild( i ) instanceof HqlParser.ExpressionOrPredicateContext exprCtx ) { + final SqmExpression expression = (SqmExpression) exprCtx.accept( this ); + if ( i + 2 < ctx.getChildCount() && ctx.getChild( i + 2 ) instanceof HqlParser.IdentifierContext identifierContext ) { + final String name = visitIdentifier( identifierContext ); + elementExpressions.add( new SqmNamedExpression<>( expression, name ) ); + i += 2; + } + else { + if ( !( expression instanceof SqmPath path ) || !( path.getModel() instanceof PersistentAttribute attribute ) ) { + throw new SemanticException( + "Can't use expression '" + exprCtx.getText() + " without explicit name in xmlforest function" + + ", because XML element names can only be derived from path expressions.", + query + ); + } + elementExpressions.add( new SqmNamedExpression<>( expression, attribute.getName() ) ); + } + } + } + return creationContext.getNodeBuilder().xmlforest( elementExpressions ); + } + + @Override + public SqmExpression visitXmlpiFunction(HqlParser.XmlpiFunctionContext ctx) { + checkXmlFunctionsEnabled( ctx ); + final String name = visitIdentifier( ctx.identifier() ); + final HqlParser.ExpressionContext exprCtx = ctx.expression(); + //noinspection unchecked + return exprCtx == null + ? creationContext.getNodeBuilder().xmlpi( name ) + : creationContext.getNodeBuilder().xmlpi( name, (Expression) exprCtx.accept( this ) ); + } + + @Override + public SqmExpression visitXmlqueryFunction(HqlParser.XmlqueryFunctionContext ctx) { + checkXmlFunctionsEnabled( ctx ); + final SqmExpression query = (SqmExpression) ctx.expression( 0 ).accept( this ); + final SqmExpression xmlDocument = (SqmExpression) ctx.expression( 1 ).accept( this ); + return creationContext.getNodeBuilder().xmlquery( query, xmlDocument ); + } + + @Override + public SqmExpression visitXmlexistsFunction(HqlParser.XmlexistsFunctionContext ctx) { + checkXmlFunctionsEnabled( ctx ); + final SqmExpression query = (SqmExpression) ctx.expression( 0 ).accept( this ); + final SqmExpression xmlDocument = (SqmExpression) ctx.expression( 1 ).accept( this ); + return creationContext.getNodeBuilder().xmlexists( query, xmlDocument ); + } + + @Override + public SqmExpression visitXmlaggFunction(HqlParser.XmlaggFunctionContext ctx) { + checkXmlFunctionsEnabled( ctx ); + final ArrayList> arguments = new ArrayList<>( 1 ); + arguments.add( (SqmTypedNode) ctx.expression().accept( this ) ); + + return applyOverClause( + ctx.overClause(), + getFunctionDescriptor( "xmlagg" ).generateOrderedSetAggregateSqmExpression( + arguments, + getFilterExpression( ctx ), + ctx.orderByClause() == null + ? null + : visitOrderByClause( ctx.orderByClause(), false ), + null, + creationContext.getQueryEngine() + ) + ); + } + + private void checkXmlFunctionsEnabled(ParserRuleContext ctx) { + if ( !creationOptions.isXmlFunctionsEnabled() ) { + throw new SemanticException( + "Can't use function '" + ctx.children.get( 0 ).getText() + + "', because tech preview XML functions are not enabled. To enable, set the '" + QuerySettings.XML_FUNCTIONS_ENABLED + "' setting to 'true'.", + query + ); + } + } + @Override public SqmPredicate visitIncludesPredicate(HqlParser.IncludesPredicateContext ctx) { final boolean negated = ctx.NOT() != null; diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SqmTreeCreationHelper.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SqmTreeCreationHelper.java index ad93326c522f..44db3663b2a9 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SqmTreeCreationHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SqmTreeCreationHelper.java @@ -5,6 +5,7 @@ package org.hibernate.query.hql.internal; import java.util.Locale; +import java.util.Set; import org.hibernate.grammars.hql.HqlParser; import org.hibernate.jpa.spi.JpaCompliance; @@ -19,8 +20,6 @@ import org.antlr.v4.runtime.Token; import org.antlr.v4.runtime.tree.ParseTree; -import static org.hibernate.grammars.hql.HqlParser.IDENTIFIER; - /** * Helper for dealing with SQM tree creation * @@ -28,6 +27,106 @@ */ public class SqmTreeCreationHelper { + // The list is from the spec section 4.4.1 + private static final Set RESERVED_WORDS = Set.of( + "abs", + "all", + "and", + "any", + "as", + "asc", + "avg", + "between", + "bit_length", + "both", + "by", + "case", + "ceiling", + "char_length", + "character_length", + "class", + "coalesce", + "concat", + "count", + "current_date", + "current_time", + "current_timestamp", + "delete", + "desc", + "distinct", + "else", + "empty", + "end", + "entry", + "escape", + "exists", + "exp", + "extract", + "false", + "fetch", + "first", + "floor", + "from", + "function", + "group", + "having", + "in", + "index", + "inner", + "is", + "join", + "key", + "leading", + "last", + "left", + "length", + "like", + "local", + "ln", + "locate", + "lower", + "max", + "member", + "min", + "mod", + "new", + "not", + "null", + "nulls", + "nullif", + "object", + "of", + "on", + "or", + "order", + "outer", + "position", + "power", + "replace", + "right", + "round", + "select", + "set", + "sign", + "size", + "some", + "sqrt", + "substring", + "sum", + "then", + "trailing", + "treat", + "trim", + "true", + "type", + "unknown", + "update", + "upper", + "value", + "when", + "where" + ); + /** * Handle secondary query roots using cross-join semantics. * @@ -108,7 +207,7 @@ public static String extractVariable(HqlParser.VariableContext ctx, SemanticQuer // which JPA disallows... if ( sqmBuilder.getCreationOptions().useStrictJpaCompliance() ) { final Token identificationVariableToken = identifierContext.getStart(); - if ( identificationVariableToken.getType() != IDENTIFIER ) { + if ( RESERVED_WORDS.contains( identificationVariableToken.getText().toLowerCase( Locale.ENGLISH ) ) ) { throw new StrictJpaComplianceViolation( String.format( Locale.ROOT, @@ -128,7 +227,7 @@ public static String extractVariable(HqlParser.VariableContext ctx, SemanticQuer // which JPA disallows... if ( sqmBuilder.getCreationOptions().useStrictJpaCompliance() ) { final Token identificationVariableToken = identifierContext.getStart(); - if ( identificationVariableToken.getType() != IDENTIFIER ) { + if ( RESERVED_WORDS.contains( identificationVariableToken.getText().toLowerCase( Locale.ENGLISH ) ) ) { throw new StrictJpaComplianceViolation( String.format( Locale.ROOT, diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SqmCreationOptions.java b/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SqmCreationOptions.java index 26b14ee3a60c..dee06842ec1c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SqmCreationOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SqmCreationOptions.java @@ -31,6 +31,13 @@ default boolean isJsonFunctionsEnabled() { return false; } + /** + * @see org.hibernate.cfg.AvailableSettings#XML_FUNCTIONS_ENABLED + */ + default boolean isXmlFunctionsEnabled() { + return false; + } + /** * @see org.hibernate.cfg.AvailableSettings#PORTABLE_INTEGER_DIVISION */ diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/QueryEngineOptions.java b/hibernate-core/src/main/java/org/hibernate/query/spi/QueryEngineOptions.java index 11fd6276525b..75a5ebd807f7 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/QueryEngineOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/QueryEngineOptions.java @@ -81,6 +81,11 @@ default ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHand */ boolean isJsonFunctionsEnabled(); + /** + * @see org.hibernate.cfg.AvailableSettings#XML_FUNCTIONS_ENABLED + */ + boolean isXmlFunctionsEnabled(); + /** * @see org.hibernate.cfg.AvailableSettings#PORTABLE_INTEGER_DIVISION */ diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java index e63a93aabe89..131a67bc1505 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java @@ -47,6 +47,7 @@ import org.hibernate.query.sqm.tree.expression.SqmJsonValueExpression; import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression; import org.hibernate.query.sqm.tree.expression.SqmTuple; +import org.hibernate.query.sqm.tree.expression.SqmXmlElementExpression; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement; @@ -749,6 +750,57 @@ SqmJsonValueExpression jsonValue( @Override SqmExpression jsonMergepatch(Expression document, Expression patch); + @Override + SqmXmlElementExpression xmlelement(String elementName); + + @Override + SqmExpression xmlcomment(String comment); + + @Override + SqmExpression named(Expression expression, String name); + + @Override + SqmExpression xmlforest(List> elements); + + @Override + SqmExpression xmlforest(Expression... elements); + + @Override + SqmExpression xmlconcat(Expression... elements); + + @Override + SqmExpression xmlconcat(List> elements); + + @Override + SqmExpression xmlpi(String elementName); + + @Override + SqmExpression xmlpi(String elementName, Expression content); + + @Override + SqmExpression xmlquery(String query, Expression xmlDocument); + + @Override + SqmExpression xmlquery(Expression query, Expression xmlDocument); + + @Override + SqmExpression xmlexists(String query, Expression xmlDocument); + + @Override + SqmExpression xmlexists(Expression query, Expression xmlDocument); + + @Override + SqmExpression xmlagg(JpaOrder order, Expression argument); + + @Override + SqmExpression xmlagg(JpaOrder order, JpaPredicate filter, Expression argument); + + @Override + SqmExpression xmlagg(JpaOrder order, JpaWindow window, Expression argument); + + @Override + SqmExpression xmlagg(JpaOrder order, JpaPredicate filter, JpaWindow window, Expression argument); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Covariant overrides diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java index d7af9a5dbe4a..a98e6bfd355b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java @@ -69,6 +69,7 @@ import org.hibernate.query.sqm.tree.expression.SqmLiteralEmbeddableType; import org.hibernate.query.sqm.tree.expression.SqmLiteralEntityType; import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression; +import org.hibernate.query.sqm.tree.expression.SqmNamedExpression; import org.hibernate.query.sqm.tree.expression.SqmNamedParameter; import org.hibernate.query.sqm.tree.expression.SqmOver; import org.hibernate.query.sqm.tree.expression.SqmOverflow; @@ -418,4 +419,6 @@ default T visitCorrelatedRoot(SqmCorrelatedRoot correlatedRoot){ T visitFullyQualifiedClass(Class namedClass); T visitAsWrapperExpression(AsWrapperSqmExpression expression); + + T visitNamedExpression(SqmNamedExpression expression); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/NamedSqmFunctionDescriptor.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/NamedSqmFunctionDescriptor.java index c2b046f56e5c..612b6ee27d9c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/NamedSqmFunctionDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/NamedSqmFunctionDescriptor.java @@ -201,7 +201,7 @@ private void render( if ( withinGroup != null && !withinGroup.isEmpty() ) { translator.getCurrentClauseStack().push( Clause.WITHIN_GROUP ); - sqlAppender.appendSql( " within group (order by" ); + sqlAppender.appendSql( " within group (order by " ); translator.render( withinGroup.get( 0 ), argumentRenderingMode ); for ( int i = 1; i < withinGroup.size(); i++ ) { sqlAppender.appendSql( SqlAppender.COMMA_SEPARATOR_CHAR ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCreationOptionsStandard.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCreationOptionsStandard.java index b2c93af76900..461fc23b1f81 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCreationOptionsStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCreationOptionsStandard.java @@ -27,6 +27,11 @@ public boolean isJsonFunctionsEnabled() { return queryEngineOptions.isJsonFunctionsEnabled(); } + @Override + public boolean isXmlFunctionsEnabled() { + return queryEngineOptions.isXmlFunctionsEnabled(); + } + @Override public boolean isPortableIntegerDivisionEnabled() { return queryEngineOptions.isPortableIntegerDivisionEnabled(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index 4f9857b4dd4f..78730ff6183e 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 @@ -43,6 +43,7 @@ import org.hibernate.jpa.spi.JpaCompliance; import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.JpaMetamodel; +import org.hibernate.metamodel.model.domain.PersistentAttribute; import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.query.BindableType; @@ -128,6 +129,7 @@ import org.hibernate.query.sqm.tree.expression.SqmLiteral; import org.hibernate.query.sqm.tree.expression.SqmLiteralNull; import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression; +import org.hibernate.query.sqm.tree.expression.SqmNamedExpression; import org.hibernate.query.sqm.tree.expression.SqmOver; import org.hibernate.query.sqm.tree.expression.SqmStar; import org.hibernate.query.sqm.tree.expression.SqmToDuration; @@ -136,6 +138,7 @@ import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation; import org.hibernate.query.sqm.tree.expression.SqmWindow; import org.hibernate.query.sqm.tree.expression.SqmWindowFrame; +import org.hibernate.query.sqm.tree.expression.SqmXmlElementExpression; import org.hibernate.query.sqm.tree.expression.ValueBindJpaCriteriaParameter; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; @@ -218,6 +221,7 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable { private transient BasicType integerType; private transient BasicType longType; private transient BasicType characterType; + private transient BasicType stringType; private transient FunctionReturnTypeResolver sumReturnTypeResolver; private transient FunctionReturnTypeResolver avgReturnTypeResolver; private final transient Map, HibernateCriteriaBuilder> extensions; @@ -311,6 +315,16 @@ public BasicType getCharacterType() { return characterType; } + public BasicType getStringType() { + final BasicType stringType = this.stringType; + if ( stringType == null ) { + return this.stringType = + getTypeConfiguration().getBasicTypeRegistry() + .resolve( StandardBasicTypes.STRING ); + } + return stringType; + } + public FunctionReturnTypeResolver getSumReturnTypeResolver() { final FunctionReturnTypeResolver resolver = sumReturnTypeResolver; if ( resolver == null ) { @@ -5664,4 +5678,138 @@ public SqmExpression jsonMergepatch(Expression document, Expression> arguments = new ArrayList<>( 3 ); + arguments.add( new SqmLiteral<>( elementName, getStringType(), this ) ); + return (SqmXmlElementExpression) getFunctionDescriptor( "xmlelement" ).generateSqmExpression( + arguments, + null, + queryEngine + ); + } + + @Override + public SqmExpression xmlcomment(String comment) { + return getFunctionDescriptor( "xmlcomment" ).generateSqmExpression( + List.of( value( comment ) ), + null, + queryEngine + ); + } + + @Override + public SqmExpression named(Expression expression, String name) { + return new SqmNamedExpression<>( (SqmExpression) expression, name ); + } + + @Override + public SqmExpression xmlforest(Expression... elements) { + return xmlforest( Arrays.asList( elements ) ); + } + + @Override + public SqmExpression xmlforest(List> elements) { + final ArrayList> arguments = new ArrayList<>( elements.size() ); + for ( Expression expression : elements ) { + if ( expression instanceof SqmNamedExpression ) { + arguments.add( (SqmNamedExpression) expression ); + } + else { + if ( !( expression instanceof SqmPath path ) || !( path.getModel() instanceof PersistentAttribute attribute ) ) { + throw new SemanticException( + "Can't use expression '" + expression + " without explicit name in xmlforest function"+ + ", because XML element names can only be derived from path expressions." + ); + } + arguments.add( new SqmNamedExpression<>( (SqmExpression) expression, attribute.getName() ) ); + } + } + return getFunctionDescriptor( "xmlforest" ).generateSqmExpression( + arguments, + null, + queryEngine + ); + } + + @Override + public SqmExpression xmlconcat(Expression... elements) { + return xmlconcat( Arrays.asList( elements ) ); + } + + @Override + public SqmExpression xmlconcat(List> elements) { + return getFunctionDescriptor( "xmlforest" ).generateSqmExpression( + (List>) elements, + null, + queryEngine + ); + } + + @Override + public SqmExpression xmlpi(String elementName) { + return getFunctionDescriptor( "xmlpi" ).generateSqmExpression( + asList( literal( elementName ) ), + null, + queryEngine + ); + } + + @Override + public SqmExpression xmlpi(String elementName, Expression content) { + return getFunctionDescriptor( "xmlpi" ).generateSqmExpression( + asList( literal( elementName ), (SqmTypedNode) content ), + null, + queryEngine + ); + } + + @Override + public SqmExpression xmlquery(String query, Expression xmlDocument) { + return xmlquery( value( query ), xmlDocument ); + } + + @Override + public SqmExpression xmlquery(Expression query, Expression xmlDocument) { + return getFunctionDescriptor( "xmlquery" ).generateSqmExpression( + asList( (SqmTypedNode) query, (SqmTypedNode) xmlDocument ), + null, + queryEngine + ); + } + + @Override + public SqmExpression xmlexists(String query, Expression xmlDocument) { + return xmlexists( value( query ), xmlDocument ); + } + + @Override + public SqmExpression xmlexists(Expression query, Expression xmlDocument) { + return getFunctionDescriptor( "xmlexists" ).generateSqmExpression( + asList( (SqmTypedNode) query, (SqmTypedNode) xmlDocument ), + null, + queryEngine + ); + } + + @Override + public SqmExpression xmlagg(JpaOrder order, Expression argument) { + return xmlagg( order, null, null, argument ); + } + + @Override + public SqmExpression xmlagg(JpaOrder order, JpaPredicate filter, Expression argument) { + return xmlagg( order, filter, null, argument ); + } + + @Override + public SqmExpression xmlagg(JpaOrder order, JpaWindow window, Expression argument) { + return xmlagg( order, null, window, argument ); + } + + @Override + public SqmExpression xmlagg(JpaOrder order, JpaPredicate filter, JpaWindow window, Expression argument) { + return functionWithinGroup( "xmlagg", String.class, order, filter, window, argument ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java index ff6aa3176076..0c6f87b72e0d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java @@ -58,6 +58,7 @@ import org.hibernate.query.sqm.tree.expression.SqmLiteralEmbeddableType; import org.hibernate.query.sqm.tree.expression.SqmLiteralEntityType; import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression; +import org.hibernate.query.sqm.tree.expression.SqmNamedExpression; import org.hibernate.query.sqm.tree.expression.SqmNamedParameter; import org.hibernate.query.sqm.tree.expression.SqmOver; import org.hibernate.query.sqm.tree.expression.SqmOverflow; @@ -1198,6 +1199,11 @@ public Object visitAsWrapperExpression(AsWrapperSqmExpression expression) { return null; } + @Override + public Object visitNamedExpression(SqmNamedExpression expression) { + return null; + } + @Override public Object visitModifiedSubQueryExpression(SqmModifiedSubQueryExpression expression) { return null; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/ArgumentTypesValidator.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/ArgumentTypesValidator.java index ecf85aaf50cf..05cb05fdb84e 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/ArgumentTypesValidator.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/ArgumentTypesValidator.java @@ -251,6 +251,8 @@ private static boolean isCompatible(FunctionParameterType type, JdbcType jdbcTyp case SPATIAL -> jdbcType.isSpatial(); case JSON -> jdbcType.isJson(); case IMPLICIT_JSON -> jdbcType.isImplicitJson(); + case XML -> jdbcType.isXml(); + case IMPLICIT_XML -> jdbcType.isImplicitXml(); default -> true; // TODO: should we throw here? }; } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionParameterType.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionParameterType.java index 750e719343e8..d2ba1aa06c84 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionParameterType.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionParameterType.java @@ -98,6 +98,18 @@ public enum FunctionParameterType { * @since 7.0 */ IMPLICIT_JSON, + /** + * Indicates that the argument should be a XML type + * @see org.hibernate.type.SqlTypes#isXmlType(int) + * @since 7.0 + */ + XML, + /** + * Indicates that the argument should be a XML or String type + * @see org.hibernate.type.SqlTypes#isImplicitXmlType(int) + * @since 7.0 + */ + IMPLICIT_XML, /** * Indicates a parameter that accepts any type, except untyped expressions like {@code null} literals */ diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java index 39df3ecfe58d..3dd3012500e7 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java @@ -60,6 +60,7 @@ import org.hibernate.query.sqm.tree.expression.SqmLiteralEmbeddableType; import org.hibernate.query.sqm.tree.expression.SqmLiteralEntityType; import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression; +import org.hibernate.query.sqm.tree.expression.SqmNamedExpression; import org.hibernate.query.sqm.tree.expression.SqmNamedParameter; import org.hibernate.query.sqm.tree.expression.SqmOrderedSetAggregateFunction; import org.hibernate.query.sqm.tree.expression.SqmOver; @@ -988,4 +989,10 @@ public Object visitAsWrapperExpression(AsWrapperSqmExpression expression) { expression.getExpression().accept( this ); return expression; } + + @Override + public Object visitNamedExpression(SqmNamedExpression expression) { + expression.getExpression().accept( this ); + return expression; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java index 2d06423e2310..131669c0204c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java @@ -224,6 +224,7 @@ import org.hibernate.query.sqm.tree.expression.SqmLiteralEntityType; import org.hibernate.query.sqm.tree.expression.SqmLiteralNull; import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression; +import org.hibernate.query.sqm.tree.expression.SqmNamedExpression; import org.hibernate.query.sqm.tree.expression.SqmNamedParameter; import org.hibernate.query.sqm.tree.expression.SqmOver; import org.hibernate.query.sqm.tree.expression.SqmOverflow; @@ -314,6 +315,7 @@ import org.hibernate.sql.ast.tree.cte.CteTableGroup; import org.hibernate.sql.ast.tree.cte.SearchClauseSpecification; import org.hibernate.sql.ast.tree.delete.DeleteStatement; +import org.hibernate.sql.ast.tree.expression.AliasedExpression; import org.hibernate.sql.ast.tree.expression.Any; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; @@ -8325,6 +8327,11 @@ public Object visitAsWrapperExpression(AsWrapperSqmExpression sqmExpression) ); } + @Override + public Object visitNamedExpression(SqmNamedExpression expression) { + return new AliasedExpression( (Expression) expression.getExpression().accept( this ), expression.getName() ); + } + @Override public Fetch visitIdentifierFetch(EntityResultGraphNode fetchParent) { final EntityIdentifierMapping identifierMapping = fetchParent.getReferencedMappingContainer() diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonNullBehavior.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonNullBehavior.java index ce9cbc7efc54..4586583aaf32 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonNullBehavior.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonNullBehavior.java @@ -35,7 +35,7 @@ public enum SqmJsonNullBehavior implements SqmTypedNode { @Override public NodeBuilder nodeBuilder() { - return null; + throw new UnsupportedOperationException(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonObjectAggUniqueKeysBehavior.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonObjectAggUniqueKeysBehavior.java index ef7bf81f24a5..65b3c829ccff 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonObjectAggUniqueKeysBehavior.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonObjectAggUniqueKeysBehavior.java @@ -35,7 +35,7 @@ public enum SqmJsonObjectAggUniqueKeysBehavior implements SqmTypedNode { @Override public NodeBuilder nodeBuilder() { - return null; + throw new UnsupportedOperationException(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmNamedExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmNamedExpression.java new file mode 100644 index 000000000000..cdd67c3fe283 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmNamedExpression.java @@ -0,0 +1,62 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.sqm.tree.expression; + +import org.hibernate.Incubating; +import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.tree.SqmCopyContext; + +/** + * A named expression. Used when the name of the expression matters + * e.g. in XML generation. + * + * @since 7.0 + */ +@Incubating +public class SqmNamedExpression extends AbstractSqmExpression { + + private final SqmExpression expression; + private final String name; + + public SqmNamedExpression(SqmExpression expression, String name) { + super( expression.getExpressible(), expression.nodeBuilder() ); + this.expression = expression; + this.name = name; + } + + @Override + public SqmNamedExpression copy(SqmCopyContext context) { + final SqmNamedExpression existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final SqmNamedExpression expression = context.registerCopy( + this, + new SqmNamedExpression<>( this.expression.copy( context ), name ) + ); + copyTo( expression, context ); + return expression; + } + + public SqmExpression getExpression() { + return expression; + } + + public String getName() { + return name; + } + + @Override + public X accept(SemanticQueryWalker walker) { + return walker.visitNamedExpression( this ); + } + + @Override + public void appendHqlString(StringBuilder sb) { + expression.appendHqlString( sb ); + sb.append( " as " ); + sb.append( name ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmXmlAttributesExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmXmlAttributesExpression.java new file mode 100644 index 000000000000..738070c85d3c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmXmlAttributesExpression.java @@ -0,0 +1,94 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.sqm.tree.expression; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.hibernate.Incubating; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.sql.ast.tree.expression.XmlAttributes; + +import jakarta.persistence.criteria.Expression; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Special expression for the json_query function that also captures special syntax elements like error and empty behavior. + * + * @since 7.0 + */ +@Incubating +public class SqmXmlAttributesExpression implements SqmTypedNode { + + private final Map> attributes; + + public SqmXmlAttributesExpression(String attributeName, Expression expression) { + final Map> attributes = new LinkedHashMap<>(); + attributes.put( attributeName, (SqmExpression) expression ); + this.attributes = attributes; + } + + private SqmXmlAttributesExpression(Map> attributes) { + this.attributes = attributes; + } + + public void attribute(String attributeName, Expression expression) { + attributes.put( attributeName, (SqmExpression) expression ); + } + + public Map> getAttributes() { + return attributes; + } + + @Override + public @Nullable SqmExpressible getNodeType() { + return null; + } + + @Override + public NodeBuilder nodeBuilder() { + throw new UnsupportedOperationException(); + } + + @Override + public X accept(SemanticQueryWalker walker) { + final Map attributes = new LinkedHashMap<>(); + for ( Map.Entry> entry : this.attributes.entrySet() ) { + attributes.put( entry.getKey(), (org.hibernate.sql.ast.tree.expression.Expression) entry.getValue().accept( walker ) ); + } + //noinspection unchecked + return (X) new XmlAttributes( attributes ); + } + + @Override + public SqmXmlAttributesExpression copy(SqmCopyContext context) { + final SqmXmlAttributesExpression existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final Map> attributes = new LinkedHashMap<>(); + for ( Map.Entry> entry : this.attributes.entrySet() ) { + attributes.put( entry.getKey(), entry.getValue().copy( context ) ); + } + return context.registerCopy( this, new SqmXmlAttributesExpression( attributes ) ); + } + + @Override + public void appendHqlString(StringBuilder sb) { + String separator = "xmlattributes("; + for ( Map.Entry> entry : attributes.entrySet() ) { + sb.append( separator ); + entry.getValue().appendHqlString( sb ); + sb.append( " as " ); + sb.append( entry.getKey() ); + separator = ", "; + } + sb.append( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmXmlElementExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmXmlElementExpression.java new file mode 100644 index 000000000000..1baf9bdb9ff5 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmXmlElementExpression.java @@ -0,0 +1,128 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.sqm.tree.expression; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.hibernate.Incubating; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.criteria.JpaXmlElementExpression; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.function.FunctionRenderer; +import org.hibernate.query.sqm.function.SelfRenderingSqmFunction; +import org.hibernate.query.sqm.function.SqmFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.ArgumentsValidator; +import org.hibernate.query.sqm.produce.function.FunctionReturnTypeResolver; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmTypedNode; + +import jakarta.persistence.criteria.Expression; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Special expression for the xmlelement function that also captures special syntax elements like xmlattributes. + * + * @since 7.0 + */ +@Incubating +public class SqmXmlElementExpression extends SelfRenderingSqmFunction implements JpaXmlElementExpression { + + public SqmXmlElementExpression( + SqmFunctionDescriptor descriptor, + FunctionRenderer renderer, + List> arguments, + @Nullable ReturnableType impliedResultType, + @Nullable ArgumentsValidator argumentsValidator, + FunctionReturnTypeResolver returnTypeResolver, + NodeBuilder nodeBuilder, + String name) { + super( + descriptor, + renderer, + arguments, + impliedResultType, + argumentsValidator, + returnTypeResolver, + nodeBuilder, + name + ); + } + + @Override + public SqmXmlElementExpression attribute(String attributeName, Expression expression) { + //noinspection unchecked + final List> arguments = (List>) getArguments(); + if ( arguments.size() > 1 && arguments.get( 1 ) instanceof SqmXmlAttributesExpression attributesExpression ) { + attributesExpression.attribute( attributeName, expression ); + } + else { + arguments.add( 1, new SqmXmlAttributesExpression( attributeName, expression ) ); + } + return this; + } + + @Override + public SqmXmlElementExpression content(Expression... expressions) { + return content( Arrays.asList(expressions) ); + } + + @Override + public SqmXmlElementExpression content(List> expressions) { + //noinspection unchecked + final List> arguments = (List>) getArguments(); + int contentIndex = 1; + if ( arguments.size() > contentIndex ) { + if ( arguments.get( contentIndex ) instanceof SqmXmlAttributesExpression ) { + contentIndex++; + } + while ( contentIndex < arguments.size() ) { + arguments.remove( arguments.size() - 1 ); + } + } + for ( Expression expression : expressions ) { + arguments.add( (SqmTypedNode) expression ); + } + return this; + } + + @Override + public SqmXmlElementExpression copy(SqmCopyContext context) { + final SqmXmlElementExpression existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final List> arguments = new ArrayList<>( getArguments().size() ); + for ( SqmTypedNode argument : getArguments() ) { + arguments.add( argument.copy( context ) ); + } + return context.registerCopy( + this, + new SqmXmlElementExpression( + getFunctionDescriptor(), + getFunctionRenderer(), + arguments, + getImpliedResultType(), + getArgumentsValidator(), + getReturnTypeResolver(), + nodeBuilder(), + getFunctionName() + ) + ); + } + + @Override + public void appendHqlString(StringBuilder sb) { + final List> arguments = getArguments(); + sb.append( "xmlelement(name " ); + arguments.get( 0 ).appendHqlString( sb ); + for ( int i = 1; i < arguments.size(); i++ ) { + sb.append( ',' ); + arguments.get( i ).appendHqlString( sb ); + } + sb.append( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/AliasedExpression.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/AliasedExpression.java index c685736002c4..822ce73da407 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/AliasedExpression.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/AliasedExpression.java @@ -36,4 +36,11 @@ public JdbcMappingContainer getExpressionType() { return expression.getExpressionType(); } + public Expression getExpression() { + return expression; + } + + public String getAlias() { + return alias; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlAttributes.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlAttributes.java new file mode 100644 index 000000000000..2df5bb775e50 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/XmlAttributes.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.ast.tree.expression; + +import java.util.Map; + +import org.hibernate.sql.ast.SqlAstWalker; +import org.hibernate.sql.ast.tree.SqlAstNode; + +/** + * @since 7.0 + */ +public class XmlAttributes implements SqlAstNode { + + private final Map attributes; + + public XmlAttributes(Map attributes) { + this.attributes = attributes; + } + + public Map getAttributes() { + return attributes; + } + + @Override + public void accept(SqlAstWalker sqlTreeWalker) { + throw new UnsupportedOperationException("XmlAttributes doesn't support walking"); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/FunctionTableReference.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/FunctionTableReference.java index 3315ec11e7ce..89f65a558632 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/FunctionTableReference.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/FunctionTableReference.java @@ -36,7 +36,7 @@ public FunctionExpression getFunctionExpression() { @Override public void accept(SqlAstWalker sqlTreeWalker) { - functionExpression.accept( sqlTreeWalker ); + sqlTreeWalker.visitFunctionTableReference( this ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java b/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java index d3b3ab441ec6..d033705c9a3e 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java +++ b/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java @@ -1021,4 +1021,36 @@ public static boolean isImplicitJsonType(int typeCode) { return isCharacterOrClobType( typeCode ); } } + + /** + * Does the typecode represent a XML type. + * + * @param typeCode - a JDBC type code + * @since 7.0 + */ + public static boolean isXmlType(int typeCode) { + switch ( typeCode ) { + case SQLXML: + case XML_ARRAY: + return true; + default: + return false; + } + } + + /** + * Does the typecode represent an XML type or a type that can be implicitly cast to XML. + * + * @param typeCode - a JDBC type code + * @since 7.0 + */ + public static boolean isImplicitXmlType(int typeCode) { + switch ( typeCode ) { + case SQLXML: + case XML_ARRAY: + return true; + default: + return isCharacterOrClobType( typeCode ); + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcType.java index 2206c4040670..bff937c0592e 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcType.java @@ -435,6 +435,16 @@ default boolean isImplicitJson() { return isImplicitJsonType( getDefaultSqlTypeCode() ); } + @Incubating + default boolean isXml() { + return isXmlType( getDefaultSqlTypeCode() ); + } + + @Incubating + default boolean isImplicitXml() { + return isImplicitXmlType( getDefaultSqlTypeCode() ); + } + @Incubating default boolean isBoolean() { return getDefaultSqlTypeCode() == BOOLEAN; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlAggTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlAggTest.java new file mode 100644 index 000000000000..aeacfdc819ea --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlAggTest.java @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.function.xml; + +import org.hibernate.cfg.QuerySettings; + +import org.hibernate.testing.orm.domain.StandardDomainModel; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +/** + * @author Christian Beikov + */ +@DomainModel(standardModels = StandardDomainModel.GAMBIT) +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.XML_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsXmlagg.class) +public class XmlAggTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-xmlagg-example[] + em.createQuery( "select xmlagg(xmlelement(name a, e.theString) order by e.id) from EntityOfBasics e" ).getResultList(); + //end::hql-xmlagg-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlCommentTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlCommentTest.java new file mode 100644 index 000000000000..f2264135763f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlCommentTest.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.function.xml; + +import org.hibernate.cfg.QuerySettings; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +/** + * @author Christian Beikov + */ +@DomainModel +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.XML_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsXmlcomment.class) +public class XmlCommentTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-xmlcomment-example[] + em.createQuery( "select xmlcomment('This is my comment')" ).getResultList(); + //end::hql-xmlcomment-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlConcatTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlConcatTest.java new file mode 100644 index 000000000000..c0d0d9790ca0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlConcatTest.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.function.xml; + +import org.hibernate.cfg.QuerySettings; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +/** + * @author Christian Beikov + */ +@DomainModel +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.XML_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsXmlconcat.class) +public class XmlConcatTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-xmlconcat-example[] + em.createQuery( "select xmlconcat(xmlelement(name e1, 123), xmlelement(name e2, 'text'))" ).getResultList(); + //end::hql-xmlconcat-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlElementTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlElementTest.java new file mode 100644 index 000000000000..cd2e669d0bda --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlElementTest.java @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.function.xml; + +import org.hibernate.cfg.QuerySettings; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +/** + * @author Christian Beikov + */ +@DomainModel +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.XML_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsXmlelement.class) +public class XmlElementTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-xmlelement-example[] + em.createQuery( "select xmlelement(name myelement)" ).getResultList(); + //end::hql-xmlelement-example[] + } ); + } + + @Test + public void testAttributesAndContent(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-xmlelement-attributes-content-example[] + em.createQuery("select xmlelement(name `my-element`, xmlattributes(123 as attr1, '456' as `attr-2`), 'myContent', xmlelement(name empty))" ).getResultList(); + //end::hql-xmlelement-attributes-content-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlExistsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlExistsTest.java new file mode 100644 index 000000000000..e3cc31fca068 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlExistsTest.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.function.xml; + +import org.hibernate.cfg.QuerySettings; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +/** + * @author Christian Beikov + */ +@DomainModel +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.XML_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsXmlexists.class) +public class XmlExistsTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-xmlexists-example[] + em.createQuery( "select xmlexists('/a/val' passing 'asd')" ).getResultList(); + //end::hql-xmlexists-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlForestTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlForestTest.java new file mode 100644 index 000000000000..e4c14cf623d5 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlForestTest.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.function.xml; + +import org.hibernate.cfg.QuerySettings; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +/** + * @author Christian Beikov + */ +@DomainModel +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.XML_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsXmlforest.class) +public class XmlForestTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-xmlforest-example[] + em.createQuery( "select xmlforest(123 as e1, 'text' as e2)" ).getResultList(); + //end::hql-xmlforest-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlPiTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlPiTest.java new file mode 100644 index 000000000000..7dbb80490284 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlPiTest.java @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.function.xml; + +import org.hibernate.cfg.QuerySettings; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +/** + * @author Christian Beikov + */ +@DomainModel +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.XML_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsXmlpi.class) +public class XmlPiTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-xmlpi-example[] + em.createQuery( "select xmlpi(name php)" ).getResultList(); + //end::hql-xmlpi-example[] + } ); + } + + @Test + public void testContent(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-xmlpi-content-example[] + em.createQuery("select xmlpi(name `php`, 'echo \"test\"')" ).getResultList(); + //end::hql-xmlpi-content-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlQueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlQueryTest.java new file mode 100644 index 000000000000..e6b696ffaca3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlQueryTest.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.function.xml; + +import org.hibernate.cfg.QuerySettings; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +/** + * @author Christian Beikov + */ +@DomainModel +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.XML_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsXmlquery.class) +public class XmlQueryTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-xmlquery-example[] + em.createQuery( "select xmlquery('/a/val' passing 'asd')" ).getResultList(); + //end::hql-xmlquery-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/XmlFunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/XmlFunctionTests.java new file mode 100644 index 000000000000..64158947ba68 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/XmlFunctionTests.java @@ -0,0 +1,318 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.hql; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.cfg.QuerySettings; +import org.hibernate.type.SqlTypes; + +import org.hibernate.testing.orm.domain.gambit.EntityOfBasics; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Tuple; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DomainModel( annotatedClasses = { + XmlFunctionTests.XmlHolder.class, + EntityOfBasics.class +}) +@ServiceRegistry(settings = @Setting(name = QuerySettings.XML_FUNCTIONS_ENABLED, value = "true")) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-18497") +public class XmlFunctionTests { + + XmlHolder entity; + + @BeforeEach + public void prepareData(SessionFactoryScope scope) { + scope.inTransaction( + em -> { + entity = new XmlHolder(); + entity.id = 1L; + entity.xml = new HashMap<>(); + entity.xml.put( "theInt", 1 ); + entity.xml.put( "theFloat", 0.1 ); + entity.xml.put( "theString", "abc" ); + entity.xml.put( "theBoolean", true ); + entity.xml.put( "theNull", null ); + entity.xml.put( "theArray", new String[] { "a", "b", "c" } ); + entity.xml.put( "theObject", new HashMap<>( entity.xml ) ); + entity.xml.put( + "theNestedObjects", + List.of( + Map.of( "id", 1, "name", "val1" ), + Map.of( "id", 2, "name", "val2" ), + Map.of( "id", 3, "name", "val3" ) + ) + ); + em.persist(entity); + + EntityOfBasics e1 = new EntityOfBasics(); + e1.setId( 1 ); + e1.setTheString( "Dog" ); + e1.setTheInteger( 0 ); + e1.setTheUuid( UUID.randomUUID() ); + EntityOfBasics e2 = new EntityOfBasics(); + e2.setId( 2 ); + e2.setTheString( "Cat" ); + e2.setTheInteger( 0 ); + + em.persist( e1 ); + em.persist( e2 ); + } + ); + } + + @AfterEach + public void cleanupData(SessionFactoryScope scope) { + scope.inTransaction( + em -> { + em.createMutationQuery( "delete from EntityOfBasics" ).executeUpdate(); + em.createMutationQuery( "delete from XmlHolder" ).executeUpdate(); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsXmlelement.class) + public void testXmlelement(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Tuple tuple = session.createQuery( + "select " + + "xmlelement(name empty), " + + "xmlelement(name `the-element`), " + + "xmlelement(name myElement, 'myContent'), " + + "xmlelement(name myElement, xmlattributes('123' as attr1)), " + + "xmlelement(name myElement, xmlattributes('123' as attr1, '456' as `attr-2`)), " + + "xmlelement(name myElement, xmlattributes('123' as attr1), 'myContent', xmlelement(name empty))", + Tuple.class + ).getSingleResult(); + assertXmlEquals( "", tuple.get( 0, String.class ) ); + assertXmlEquals( "", tuple.get( 1 , String.class ) ); + assertXmlEquals( "myContent", tuple.get( 2, String.class ) ); + assertXmlEquals( "", tuple.get( 3, String.class ) ); + assertXmlEquals( "", tuple.get( 4, String.class ) ); + assertXmlEquals( "myContent", tuple.get( 5, String.class ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsXmlcomment.class) + public void testXmlcomment(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Tuple tuple = session.createQuery( + "select " + + "xmlcomment('Abc'), " + + "xmlcomment('<>')", + Tuple.class + ).getSingleResult(); + assertXmlEquals( "", tuple.get( 0, String.class ) + "" ); + assertXmlEquals( "", tuple.get( 1 , String.class ) + "" ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsXmlforest.class) + public void testXmlforest(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Tuple tuple = session.createQuery( + "select xmlforest(123 as e1, 'text' as e2)," + + "xmlforest(e.id, e.theString) " + + "from EntityOfBasics e where e.id = 1", + Tuple.class + ).getSingleResult(); + assertXmlEquals( "123text", "" + tuple.get( 0, String.class ) + "" ); + assertXmlEquals( "1Dog", "" + tuple.get( 1, String.class ) + "" ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsXmlconcat.class) + public void testXmlconcat(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Tuple tuple = session.createQuery( + "select xmlconcat(xmlelement(name e1, 123), xmlelement(name e2, 'text'))," + + "xmlconcat(xmlelement(name id, e.id), xmlelement(name theString, e.theString)) " + + "from EntityOfBasics e where e.id = 1", + Tuple.class + ).getSingleResult(); + assertXmlEquals( "123text", "" + tuple.get( 0, String.class ) + "" ); + assertXmlEquals( "1Dog", "" + tuple.get( 1, String.class ) + "" ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsXmlpi.class) + public void testXmlpi(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Tuple tuple = session.createQuery( + "select xmlpi(name test, 'abc')", + Tuple.class + ).getSingleResult(); + assertEquals( "", tuple.get( 0, String.class ).trim() ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsXmlquery.class) + public void testXmlquery(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Tuple tuple = session.createQuery( + "select xmlquery('/a/val' passing 'asd')", + Tuple.class + ).getSingleResult(); + assertXmlEquals( "asd", tuple.get( 0, String.class ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsXmlexists.class) + public void testXmlexists(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Tuple tuple = session.createQuery( + "select xmlexists('/a/val' passing 'asd')", + Tuple.class + ).getSingleResult(); + assertTrue( tuple.get( 0, Boolean.class ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsXmlagg.class) + public void testXmlagg(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Tuple tuple = session.createQuery( + "select xmlagg(xmlelement(name a, e.theString) order by e.id) " + + "from from EntityOfBasics e", + Tuple.class + ).getSingleResult(); + assertXmlEquals( "DogCat", "" + tuple.get( 0, String.class ) + "" ); + } + ); + } + + 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 96e6c11a734e..b5403949a75c 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 @@ -835,6 +835,54 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsXmlelement implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "xmlelement" ); + } + } + + public static class SupportsXmlcomment implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "xmlcomment" ); + } + } + + public static class SupportsXmlforest implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "xmlforest" ); + } + } + + public static class SupportsXmlconcat implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "xmlconcat" ); + } + } + + public static class SupportsXmlpi implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "xmlpi" ); + } + } + + public static class SupportsXmlquery implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "xmlquery" ); + } + } + + public static class SupportsXmlexists implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "xmlexists" ); + } + } + + public static class SupportsXmlagg implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "xmlagg" ); + } + } + public static class IsJtds implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return dialect instanceof SybaseDialect && ( (SybaseDialect) dialect ).getDriverKind() == SybaseDriverKind.JTDS;