mirror of
https://github.com/hibernate/hibernate-orm
synced 2025-02-17 16:44:57 +00:00
HHH-18759 Add xmltable() set-returning function
This commit is contained in:
parent
854a982927
commit
e1a8990358
@ -2257,14 +2257,15 @@ it is necessary to enable the `hibernate.query.hql.xml_functions_enabled` config
|
|||||||
|===
|
|===
|
||||||
| Function | Purpose
|
| Function | Purpose
|
||||||
|
|
||||||
| `xmlelement()` | Constructs an XML element from arguments
|
| <<hql-xmlelement-function,`xmlelement()`>> | Constructs an XML element from arguments
|
||||||
| `xmlcomment()` | Constructs an XML comment from the single argument
|
| <<hql-xmlcomment-function,`xmlcomment()`>> | Constructs an XML comment from the single argument
|
||||||
| `xmlforest()` | Constructs an XML forest from the arguments
|
| <<hql-xmlforest-function,`xmlforest()`>> | Constructs an XML forest from the arguments
|
||||||
| `xmlconcat()` | Concatenates multiple XML fragments to each other
|
| <<hql-xmlconcat-function,`xmlconcat()`>> | Concatenates multiple XML fragments to each other
|
||||||
| `xmlpi()` | Constructs an XML processing instruction
|
| <<hql-xmlpi-function,`xmlpi()`>> | Constructs an XML processing instruction
|
||||||
| `xmlquery()` | Extracts content from XML document using XQuery or XPath
|
| <<hql-xmlquery-function,`xmlquery()`>> | Extracts content from XML document using XQuery or XPath
|
||||||
| `xmlexists()` | Checks if an XQuery or XPath expression exists in an XML document
|
| <<hql-xmlexists-function,`xmlexists()`>> | Checks if an XQuery or XPath expression exists in an XML document
|
||||||
| `xmlagg()` | Aggregates XML elements by concatenation
|
| <<hql-xmlagg-function,`xmlagg()`>> | Aggregates XML elements by concatenation
|
||||||
|
| <<hql-xmltable-function,`xmltable()`>> | Turns an XML document into rows
|
||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
@ -2461,6 +2462,39 @@ include::{xml-example-dir-hql}/XmlAggTest.java[tags=hql-xmlagg-example]
|
|||||||
|
|
||||||
WARNING: SAP HANA, MySQL, MariaDB and HSQLDB do not support this function.
|
WARNING: SAP HANA, MySQL, MariaDB and HSQLDB do not support this function.
|
||||||
|
|
||||||
|
[[hql-xmltable-function]]
|
||||||
|
===== `xmltable()`
|
||||||
|
|
||||||
|
A <<hql-from-set-returning-functions,set-returning function>>, which turns an XML document argument into rows.
|
||||||
|
Returns no rows if the document is `null` or the XPath expression resolves to no nodes.
|
||||||
|
|
||||||
|
[[hql-xmltable-bnf]]
|
||||||
|
[source, antlrv4, indent=0]
|
||||||
|
----
|
||||||
|
include::{extrasdir}/xmltable_bnf.txt[]
|
||||||
|
----
|
||||||
|
|
||||||
|
The first argument is the XPath expression. The second argument represents the XML document expression.
|
||||||
|
|
||||||
|
Columns that ought to be accessible via the `from` node alias are defined in the `columns` clause,
|
||||||
|
which can be of varying forms:
|
||||||
|
|
||||||
|
* Value attributes - denoted by a `castTarget` after the name, will cast the content of the XML node matching the XPath expression of the column
|
||||||
|
* Query attributes - denoted by the `xml` type after the name, returns the XML node matching the XPath expression of the column
|
||||||
|
* Ordinal attributes - denoted by the `for ordinality` syntax after the name, gives access to the 1-based index of the currently processed XML node
|
||||||
|
|
||||||
|
[[hql-xmltable-simple-example]]
|
||||||
|
====
|
||||||
|
[source, java, indent=0]
|
||||||
|
----
|
||||||
|
include::{xml-example-dir-hql}/XmlTableTest.java[tags=hql-xml-table-example]
|
||||||
|
----
|
||||||
|
====
|
||||||
|
|
||||||
|
The `lateral` keyword is mandatory if one of the arguments refer to a from node item of the same query level.
|
||||||
|
|
||||||
|
WARNING: H2, MySQL, MariaDB and HSQLDB do not support this function.
|
||||||
|
|
||||||
[[hql-user-defined-functions]]
|
[[hql-user-defined-functions]]
|
||||||
==== Native and user-defined functions
|
==== Native and user-defined functions
|
||||||
|
|
||||||
@ -3001,7 +3035,8 @@ The following set-returning functions are available on many platforms:
|
|||||||
|
|
||||||
| <<hql-array-unnest,`unnest()`>> | Turns an array into rows
|
| <<hql-array-unnest,`unnest()`>> | Turns an array into rows
|
||||||
| <<hql-from-set-returning-functions-generate-series,`generate_series()`>> | Creates a series of values as rows
|
| <<hql-from-set-returning-functions-generate-series,`generate_series()`>> | Creates a series of values as rows
|
||||||
| <<hql-json-table,`json_table()`>> | Turns a JSON document into rows
|
| <<hql-json-table-function,`json_table()`>> | Turns a JSON document into rows
|
||||||
|
| <<hql-xmltable-function,`xmltable()`>> | Turns an XML document into rows
|
||||||
|===
|
|===
|
||||||
|
|
||||||
To use set returning functions defined in the database, it is required to register them in a `FunctionContributor`:
|
To use set returning functions defined in the database, it is required to register them in a `FunctionContributor`:
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
"xmltable(" expression "passing" expression columnsClause ")"
|
||||||
|
|
||||||
|
columnsClause
|
||||||
|
: "columns" column ("," column)*
|
||||||
|
|
||||||
|
column
|
||||||
|
: attributeName "xml" ("path" STRING_LITERAL)? defaultClause?
|
||||||
|
| attributeName "for ordinality"
|
||||||
|
| attributeName castTarget ("path" STRING_LITERAL)? defaultClause?
|
||||||
|
|
||||||
|
defaultClause
|
||||||
|
: "default" expression;
|
@ -457,6 +457,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
|
|||||||
functionFactory.xmlexists_db2_legacy();
|
functionFactory.xmlexists_db2_legacy();
|
||||||
}
|
}
|
||||||
functionFactory.xmlagg();
|
functionFactory.xmlagg();
|
||||||
|
functionFactory.xmltable_db2();
|
||||||
|
|
||||||
functionFactory.unnest_emulated();
|
functionFactory.unnest_emulated();
|
||||||
if ( supportsRecursiveCTE() ) {
|
if ( supportsRecursiveCTE() ) {
|
||||||
|
@ -508,7 +508,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
|
|||||||
functionFactory.jsonObjectAgg_hana();
|
functionFactory.jsonObjectAgg_hana();
|
||||||
}
|
}
|
||||||
|
|
||||||
// functionFactory.xmltable();
|
functionFactory.xmltable_hana();
|
||||||
}
|
}
|
||||||
|
|
||||||
// functionFactory.xmlextract();
|
// functionFactory.xmlextract();
|
||||||
|
@ -334,6 +334,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
|
|||||||
functionFactory.xmlquery_oracle();
|
functionFactory.xmlquery_oracle();
|
||||||
functionFactory.xmlexists();
|
functionFactory.xmlexists();
|
||||||
functionFactory.xmlagg();
|
functionFactory.xmlagg();
|
||||||
|
functionFactory.xmltable_oracle();
|
||||||
|
|
||||||
functionFactory.unnest_oracle();
|
functionFactory.unnest_oracle();
|
||||||
functionFactory.generateSeries_recursive( getMaximumSeriesSize(), true, false );
|
functionFactory.generateSeries_recursive( getMaximumSeriesSize(), true, false );
|
||||||
|
@ -678,6 +678,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
|
|||||||
functionFactory.xmlquery_postgresql();
|
functionFactory.xmlquery_postgresql();
|
||||||
functionFactory.xmlexists();
|
functionFactory.xmlexists();
|
||||||
functionFactory.xmlagg();
|
functionFactory.xmlagg();
|
||||||
|
functionFactory.xmltable( true );
|
||||||
|
|
||||||
if ( getVersion().isSameOrAfter( 9, 4 ) ) {
|
if ( getVersion().isSameOrAfter( 9, 4 ) ) {
|
||||||
functionFactory.makeDateTimeTimestamp();
|
functionFactory.makeDateTimeTimestamp();
|
||||||
|
@ -429,6 +429,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
|
|||||||
functionFactory.xmlquery_sqlserver();
|
functionFactory.xmlquery_sqlserver();
|
||||||
functionFactory.xmlexists_sqlserver();
|
functionFactory.xmlexists_sqlserver();
|
||||||
functionFactory.xmlagg_sqlserver();
|
functionFactory.xmlagg_sqlserver();
|
||||||
|
functionFactory.xmltable_sqlserver();
|
||||||
|
|
||||||
functionFactory.unnest_sqlserver();
|
functionFactory.unnest_sqlserver();
|
||||||
|
|
||||||
|
@ -167,6 +167,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
|
|||||||
|
|
||||||
functionFactory.unnest_sybasease();
|
functionFactory.unnest_sybasease();
|
||||||
functionFactory.generateSeries_sybasease( getMaximumSeriesSize() );
|
functionFactory.generateSeries_sybasease( getMaximumSeriesSize() );
|
||||||
|
functionFactory.xmltable_sybasease();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -336,6 +336,7 @@ WITH : [wW] [iI] [tT] [hH];
|
|||||||
WITHIN : [wW] [iI] [tT] [hH] [iI] [nN];
|
WITHIN : [wW] [iI] [tT] [hH] [iI] [nN];
|
||||||
WITHOUT : [wW] [iI] [tT] [hH] [oO] [uU] [tT];
|
WITHOUT : [wW] [iI] [tT] [hH] [oO] [uU] [tT];
|
||||||
WRAPPER : [wW] [rR] [aA] [pP] [pP] [eE] [rR];
|
WRAPPER : [wW] [rR] [aA] [pP] [pP] [eE] [rR];
|
||||||
|
XML : [xX] [mM] [lL];
|
||||||
XMLAGG : [xX] [mM] [lL] [aA] [gG] [gG];
|
XMLAGG : [xX] [mM] [lL] [aA] [gG] [gG];
|
||||||
XMLATTRIBUTES : [xX] [mM] [lL] [aA] [tT] [tT] [rR] [iI] [bB] [uU] [tT] [eE] [sS];
|
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];
|
XMLELEMENT : [xX] [mM] [lL] [eE] [lL] [eE] [mM] [eE] [nN] [tT];
|
||||||
@ -343,6 +344,7 @@ XMLEXISTS : [xX] [mM] [lL] [eE] [xX] [iI] [sS] [tT] [sS];
|
|||||||
XMLFOREST : [xX] [mM] [lL] [fF] [oO] [rR] [eE] [sS] [tT];
|
XMLFOREST : [xX] [mM] [lL] [fF] [oO] [rR] [eE] [sS] [tT];
|
||||||
XMLPI : [xX] [mM] [lL] [pP] [iI];
|
XMLPI : [xX] [mM] [lL] [pP] [iI];
|
||||||
XMLQUERY : [xX] [mM] [lL] [qQ] [uU] [eE] [rR] [yY];
|
XMLQUERY : [xX] [mM] [lL] [qQ] [uU] [eE] [rR] [yY];
|
||||||
|
XMLTABLE : [xX] [mM] [lL] [tT] [aA] [bB] [lL] [eE];
|
||||||
YEAR : [yY] [eE] [aA] [rR];
|
YEAR : [yY] [eE] [aA] [rR];
|
||||||
ZONED : [zZ] [oO] [nN] [eE] [dD];
|
ZONED : [zZ] [oO] [nN] [eE] [dD];
|
||||||
|
|
||||||
|
@ -1119,6 +1119,7 @@ function
|
|||||||
setReturningFunction
|
setReturningFunction
|
||||||
: simpleSetReturningFunction
|
: simpleSetReturningFunction
|
||||||
| jsonTableFunction
|
| jsonTableFunction
|
||||||
|
| xmltableFunction
|
||||||
;
|
;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1813,6 +1814,24 @@ xmlaggFunction
|
|||||||
: XMLAGG LEFT_PAREN expression orderByClause? RIGHT_PAREN filterClause? overClause?
|
: XMLAGG LEFT_PAREN expression orderByClause? RIGHT_PAREN filterClause? overClause?
|
||||||
;
|
;
|
||||||
|
|
||||||
|
xmltableFunction
|
||||||
|
: XMLTABLE LEFT_PAREN expression PASSING expression xmltableColumnsClause RIGHT_PAREN
|
||||||
|
;
|
||||||
|
|
||||||
|
xmltableColumnsClause
|
||||||
|
: COLUMNS xmltableColumn (COMMA xmltableColumn)*
|
||||||
|
;
|
||||||
|
|
||||||
|
xmltableColumn
|
||||||
|
: identifier XML (PATH STRING_LITERAL)? xmltableDefaultClause? # XmlTableQueryColumn
|
||||||
|
| identifier FOR ORDINALITY # XmlTableOrdinalityColumn
|
||||||
|
| identifier castTarget (PATH STRING_LITERAL)? xmltableDefaultClause? # XmlTableValueColumn
|
||||||
|
;
|
||||||
|
|
||||||
|
xmltableDefaultClause
|
||||||
|
: DEFAULT expression
|
||||||
|
;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Support for "soft" keywords which may be used as identifiers
|
* Support for "soft" keywords which may be used as identifiers
|
||||||
*
|
*
|
||||||
@ -2025,6 +2044,7 @@ xmlaggFunction
|
|||||||
| WITHIN
|
| WITHIN
|
||||||
| WITHOUT
|
| WITHOUT
|
||||||
| WRAPPER
|
| WRAPPER
|
||||||
|
| XML
|
||||||
| XMLAGG
|
| XMLAGG
|
||||||
| XMLATTRIBUTES
|
| XMLATTRIBUTES
|
||||||
| XMLELEMENT
|
| XMLELEMENT
|
||||||
@ -2032,6 +2052,7 @@ xmlaggFunction
|
|||||||
| XMLFOREST
|
| XMLFOREST
|
||||||
| XMLPI
|
| XMLPI
|
||||||
| XMLQUERY
|
| XMLQUERY
|
||||||
|
| XMLTABLE
|
||||||
| YEAR
|
| YEAR
|
||||||
| ZONED) {
|
| ZONED) {
|
||||||
logUseOfReservedWordAsIdentifier( getCurrentToken() );
|
logUseOfReservedWordAsIdentifier( getCurrentToken() );
|
||||||
|
@ -442,6 +442,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
|
|||||||
functionFactory.xmlexists_db2_legacy();
|
functionFactory.xmlexists_db2_legacy();
|
||||||
}
|
}
|
||||||
functionFactory.xmlagg();
|
functionFactory.xmlagg();
|
||||||
|
functionFactory.xmltable_db2();
|
||||||
|
|
||||||
functionFactory.unnest_emulated();
|
functionFactory.unnest_emulated();
|
||||||
functionFactory.generateSeries_recursive( getMaximumSeriesSize(), false, true );
|
functionFactory.generateSeries_recursive( getMaximumSeriesSize(), false, true );
|
||||||
|
@ -511,7 +511,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
|
|||||||
functionFactory.jsonArrayAgg_hana();
|
functionFactory.jsonArrayAgg_hana();
|
||||||
functionFactory.jsonObjectAgg_hana();
|
functionFactory.jsonObjectAgg_hana();
|
||||||
|
|
||||||
// functionFactory.xmltable();
|
functionFactory.xmltable_hana();
|
||||||
|
|
||||||
// functionFactory.xmlextract();
|
// functionFactory.xmlextract();
|
||||||
functionFactory.generateSeries_hana( getMaximumSeriesSize() );
|
functionFactory.generateSeries_hana( getMaximumSeriesSize() );
|
||||||
|
@ -421,6 +421,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
|
|||||||
functionFactory.xmlquery_oracle();
|
functionFactory.xmlquery_oracle();
|
||||||
functionFactory.xmlexists();
|
functionFactory.xmlexists();
|
||||||
functionFactory.xmlagg();
|
functionFactory.xmlagg();
|
||||||
|
functionFactory.xmltable_oracle();
|
||||||
|
|
||||||
functionFactory.unnest_oracle();
|
functionFactory.unnest_oracle();
|
||||||
functionFactory.generateSeries_recursive( getMaximumSeriesSize(), true, false );
|
functionFactory.generateSeries_recursive( getMaximumSeriesSize(), true, false );
|
||||||
|
@ -640,6 +640,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
|
|||||||
functionFactory.xmlquery_postgresql();
|
functionFactory.xmlquery_postgresql();
|
||||||
functionFactory.xmlexists();
|
functionFactory.xmlexists();
|
||||||
functionFactory.xmlagg();
|
functionFactory.xmlagg();
|
||||||
|
functionFactory.xmltable( true );
|
||||||
|
|
||||||
functionFactory.makeDateTimeTimestamp();
|
functionFactory.makeDateTimeTimestamp();
|
||||||
// Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions
|
// Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions
|
||||||
|
@ -446,6 +446,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
|
|||||||
functionFactory.xmlquery_sqlserver();
|
functionFactory.xmlquery_sqlserver();
|
||||||
functionFactory.xmlexists_sqlserver();
|
functionFactory.xmlexists_sqlserver();
|
||||||
functionFactory.xmlagg_sqlserver();
|
functionFactory.xmlagg_sqlserver();
|
||||||
|
functionFactory.xmltable_sqlserver();
|
||||||
|
|
||||||
functionFactory.unnest_sqlserver();
|
functionFactory.unnest_sqlserver();
|
||||||
|
|
||||||
|
@ -184,6 +184,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
|
|||||||
|
|
||||||
functionFactory.unnest_sybasease();
|
functionFactory.unnest_sybasease();
|
||||||
functionFactory.generateSeries_sybasease( getMaximumSeriesSize() );
|
functionFactory.generateSeries_sybasease( getMaximumSeriesSize() );
|
||||||
|
functionFactory.xmltable_sybasease();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,12 +13,15 @@
|
|||||||
|
|
||||||
import org.hibernate.dialect.function.array.*;
|
import org.hibernate.dialect.function.array.*;
|
||||||
import org.hibernate.dialect.function.json.*;
|
import org.hibernate.dialect.function.json.*;
|
||||||
|
import org.hibernate.dialect.function.xml.DB2XmlTableFunction;
|
||||||
import org.hibernate.dialect.function.xml.H2XmlConcatFunction;
|
import org.hibernate.dialect.function.xml.H2XmlConcatFunction;
|
||||||
import org.hibernate.dialect.function.xml.H2XmlElementFunction;
|
import org.hibernate.dialect.function.xml.H2XmlElementFunction;
|
||||||
import org.hibernate.dialect.function.xml.H2XmlForestFunction;
|
import org.hibernate.dialect.function.xml.H2XmlForestFunction;
|
||||||
import org.hibernate.dialect.function.xml.H2XmlPiFunction;
|
import org.hibernate.dialect.function.xml.H2XmlPiFunction;
|
||||||
|
import org.hibernate.dialect.function.xml.HANAXmlTableFunction;
|
||||||
import org.hibernate.dialect.function.xml.LegacyDB2XmlExistsFunction;
|
import org.hibernate.dialect.function.xml.LegacyDB2XmlExistsFunction;
|
||||||
import org.hibernate.dialect.function.xml.LegacyDB2XmlQueryFunction;
|
import org.hibernate.dialect.function.xml.LegacyDB2XmlQueryFunction;
|
||||||
|
import org.hibernate.dialect.function.xml.OracleXmlTableFunction;
|
||||||
import org.hibernate.dialect.function.xml.PostgreSQLXmlQueryFunction;
|
import org.hibernate.dialect.function.xml.PostgreSQLXmlQueryFunction;
|
||||||
import org.hibernate.dialect.function.xml.SQLServerXmlAggFunction;
|
import org.hibernate.dialect.function.xml.SQLServerXmlAggFunction;
|
||||||
import org.hibernate.dialect.function.xml.SQLServerXmlConcatFunction;
|
import org.hibernate.dialect.function.xml.SQLServerXmlConcatFunction;
|
||||||
@ -27,6 +30,8 @@
|
|||||||
import org.hibernate.dialect.function.xml.SQLServerXmlForestFunction;
|
import org.hibernate.dialect.function.xml.SQLServerXmlForestFunction;
|
||||||
import org.hibernate.dialect.function.xml.SQLServerXmlPiFunction;
|
import org.hibernate.dialect.function.xml.SQLServerXmlPiFunction;
|
||||||
import org.hibernate.dialect.function.xml.SQLServerXmlQueryFunction;
|
import org.hibernate.dialect.function.xml.SQLServerXmlQueryFunction;
|
||||||
|
import org.hibernate.dialect.function.xml.SQLServerXmlTableFunction;
|
||||||
|
import org.hibernate.dialect.function.xml.SybaseASEXmlTableFunction;
|
||||||
import org.hibernate.dialect.function.xml.XmlAggFunction;
|
import org.hibernate.dialect.function.xml.XmlAggFunction;
|
||||||
import org.hibernate.dialect.function.xml.XmlConcatFunction;
|
import org.hibernate.dialect.function.xml.XmlConcatFunction;
|
||||||
import org.hibernate.dialect.function.xml.XmlElementFunction;
|
import org.hibernate.dialect.function.xml.XmlElementFunction;
|
||||||
@ -34,6 +39,7 @@
|
|||||||
import org.hibernate.dialect.function.xml.XmlForestFunction;
|
import org.hibernate.dialect.function.xml.XmlForestFunction;
|
||||||
import org.hibernate.dialect.function.xml.XmlPiFunction;
|
import org.hibernate.dialect.function.xml.XmlPiFunction;
|
||||||
import org.hibernate.dialect.function.xml.XmlQueryFunction;
|
import org.hibernate.dialect.function.xml.XmlQueryFunction;
|
||||||
|
import org.hibernate.dialect.function.xml.XmlTableFunction;
|
||||||
import org.hibernate.query.sqm.function.SqmFunctionRegistry;
|
import org.hibernate.query.sqm.function.SqmFunctionRegistry;
|
||||||
import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator;
|
import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator;
|
||||||
import org.hibernate.query.sqm.produce.function.FunctionParameterType;
|
import org.hibernate.query.sqm.produce.function.FunctionParameterType;
|
||||||
@ -4323,4 +4329,46 @@ public void jsonTable_sqlserver() {
|
|||||||
public void jsonTable_h2(int maximumArraySize) {
|
public void jsonTable_h2(int maximumArraySize) {
|
||||||
functionRegistry.register( "json_table", new H2JsonTableFunction( maximumArraySize, typeConfiguration ) );
|
functionRegistry.register( "json_table", new H2JsonTableFunction( maximumArraySize, typeConfiguration ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard xmltable() function
|
||||||
|
*/
|
||||||
|
public void xmltable(boolean supportsParametersInDefault) {
|
||||||
|
functionRegistry.register( "xmltable", new XmlTableFunction( supportsParametersInDefault, typeConfiguration ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Oracle xmltable() function
|
||||||
|
*/
|
||||||
|
public void xmltable_oracle() {
|
||||||
|
functionRegistry.register( "xmltable", new OracleXmlTableFunction( typeConfiguration ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DB2 xmltable() function
|
||||||
|
*/
|
||||||
|
public void xmltable_db2() {
|
||||||
|
functionRegistry.register( "xmltable", new DB2XmlTableFunction( typeConfiguration ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HANA xmltable() function
|
||||||
|
*/
|
||||||
|
public void xmltable_hana() {
|
||||||
|
functionRegistry.register( "xmltable", new HANAXmlTableFunction( typeConfiguration ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL Server xmltable() function
|
||||||
|
*/
|
||||||
|
public void xmltable_sqlserver() {
|
||||||
|
functionRegistry.register( "xmltable", new SQLServerXmlTableFunction( typeConfiguration ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sybase ASE xmltable() function
|
||||||
|
*/
|
||||||
|
public void xmltable_sybasease() {
|
||||||
|
functionRegistry.register( "xmltable", new SybaseASEXmlTableFunction( typeConfiguration ) );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -411,7 +411,7 @@ public void renderToSql(
|
|||||||
sessionFactory
|
sessionFactory
|
||||||
);
|
);
|
||||||
|
|
||||||
// Produce a XML string e.g. <root id="1">...</root>
|
// Produce an XML string e.g. <root id="1">...</root>
|
||||||
// which will contain the original XML as well as id column information for correlation
|
// which will contain the original XML as well as id column information for correlation
|
||||||
sqlAppender.appendSql( "trim('/>' from (select" );
|
sqlAppender.appendSql( "trim('/>' from (select" );
|
||||||
char separator = ' ';
|
char separator = ' ';
|
||||||
@ -424,8 +424,9 @@ public void renderToSql(
|
|||||||
sqlAppender.appendDoubleQuoteEscapedString( columnInfo.name() );
|
sqlAppender.appendDoubleQuoteEscapedString( columnInfo.name() );
|
||||||
separator = ',';
|
separator = ',';
|
||||||
}
|
}
|
||||||
sqlAppender.appendSql( " from sys.dummy for xml('root'='no','columnstyle'='attribute','rowname'='Strings','format'='no')))||" );
|
sqlAppender.appendSql( " from sys.dummy for xml('root'='no','columnstyle'='attribute','rowname'='" );
|
||||||
sqlAppender.appendSql( "substring(" );
|
sqlAppender.appendSql( collectionTags.rootName() );
|
||||||
|
sqlAppender.appendSql( "','format'='no')))||substring(" );
|
||||||
argument.accept( walker );
|
argument.accept( walker );
|
||||||
sqlAppender.appendSql( ",locate('<" );
|
sqlAppender.appendSql( ",locate('<" );
|
||||||
sqlAppender.appendSql( collectionTags.rootName() );
|
sqlAppender.appendSql( collectionTags.rootName() );
|
||||||
|
@ -63,7 +63,7 @@ protected void renderReturningClause(SqlAppender sqlAppender, JsonValueArguments
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean isEncodedBoolean(JdbcMapping type) {
|
public static boolean isEncodedBoolean(JdbcMapping type) {
|
||||||
return type.getJdbcType().isBoolean() && type.getJdbcType().getDdlTypeCode() != SqlTypes.BOOLEAN;
|
return type.getJdbcType().isBoolean() && type.getJdbcType().getDdlTypeCode() != SqlTypes.BOOLEAN;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
* Copyright Red Hat Inc. and Hibernate Authors
|
||||||
|
*/
|
||||||
|
package org.hibernate.dialect.function.xml;
|
||||||
|
|
||||||
|
import org.hibernate.dialect.Dialect;
|
||||||
|
import org.hibernate.engine.spi.SessionFactoryImplementor;
|
||||||
|
import org.hibernate.metamodel.mapping.JdbcMapping;
|
||||||
|
import org.hibernate.metamodel.mapping.SelectableMapping;
|
||||||
|
import org.hibernate.metamodel.mapping.SelectablePath;
|
||||||
|
import org.hibernate.metamodel.mapping.internal.SelectableMappingImpl;
|
||||||
|
import org.hibernate.query.derived.AnonymousTupleTableGroupProducer;
|
||||||
|
import org.hibernate.query.sqm.sql.SqmToSqlAstConverter;
|
||||||
|
import org.hibernate.sql.Template;
|
||||||
|
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
|
||||||
|
import org.hibernate.sql.ast.SqlAstTranslator;
|
||||||
|
import org.hibernate.sql.ast.spi.SqlAppender;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.CastTarget;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableValueColumnDefinition;
|
||||||
|
import org.hibernate.type.descriptor.WrapperOptions;
|
||||||
|
import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter;
|
||||||
|
import org.hibernate.type.spi.TypeConfiguration;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DB2 xmltable function.
|
||||||
|
*/
|
||||||
|
public class DB2XmlTableFunction extends XmlTableFunction {
|
||||||
|
|
||||||
|
public DB2XmlTableFunction(TypeConfiguration typeConfiguration) {
|
||||||
|
super( false, new DB2XmlTableSetReturningFunctionTypeResolver(), typeConfiguration );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void renderXmlTable(SqlAppender sqlAppender, XmlTableArguments arguments, AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstTranslator<?> walker) {
|
||||||
|
sqlAppender.appendSql( "xmltable(" );
|
||||||
|
// DB2 doesn't like parameters for the xpath expression
|
||||||
|
walker.render( arguments.xpath(), SqlAstNodeRenderingMode.INLINE_PARAMETERS );
|
||||||
|
sqlAppender.appendSql( " passing " );
|
||||||
|
if ( !arguments.isXmlType() ) {
|
||||||
|
sqlAppender.appendSql( "xmlparse(document " );
|
||||||
|
}
|
||||||
|
// DB2 needs parameters to be casted here
|
||||||
|
walker.render( arguments.xmlDocument(), SqlAstNodeRenderingMode.NO_PLAIN_PARAMETER );
|
||||||
|
if ( !arguments.isXmlType() ) {
|
||||||
|
sqlAppender.appendSql( ")" );
|
||||||
|
}
|
||||||
|
renderColumns( sqlAppender, arguments.columnsClause(), walker );
|
||||||
|
sqlAppender.appendSql( ')' );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String determineColumnType(CastTarget castTarget, SqlAstTranslator<?> walker) {
|
||||||
|
final String typeName = super.determineColumnType( castTarget, walker );
|
||||||
|
return isBoolean( castTarget.getJdbcMapping() ) ? "varchar(5)" : typeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void renderXmlValueColumnDefinition(SqlAppender sqlAppender, XmlTableValueColumnDefinition definition, SqlAstTranslator<?> walker) {
|
||||||
|
sqlAppender.appendSql( definition.name() );
|
||||||
|
sqlAppender.appendSql( ' ' );
|
||||||
|
sqlAppender.appendSql( determineColumnType( definition.type(), walker ) );
|
||||||
|
|
||||||
|
// DB2 wants the default before the path
|
||||||
|
renderDefaultExpression( definition.defaultExpression(), sqlAppender, walker );
|
||||||
|
renderColumnPath( definition.name(), definition.xpath(), sqlAppender, walker );
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isBoolean(JdbcMapping type) {
|
||||||
|
return type.getJdbcType().isBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DB2XmlTableSetReturningFunctionTypeResolver extends XmlTableSetReturningFunctionTypeResolver {
|
||||||
|
@Override
|
||||||
|
protected void addSelectableMapping(List<SelectableMapping> selectableMappings, String name, JdbcMapping type, SqmToSqlAstConverter converter) {
|
||||||
|
if ( isBoolean( type ) ) {
|
||||||
|
//noinspection unchecked
|
||||||
|
final JdbcLiteralFormatter<Object> jdbcLiteralFormatter = type.getJdbcLiteralFormatter();
|
||||||
|
final SessionFactoryImplementor sessionFactory = converter.getCreationContext().getSessionFactory();
|
||||||
|
final Dialect dialect = sessionFactory.getJdbcServices().getDialect();
|
||||||
|
final WrapperOptions wrapperOptions = sessionFactory.getWrapperOptions();
|
||||||
|
final Object trueValue = type.convertToRelationalValue( true );
|
||||||
|
final Object falseValue = type.convertToRelationalValue( false );
|
||||||
|
final String trueFragment = jdbcLiteralFormatter.toJdbcLiteral( trueValue, dialect, wrapperOptions );
|
||||||
|
final String falseFragment = jdbcLiteralFormatter.toJdbcLiteral( falseValue, dialect, wrapperOptions );
|
||||||
|
selectableMappings.add( new SelectableMappingImpl(
|
||||||
|
"",
|
||||||
|
name,
|
||||||
|
new SelectablePath( name ),
|
||||||
|
"decode(" + Template.TEMPLATE + "." + name + ",'true'," + trueFragment + ",'false'," + falseFragment + ")",
|
||||||
|
null,
|
||||||
|
"varchar(5)",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
type
|
||||||
|
));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
super.addSelectableMapping( selectableMappings, name, type, converter );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,454 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
* Copyright Red Hat Inc. and Hibernate Authors
|
||||||
|
*/
|
||||||
|
package org.hibernate.dialect.function.xml;
|
||||||
|
|
||||||
|
import org.hibernate.QueryException;
|
||||||
|
import org.hibernate.dialect.Dialect;
|
||||||
|
import org.hibernate.engine.spi.SessionFactoryImplementor;
|
||||||
|
import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart;
|
||||||
|
import org.hibernate.metamodel.mapping.EntityMappingType;
|
||||||
|
import org.hibernate.metamodel.mapping.EntityValuedModelPart;
|
||||||
|
import org.hibernate.metamodel.mapping.JdbcMapping;
|
||||||
|
import org.hibernate.metamodel.mapping.JdbcMappingContainer;
|
||||||
|
import org.hibernate.metamodel.mapping.ModelPartContainer;
|
||||||
|
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
|
||||||
|
import org.hibernate.metamodel.mapping.SelectableMapping;
|
||||||
|
import org.hibernate.metamodel.mapping.SelectablePath;
|
||||||
|
import org.hibernate.metamodel.mapping.ValuedModelPart;
|
||||||
|
import org.hibernate.metamodel.mapping.internal.EmbeddedCollectionPart;
|
||||||
|
import org.hibernate.metamodel.mapping.internal.SelectableMappingImpl;
|
||||||
|
import org.hibernate.query.derived.AnonymousTupleTableGroupProducer;
|
||||||
|
import org.hibernate.query.spi.QueryEngine;
|
||||||
|
import org.hibernate.query.sqm.ComparisonOperator;
|
||||||
|
import org.hibernate.query.sqm.function.SelfRenderingSqmSetReturningFunction;
|
||||||
|
import org.hibernate.query.sqm.sql.SqmToSqlAstConverter;
|
||||||
|
import org.hibernate.query.sqm.tree.SqmTypedNode;
|
||||||
|
import org.hibernate.query.sqm.tree.expression.SqmExpression;
|
||||||
|
import org.hibernate.query.sqm.tree.expression.SqmXmlTableFunction;
|
||||||
|
import org.hibernate.spi.NavigablePath;
|
||||||
|
import org.hibernate.sql.Template;
|
||||||
|
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
|
||||||
|
import org.hibernate.sql.ast.SqlAstTranslator;
|
||||||
|
import org.hibernate.sql.ast.internal.ColumnQualifierCollectorSqlAstWalker;
|
||||||
|
import org.hibernate.sql.ast.spi.FromClauseAccess;
|
||||||
|
import org.hibernate.sql.ast.spi.SqlAppender;
|
||||||
|
import org.hibernate.sql.ast.tree.SqlAstNode;
|
||||||
|
import org.hibernate.sql.ast.tree.cte.CteColumn;
|
||||||
|
import org.hibernate.sql.ast.tree.cte.CteStatement;
|
||||||
|
import org.hibernate.sql.ast.tree.cte.CteTable;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.CastTarget;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.ColumnReference;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.Expression;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.Literal;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.SqlTuple;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableQueryColumnDefinition;
|
||||||
|
import org.hibernate.sql.ast.tree.from.FunctionTableGroup;
|
||||||
|
import org.hibernate.sql.ast.tree.from.StandardTableGroup;
|
||||||
|
import org.hibernate.sql.ast.tree.from.TableGroup;
|
||||||
|
import org.hibernate.sql.ast.tree.from.TableGroupJoin;
|
||||||
|
import org.hibernate.sql.ast.tree.from.TableGroupProducer;
|
||||||
|
import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate;
|
||||||
|
import org.hibernate.sql.ast.tree.predicate.NullnessPredicate;
|
||||||
|
import org.hibernate.sql.ast.tree.select.QuerySpec;
|
||||||
|
import org.hibernate.sql.ast.tree.select.SelectStatement;
|
||||||
|
import org.hibernate.sql.results.internal.SqlSelectionImpl;
|
||||||
|
import org.hibernate.type.Type;
|
||||||
|
import org.hibernate.type.descriptor.WrapperOptions;
|
||||||
|
import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter;
|
||||||
|
import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry;
|
||||||
|
import org.hibernate.type.spi.TypeConfiguration;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.hibernate.sql.ast.spi.AbstractSqlAstTranslator.isParameter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HANA xmltable function.
|
||||||
|
*/
|
||||||
|
public class HANAXmlTableFunction extends XmlTableFunction {
|
||||||
|
|
||||||
|
public HANAXmlTableFunction(TypeConfiguration typeConfiguration) {
|
||||||
|
super( false, new DB2XmlTableSetReturningFunctionTypeResolver(), typeConfiguration );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected <T> SelfRenderingSqmSetReturningFunction<T> generateSqmSetReturningFunctionExpression(
|
||||||
|
List<? extends SqmTypedNode<?>> arguments,
|
||||||
|
QueryEngine queryEngine) {
|
||||||
|
//noinspection unchecked
|
||||||
|
return new SqmXmlTableFunction<>(
|
||||||
|
this,
|
||||||
|
this,
|
||||||
|
getArgumentsValidator(),
|
||||||
|
getSetReturningTypeResolver(),
|
||||||
|
queryEngine.getCriteriaBuilder(),
|
||||||
|
(SqmExpression<String>) arguments.get( 0 ),
|
||||||
|
(SqmExpression<?>) arguments.get( 1 )
|
||||||
|
) {
|
||||||
|
@Override
|
||||||
|
public TableGroup convertToSqlAst(
|
||||||
|
NavigablePath navigablePath,
|
||||||
|
String identifierVariable,
|
||||||
|
boolean lateral,
|
||||||
|
boolean canUseInnerJoins,
|
||||||
|
boolean withOrdinality,
|
||||||
|
SqmToSqlAstConverter walker) {
|
||||||
|
// SAP HANA only supports table column references i.e. `TABLE_NAME.COLUMN_NAME`
|
||||||
|
// or constants as arguments to xmltable, so it's impossible to do lateral joins.
|
||||||
|
// There is a nice trick we can apply to make this work though, which is to figure out
|
||||||
|
// the table group an expression belongs to and render a special CTE returning xml/json that can be joined.
|
||||||
|
// The xml of that CTE needs to be extended by table group primary key data,
|
||||||
|
// so we can join it later.
|
||||||
|
final FunctionTableGroup functionTableGroup = (FunctionTableGroup) super.convertToSqlAst(
|
||||||
|
navigablePath,
|
||||||
|
identifierVariable,
|
||||||
|
lateral,
|
||||||
|
canUseInnerJoins,
|
||||||
|
withOrdinality,
|
||||||
|
walker
|
||||||
|
);
|
||||||
|
//noinspection unchecked
|
||||||
|
final List<SqlAstNode> sqlArguments = (List<SqlAstNode>) functionTableGroup.getPrimaryTableReference()
|
||||||
|
.getFunctionExpression()
|
||||||
|
.getArguments();
|
||||||
|
final Expression document = (Expression) sqlArguments.get( 1 );
|
||||||
|
final Set<String> qualifiers = ColumnQualifierCollectorSqlAstWalker.determineColumnQualifiers( document );
|
||||||
|
// Can only do this transformation if the argument contains a single column reference qualifier
|
||||||
|
if ( qualifiers.size() == 1 ) {
|
||||||
|
final String tableQualifier = qualifiers.iterator().next();
|
||||||
|
// Find the table group which the unnest argument refers to
|
||||||
|
final FromClauseAccess fromClauseAccess = walker.getFromClauseAccess();
|
||||||
|
final TableGroup sourceTableGroup =
|
||||||
|
fromClauseAccess.findTableGroupByIdentificationVariable( tableQualifier );
|
||||||
|
if ( sourceTableGroup != null ) {
|
||||||
|
final List<ColumnInfo> idColumns = new ArrayList<>();
|
||||||
|
addIdColumns( sourceTableGroup.getModelPart(), idColumns );
|
||||||
|
|
||||||
|
// Register a query transformer to register the CTE and rewrite the array argument
|
||||||
|
walker.registerQueryTransformer( (cteContainer, querySpec, converter) -> {
|
||||||
|
// Determine a CTE name that is available
|
||||||
|
final String baseName = "_data";
|
||||||
|
String cteName;
|
||||||
|
int index = 0;
|
||||||
|
do {
|
||||||
|
cteName = baseName + ( index++ );
|
||||||
|
} while ( cteContainer.getCteStatement( cteName ) != null );
|
||||||
|
|
||||||
|
final TableGroup parentTableGroup = querySpec.getFromClause().queryTableGroups(
|
||||||
|
tg -> tg.findTableGroupJoin( functionTableGroup ) == null ? null : tg
|
||||||
|
);
|
||||||
|
final TableGroupJoin join = parentTableGroup.findTableGroupJoin( functionTableGroup );
|
||||||
|
final Expression lhs = createExpression( tableQualifier, idColumns );
|
||||||
|
final Expression rhs = createExpression(
|
||||||
|
functionTableGroup.getPrimaryTableReference().getIdentificationVariable(),
|
||||||
|
idColumns
|
||||||
|
);
|
||||||
|
join.applyPredicate( new ComparisonPredicate( lhs, ComparisonOperator.EQUAL, rhs ) );
|
||||||
|
|
||||||
|
final String tableName = cteName;
|
||||||
|
final List<CteColumn> cteColumns = List.of(
|
||||||
|
new CteColumn( "v", document.getExpressionType().getSingleJdbcMapping() )
|
||||||
|
);
|
||||||
|
final QuerySpec cteQuery = new QuerySpec( false );
|
||||||
|
cteQuery.getFromClause().addRoot(
|
||||||
|
new StandardTableGroup(
|
||||||
|
true,
|
||||||
|
sourceTableGroup.getNavigablePath(),
|
||||||
|
(TableGroupProducer) sourceTableGroup.getModelPart(),
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
sourceTableGroup.findTableReference( tableQualifier ),
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
joinTableName -> false,
|
||||||
|
(joinTableName, tg) -> null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
final Expression wrapperExpression = new XmlWrapperExpression( idColumns, tableQualifier, document );
|
||||||
|
// xmltable is allergic to null values and produces no result if one occurs,
|
||||||
|
// so we must filter them out
|
||||||
|
cteQuery.applyPredicate( new NullnessPredicate( document, true ) );
|
||||||
|
cteQuery.getSelectClause().addSqlSelection( new SqlSelectionImpl( wrapperExpression ) );
|
||||||
|
cteContainer.addCteStatement( new CteStatement(
|
||||||
|
new CteTable( tableName, cteColumns ),
|
||||||
|
new SelectStatement( cteQuery )
|
||||||
|
) );
|
||||||
|
sqlArguments.set( 1, new TableColumnReferenceExpression( document, tableName, idColumns ) );
|
||||||
|
return querySpec;
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return functionTableGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Expression createExpression(String qualifier, List<ColumnInfo> idColumns) {
|
||||||
|
if ( idColumns.size() == 1 ) {
|
||||||
|
final ColumnInfo columnInfo = idColumns.get( 0 );
|
||||||
|
return new ColumnReference( qualifier, columnInfo.name(), false, null, columnInfo.jdbcMapping() );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
final ArrayList<Expression> expressions = new ArrayList<>( idColumns.size() );
|
||||||
|
for ( ColumnInfo columnInfo : idColumns ) {
|
||||||
|
expressions.add(
|
||||||
|
new ColumnReference(
|
||||||
|
qualifier,
|
||||||
|
columnInfo.name(),
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
columnInfo.jdbcMapping()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new SqlTuple( expressions, null );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addIdColumns(ModelPartContainer modelPartContainer, List<ColumnInfo> idColumns) {
|
||||||
|
if ( modelPartContainer instanceof EntityValuedModelPart entityValuedModelPart ) {
|
||||||
|
addIdColumns( entityValuedModelPart.getEntityMappingType(), idColumns );
|
||||||
|
}
|
||||||
|
else if ( modelPartContainer instanceof PluralAttributeMapping pluralAttributeMapping ) {
|
||||||
|
addIdColumns( pluralAttributeMapping, idColumns );
|
||||||
|
}
|
||||||
|
else if ( modelPartContainer instanceof EmbeddableValuedModelPart embeddableModelPart ) {
|
||||||
|
addIdColumns( embeddableModelPart, idColumns );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new QueryException( "Unsupported model part container: " + modelPartContainer );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addIdColumns(EmbeddableValuedModelPart embeddableModelPart, List<ColumnInfo> idColumns) {
|
||||||
|
if ( embeddableModelPart instanceof EmbeddedCollectionPart collectionPart ) {
|
||||||
|
addIdColumns( collectionPart.getCollectionAttribute(), idColumns );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
addIdColumns( embeddableModelPart.asAttributeMapping().getDeclaringType(), idColumns );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addIdColumns(PluralAttributeMapping pluralAttributeMapping, List<ColumnInfo> idColumns) {
|
||||||
|
final DdlTypeRegistry ddlTypeRegistry = pluralAttributeMapping.getCollectionDescriptor()
|
||||||
|
.getFactory()
|
||||||
|
.getTypeConfiguration()
|
||||||
|
.getDdlTypeRegistry();
|
||||||
|
addIdColumns( pluralAttributeMapping.getKeyDescriptor().getKeyPart(), ddlTypeRegistry, idColumns );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addIdColumns(EntityMappingType entityMappingType, List<ColumnInfo> idColumns) {
|
||||||
|
final DdlTypeRegistry ddlTypeRegistry = entityMappingType.getEntityPersister()
|
||||||
|
.getFactory()
|
||||||
|
.getTypeConfiguration()
|
||||||
|
.getDdlTypeRegistry();
|
||||||
|
addIdColumns( entityMappingType.getIdentifierMapping(), ddlTypeRegistry, idColumns );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addIdColumns(
|
||||||
|
ValuedModelPart modelPart,
|
||||||
|
DdlTypeRegistry ddlTypeRegistry,
|
||||||
|
List<ColumnInfo> idColumns) {
|
||||||
|
modelPart.forEachSelectable( (selectionIndex, selectableMapping) -> {
|
||||||
|
final JdbcMapping jdbcMapping = selectableMapping.getJdbcMapping().getSingleJdbcMapping();
|
||||||
|
idColumns.add( new ColumnInfo(
|
||||||
|
selectableMapping.getSelectionExpression(),
|
||||||
|
jdbcMapping,
|
||||||
|
ddlTypeRegistry.getTypeName(
|
||||||
|
jdbcMapping.getJdbcType().getDefaultSqlTypeCode(),
|
||||||
|
selectableMapping.toSize(),
|
||||||
|
(Type) jdbcMapping
|
||||||
|
)
|
||||||
|
) );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
record ColumnInfo(String name, JdbcMapping jdbcMapping, String ddlType) {}
|
||||||
|
|
||||||
|
static class TableColumnReferenceExpression implements SelfRenderingExpression {
|
||||||
|
|
||||||
|
private final Expression argument;
|
||||||
|
private final String tableName;
|
||||||
|
private final List<ColumnInfo> idColumns;
|
||||||
|
|
||||||
|
public TableColumnReferenceExpression(Expression argument, String tableName, List<ColumnInfo> idColumns) {
|
||||||
|
this.argument = argument;
|
||||||
|
this.tableName = tableName;
|
||||||
|
this.idColumns = idColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void renderToSql(
|
||||||
|
SqlAppender sqlAppender,
|
||||||
|
SqlAstTranslator<?> walker,
|
||||||
|
SessionFactoryImplementor sessionFactory) {
|
||||||
|
sqlAppender.appendSql( tableName );
|
||||||
|
sqlAppender.appendSql( ".v" );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JdbcMappingContainer getExpressionType() {
|
||||||
|
return argument.getExpressionType();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ColumnInfo> getIdColumns() {
|
||||||
|
return idColumns;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class XmlWrapperExpression implements SelfRenderingExpression {
|
||||||
|
private final List<ColumnInfo> idColumns;
|
||||||
|
private final String tableQualifier;
|
||||||
|
private final Expression argument;
|
||||||
|
|
||||||
|
public XmlWrapperExpression(List<ColumnInfo> idColumns, String tableQualifier, Expression argument) {
|
||||||
|
this.idColumns = idColumns;
|
||||||
|
this.tableQualifier = tableQualifier;
|
||||||
|
this.argument = argument;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void renderToSql(
|
||||||
|
SqlAppender sqlAppender,
|
||||||
|
SqlAstTranslator<?> walker,
|
||||||
|
SessionFactoryImplementor sessionFactory) {
|
||||||
|
// Produce an XML string e.g. <root id="1">...</root>
|
||||||
|
// which will contain the original XML as well as id column information for correlation
|
||||||
|
sqlAppender.appendSql( "'<root'" );
|
||||||
|
for ( ColumnInfo columnInfo : idColumns ) {
|
||||||
|
sqlAppender.appendSql( "||' " );
|
||||||
|
sqlAppender.appendSql( columnInfo.name() );
|
||||||
|
sqlAppender.appendSql( "=\"'||" );
|
||||||
|
sqlAppender.appendSql( tableQualifier );
|
||||||
|
sqlAppender.appendSql( '.' );
|
||||||
|
sqlAppender.appendSql( columnInfo.name() );
|
||||||
|
sqlAppender.appendSql( "||'\"'" );
|
||||||
|
}
|
||||||
|
sqlAppender.appendSql( "||'>'||" );
|
||||||
|
argument.accept( walker );
|
||||||
|
sqlAppender.appendSql( "||'</root>'" );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JdbcMappingContainer getExpressionType() {
|
||||||
|
return argument.getExpressionType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void renderXmlTable(SqlAppender sqlAppender, XmlTableArguments arguments, AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstTranslator<?> walker) {
|
||||||
|
sqlAppender.appendSql( "xmltable(" );
|
||||||
|
final Expression documentExpression = arguments.xmlDocument();
|
||||||
|
final String xpath = walker.getLiteralValue( arguments.xpath() );
|
||||||
|
if ( documentExpression instanceof TableColumnReferenceExpression ) {
|
||||||
|
sqlAppender.appendSingleQuoteEscapedString( "/root" + xpath );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sqlAppender.appendSingleQuoteEscapedString( xpath );
|
||||||
|
}
|
||||||
|
sqlAppender.appendSql( " passing " );
|
||||||
|
// We have to handle the rendering of strings/literals manually here to avoid using nationalized literals,
|
||||||
|
// because HANA doesn't support that syntax in xmltable()
|
||||||
|
if ( documentExpression instanceof Literal literal ) {
|
||||||
|
sqlAppender.appendSingleQuoteEscapedString( (String) literal.getLiteralValue() );
|
||||||
|
}
|
||||||
|
else if ( isParameter( documentExpression ) ) {
|
||||||
|
sqlAppender.appendSingleQuoteEscapedString( walker.getLiteralValue( documentExpression ) );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
documentExpression.accept( walker );
|
||||||
|
}
|
||||||
|
renderColumns( sqlAppender, arguments.columnsClause(), walker );
|
||||||
|
if ( documentExpression instanceof TableColumnReferenceExpression expression ) {
|
||||||
|
for ( ColumnInfo columnInfo : expression.getIdColumns() ) {
|
||||||
|
sqlAppender.appendSql( ',' );
|
||||||
|
sqlAppender.appendSql( columnInfo.name() );
|
||||||
|
sqlAppender.appendSql( ' ' );
|
||||||
|
sqlAppender.appendSql( columnInfo.ddlType() );
|
||||||
|
sqlAppender.appendSql( " path '/root/@" );
|
||||||
|
sqlAppender.appendSql( columnInfo.name() );
|
||||||
|
sqlAppender.appendSql( '\'' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sqlAppender.appendSql( ')' );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String determineColumnType(CastTarget castTarget, SqlAstTranslator<?> walker) {
|
||||||
|
final String typeName = super.determineColumnType( castTarget, walker );
|
||||||
|
return switch ( typeName ) {
|
||||||
|
// xmltable doesn't support tinyint. Usually it is a boolean, but if not, use "integer"
|
||||||
|
case "tinyint" -> isBoolean( castTarget.getJdbcMapping() ) ? "varchar(5)" : "integer";
|
||||||
|
// Also, smallint isn't supported
|
||||||
|
case "smallint" -> "integer";
|
||||||
|
// For boolean, use varchar since that decoding is done through a read expression
|
||||||
|
case "boolean" -> "varchar(5)";
|
||||||
|
// Float is also not supported, but double is
|
||||||
|
case "float" -> "double";
|
||||||
|
// Clobs are also not supported, so use the biggest nvarchar possible
|
||||||
|
case "clob", "nclob" -> "nvarchar(5000)";
|
||||||
|
default -> typeName;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void renderXmlQueryColumnDefinition(SqlAppender sqlAppender, XmlTableQueryColumnDefinition definition, SqlAstTranslator<?> walker) {
|
||||||
|
sqlAppender.appendSql( definition.name() );
|
||||||
|
sqlAppender.appendSql( ' ' );
|
||||||
|
sqlAppender.appendSql( determineColumnType( new CastTarget( definition.type() ), walker ) );
|
||||||
|
sqlAppender.appendSql( " format xml" );
|
||||||
|
|
||||||
|
renderColumnPath( definition.name(), definition.xpath(), sqlAppender, walker );
|
||||||
|
renderDefaultExpression( definition.defaultExpression(), sqlAppender, walker );
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isBoolean(JdbcMapping type) {
|
||||||
|
return type.getJdbcType().isBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DB2XmlTableSetReturningFunctionTypeResolver extends XmlTableSetReturningFunctionTypeResolver {
|
||||||
|
@Override
|
||||||
|
protected void addSelectableMapping(List<SelectableMapping> selectableMappings, String name, JdbcMapping type, SqmToSqlAstConverter converter) {
|
||||||
|
if ( isBoolean( type ) ) {
|
||||||
|
//noinspection unchecked
|
||||||
|
final JdbcLiteralFormatter<Object> jdbcLiteralFormatter = type.getJdbcLiteralFormatter();
|
||||||
|
final SessionFactoryImplementor sessionFactory = converter.getCreationContext().getSessionFactory();
|
||||||
|
final Dialect dialect = sessionFactory.getJdbcServices().getDialect();
|
||||||
|
final WrapperOptions wrapperOptions = sessionFactory.getWrapperOptions();
|
||||||
|
final Object trueValue = type.convertToRelationalValue( true );
|
||||||
|
final Object falseValue = type.convertToRelationalValue( false );
|
||||||
|
final String trueFragment = jdbcLiteralFormatter.toJdbcLiteral( trueValue, dialect, wrapperOptions );
|
||||||
|
final String falseFragment = jdbcLiteralFormatter.toJdbcLiteral( falseValue, dialect, wrapperOptions );
|
||||||
|
selectableMappings.add( new SelectableMappingImpl(
|
||||||
|
"",
|
||||||
|
name,
|
||||||
|
new SelectablePath( name ),
|
||||||
|
"case " + Template.TEMPLATE + "." + name + " when 'true' then " + trueFragment + " when 'false' then " + falseFragment + " end",
|
||||||
|
null,
|
||||||
|
"varchar(5)",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
type
|
||||||
|
));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
super.addSelectableMapping( selectableMappings, name, type, converter );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
* Copyright Red Hat Inc. and Hibernate Authors
|
||||||
|
*/
|
||||||
|
package org.hibernate.dialect.function.xml;
|
||||||
|
|
||||||
|
import org.hibernate.dialect.Dialect;
|
||||||
|
import org.hibernate.engine.spi.SessionFactoryImplementor;
|
||||||
|
import org.hibernate.metamodel.mapping.JdbcMapping;
|
||||||
|
import org.hibernate.metamodel.mapping.SelectableMapping;
|
||||||
|
import org.hibernate.metamodel.mapping.SelectablePath;
|
||||||
|
import org.hibernate.metamodel.mapping.internal.SelectableMappingImpl;
|
||||||
|
import org.hibernate.query.sqm.sql.SqmToSqlAstConverter;
|
||||||
|
import org.hibernate.sql.Template;
|
||||||
|
import org.hibernate.sql.ast.SqlAstTranslator;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.CastTarget;
|
||||||
|
import org.hibernate.type.descriptor.WrapperOptions;
|
||||||
|
import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter;
|
||||||
|
import org.hibernate.type.spi.TypeConfiguration;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.hibernate.dialect.function.json.OracleJsonValueFunction.isEncodedBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Oracle xmltable function.
|
||||||
|
*/
|
||||||
|
public class OracleXmlTableFunction extends XmlTableFunction {
|
||||||
|
|
||||||
|
public OracleXmlTableFunction(TypeConfiguration typeConfiguration) {
|
||||||
|
super( false, new OracleXmlTableSetReturningFunctionTypeResolver(), typeConfiguration );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String determineColumnType(CastTarget castTarget, SqlAstTranslator<?> walker) {
|
||||||
|
final String typeName = super.determineColumnType( castTarget, walker );
|
||||||
|
return switch ( typeName ) {
|
||||||
|
// clob is not supported as column type for xmltable
|
||||||
|
case "clob" -> "varchar2(" + walker.getSessionFactory().getJdbcServices().getDialect().getMaxVarcharLength() + ")";
|
||||||
|
case "number(1,0)" -> isEncodedBoolean( castTarget.getJdbcMapping() ) ? "varchar2(5)" : typeName;
|
||||||
|
default -> typeName;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class OracleXmlTableSetReturningFunctionTypeResolver extends XmlTableSetReturningFunctionTypeResolver {
|
||||||
|
@Override
|
||||||
|
protected void addSelectableMapping(List<SelectableMapping> selectableMappings, String name, JdbcMapping type, SqmToSqlAstConverter converter) {
|
||||||
|
if ( isEncodedBoolean( type ) ) {
|
||||||
|
//noinspection unchecked
|
||||||
|
final JdbcLiteralFormatter<Object> jdbcLiteralFormatter = type.getJdbcLiteralFormatter();
|
||||||
|
final SessionFactoryImplementor sessionFactory = converter.getCreationContext().getSessionFactory();
|
||||||
|
final Dialect dialect = sessionFactory.getJdbcServices().getDialect();
|
||||||
|
final WrapperOptions wrapperOptions = sessionFactory.getWrapperOptions();
|
||||||
|
final Object trueValue = type.convertToRelationalValue( true );
|
||||||
|
final Object falseValue = type.convertToRelationalValue( false );
|
||||||
|
final String trueFragment = jdbcLiteralFormatter.toJdbcLiteral( trueValue, dialect, wrapperOptions );
|
||||||
|
final String falseFragment = jdbcLiteralFormatter.toJdbcLiteral( falseValue, dialect, wrapperOptions );
|
||||||
|
selectableMappings.add( new SelectableMappingImpl(
|
||||||
|
"",
|
||||||
|
name,
|
||||||
|
new SelectablePath( name ),
|
||||||
|
"decode(" + Template.TEMPLATE + "." + name + ",'true'," + trueFragment + ",'false'," + falseFragment + ")",
|
||||||
|
null,
|
||||||
|
"varchar2(5)",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
type
|
||||||
|
));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
super.addSelectableMapping( selectableMappings, name, type, converter );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
* Copyright Red Hat Inc. and Hibernate Authors
|
||||||
|
*/
|
||||||
|
package org.hibernate.dialect.function.xml;
|
||||||
|
|
||||||
|
import org.hibernate.query.derived.AnonymousTupleTableGroupProducer;
|
||||||
|
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
|
||||||
|
import org.hibernate.sql.ast.SqlAstTranslator;
|
||||||
|
import org.hibernate.sql.ast.spi.SqlAppender;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableColumnDefinition;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableColumnsClause;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableOrdinalityColumnDefinition;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableQueryColumnDefinition;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableValueColumnDefinition;
|
||||||
|
import org.hibernate.type.spi.TypeConfiguration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL Server xmltable function.
|
||||||
|
*/
|
||||||
|
public class SQLServerXmlTableFunction extends XmlTableFunction {
|
||||||
|
|
||||||
|
public SQLServerXmlTableFunction(TypeConfiguration typeConfiguration) {
|
||||||
|
super( false, typeConfiguration );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void renderXmlTable(SqlAppender sqlAppender, XmlTableArguments arguments, AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstTranslator<?> walker) {
|
||||||
|
sqlAppender.appendSql( "(select" );
|
||||||
|
renderColumns( sqlAppender, arguments.columnsClause(), walker );
|
||||||
|
sqlAppender.appendSql( " from (select " );
|
||||||
|
if ( !arguments.isXmlType() ) {
|
||||||
|
sqlAppender.appendSql( "cast(" );
|
||||||
|
}
|
||||||
|
arguments.xmlDocument().accept( walker );
|
||||||
|
if ( !arguments.isXmlType() ) {
|
||||||
|
sqlAppender.appendSql( " as xml)" );
|
||||||
|
}
|
||||||
|
sqlAppender.appendSql( ") t0_(d) cross apply t0_.d.nodes(" );
|
||||||
|
walker.render( arguments.xpath(), SqlAstNodeRenderingMode.INLINE_PARAMETERS );
|
||||||
|
sqlAppender.appendSql( ") t1_(d))" );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void renderColumns(SqlAppender sqlAppender, XmlTableColumnsClause xmlTableColumnsClause, SqlAstTranslator<?> walker) {
|
||||||
|
char separator = ' ';
|
||||||
|
for ( XmlTableColumnDefinition columnDefinition : xmlTableColumnsClause.getColumnDefinitions() ) {
|
||||||
|
sqlAppender.appendSql( separator );
|
||||||
|
if ( columnDefinition instanceof XmlTableQueryColumnDefinition definition ) {
|
||||||
|
renderXmlQueryColumnDefinition( sqlAppender, definition, walker );
|
||||||
|
}
|
||||||
|
else if ( columnDefinition instanceof XmlTableValueColumnDefinition definition ) {
|
||||||
|
renderXmlValueColumnDefinition( sqlAppender, definition, walker );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
renderXmlOrdinalityColumnDefinition(
|
||||||
|
sqlAppender,
|
||||||
|
(XmlTableOrdinalityColumnDefinition) columnDefinition,
|
||||||
|
walker
|
||||||
|
);
|
||||||
|
}
|
||||||
|
separator = ',';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void renderXmlOrdinalityColumnDefinition(SqlAppender sqlAppender, XmlTableOrdinalityColumnDefinition definition, SqlAstTranslator<?> walker) {
|
||||||
|
sqlAppender.appendSql( "row_number() over (order by (select 1)) " );
|
||||||
|
sqlAppender.appendSql( definition.name() );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void renderXmlValueColumnDefinition(SqlAppender sqlAppender, XmlTableValueColumnDefinition definition, SqlAstTranslator<?> walker) {
|
||||||
|
if ( definition.defaultExpression() != null ) {
|
||||||
|
sqlAppender.appendSql( "coalesce(" );
|
||||||
|
}
|
||||||
|
sqlAppender.appendSql( "t1_.d.value('(" );
|
||||||
|
sqlAppender.appendSql( definition.xpath() == null ? definition.name() : definition.xpath() );
|
||||||
|
sqlAppender.appendSql( ")[1]'," );
|
||||||
|
sqlAppender.appendSingleQuoteEscapedString( determineColumnType( definition.type(), walker ) );
|
||||||
|
sqlAppender.appendSql( ')' );
|
||||||
|
|
||||||
|
if ( definition.defaultExpression() != null ) {
|
||||||
|
sqlAppender.appendSql( ',' );
|
||||||
|
definition.defaultExpression().accept( walker );
|
||||||
|
sqlAppender.appendSql( ')' );
|
||||||
|
}
|
||||||
|
sqlAppender.appendSql( ' ' );
|
||||||
|
sqlAppender.appendSql( definition.name() );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void renderXmlQueryColumnDefinition(SqlAppender sqlAppender, XmlTableQueryColumnDefinition definition, SqlAstTranslator<?> walker) {
|
||||||
|
if ( definition.defaultExpression() != null ) {
|
||||||
|
sqlAppender.appendSql( "coalesce(" );
|
||||||
|
}
|
||||||
|
sqlAppender.appendSql( "t1_.d.query('(" );
|
||||||
|
sqlAppender.appendSql( definition.xpath() == null ? definition.name() : definition.xpath() );
|
||||||
|
sqlAppender.appendSql( ")[1]')" );
|
||||||
|
|
||||||
|
if ( definition.defaultExpression() != null ) {
|
||||||
|
sqlAppender.appendSql( ',' );
|
||||||
|
definition.defaultExpression().accept( walker );
|
||||||
|
sqlAppender.appendSql( ')' );
|
||||||
|
}
|
||||||
|
sqlAppender.appendSql( ' ' );
|
||||||
|
sqlAppender.appendSql( definition.name() );
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,208 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
* Copyright Red Hat Inc. and Hibernate Authors
|
||||||
|
*/
|
||||||
|
package org.hibernate.dialect.function.xml;
|
||||||
|
|
||||||
|
import org.hibernate.QueryException;
|
||||||
|
import org.hibernate.dialect.Dialect;
|
||||||
|
import org.hibernate.engine.spi.SessionFactoryImplementor;
|
||||||
|
import org.hibernate.metamodel.mapping.JdbcMapping;
|
||||||
|
import org.hibernate.metamodel.mapping.SelectableMapping;
|
||||||
|
import org.hibernate.metamodel.mapping.SelectablePath;
|
||||||
|
import org.hibernate.metamodel.mapping.internal.SelectableMappingImpl;
|
||||||
|
import org.hibernate.query.derived.AnonymousTupleTableGroupProducer;
|
||||||
|
import org.hibernate.query.sqm.sql.SqmToSqlAstConverter;
|
||||||
|
import org.hibernate.sql.Template;
|
||||||
|
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
|
||||||
|
import org.hibernate.sql.ast.SqlAstTranslator;
|
||||||
|
import org.hibernate.sql.ast.spi.SqlAppender;
|
||||||
|
import org.hibernate.sql.ast.tree.SqlAstNode;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.CastTarget;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.ColumnReference;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.Literal;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableColumnDefinition;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableOrdinalityColumnDefinition;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableQueryColumnDefinition;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableValueColumnDefinition;
|
||||||
|
import org.hibernate.type.SqlTypes;
|
||||||
|
import org.hibernate.type.descriptor.WrapperOptions;
|
||||||
|
import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter;
|
||||||
|
import org.hibernate.type.spi.TypeConfiguration;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sybase ASE xmltable function.
|
||||||
|
*/
|
||||||
|
public class SybaseASEXmlTableFunction extends XmlTableFunction {
|
||||||
|
|
||||||
|
public SybaseASEXmlTableFunction(TypeConfiguration typeConfiguration) {
|
||||||
|
super( false, new SybaseASEXmlTableSetReturningFunctionTypeResolver(), typeConfiguration );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void renderXmlTable(SqlAppender sqlAppender, XmlTableArguments arguments, AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstTranslator<?> walker) {
|
||||||
|
sqlAppender.appendSql( "xmltable(" );
|
||||||
|
walker.render( arguments.xpath(), SqlAstNodeRenderingMode.INLINE_PARAMETERS );
|
||||||
|
sqlAppender.appendSql( " passing " );
|
||||||
|
walker.render( arguments.xmlDocument(), SqlAstNodeRenderingMode.INLINE_PARAMETERS );
|
||||||
|
renderColumns( sqlAppender, arguments.columnsClause(), walker );
|
||||||
|
sqlAppender.appendSql( ')' );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String determineColumnType(CastTarget castTarget, SqlAstTranslator<?> walker) {
|
||||||
|
if ( isBoolean( castTarget.getJdbcMapping() ) ) {
|
||||||
|
return "varchar(5)";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return super.determineColumnType( castTarget, walker );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void renderXmlQueryColumnDefinition(SqlAppender sqlAppender, XmlTableQueryColumnDefinition definition, SqlAstTranslator<?> walker) {
|
||||||
|
// Queries don't really work, so we have to extract the ordinality instead and extract the value through a read expression
|
||||||
|
sqlAppender.appendSql( definition.name() );
|
||||||
|
sqlAppender.appendSql( " int for ordinality" );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void renderXmlValueColumnDefinition(SqlAppender sqlAppender, XmlTableValueColumnDefinition definition, SqlAstTranslator<?> walker) {
|
||||||
|
sqlAppender.appendSql( definition.name() );
|
||||||
|
sqlAppender.appendSql( ' ' );
|
||||||
|
sqlAppender.appendSql( determineColumnType( definition.type(), walker ) );
|
||||||
|
|
||||||
|
// Sybase ASE wants the default before the path
|
||||||
|
renderDefaultExpression( definition.defaultExpression(), sqlAppender, walker );
|
||||||
|
renderColumnPath( definition.name(), definition.xpath(), sqlAppender, walker );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void renderXmlOrdinalityColumnDefinition(SqlAppender sqlAppender, XmlTableOrdinalityColumnDefinition definition, SqlAstTranslator<?> walker) {
|
||||||
|
sqlAppender.appendSql( definition.name() );
|
||||||
|
sqlAppender.appendSql( " bigint for ordinality" );
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SybaseASEXmlTableSetReturningFunctionTypeResolver extends XmlTableSetReturningFunctionTypeResolver {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SelectableMapping[] resolveFunctionReturnType(
|
||||||
|
List<? extends SqlAstNode> sqlAstNodes,
|
||||||
|
String tableIdentifierVariable,
|
||||||
|
boolean lateral,
|
||||||
|
boolean withOrdinality,
|
||||||
|
SqmToSqlAstConverter converter) {
|
||||||
|
final XmlTableArguments arguments = XmlTableArguments.extract( sqlAstNodes );
|
||||||
|
final List<SelectableMapping> selectableMappings = new ArrayList<>( arguments.columnsClause().getColumnDefinitions().size() );
|
||||||
|
addSelectableMappings( selectableMappings, arguments, converter );
|
||||||
|
return selectableMappings.toArray( new SelectableMapping[0] );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addSelectableMappings(List<SelectableMapping> selectableMappings, XmlTableArguments arguments, SqmToSqlAstConverter converter) {
|
||||||
|
for ( XmlTableColumnDefinition columnDefinition : arguments.columnsClause().getColumnDefinitions() ) {
|
||||||
|
if ( columnDefinition instanceof XmlTableQueryColumnDefinition definition ) {
|
||||||
|
addSelectableMappings( selectableMappings, definition, arguments, converter );
|
||||||
|
}
|
||||||
|
else if ( columnDefinition instanceof XmlTableValueColumnDefinition definition ) {
|
||||||
|
addSelectableMappings( selectableMappings, definition, converter );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
final XmlTableOrdinalityColumnDefinition definition
|
||||||
|
= (XmlTableOrdinalityColumnDefinition) columnDefinition;
|
||||||
|
addSelectableMappings( selectableMappings, definition, converter );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addSelectableMappings(List<SelectableMapping> selectableMappings, XmlTableQueryColumnDefinition definition, XmlTableArguments arguments, SqmToSqlAstConverter converter) {
|
||||||
|
// Sybase ASE can't extract XML nodes via xmltable, so we select the ordinality instead and extract
|
||||||
|
// the XML nodes via xmlextract in select item. Unfortunately, this limits XPaths to literals
|
||||||
|
// and documents to columns or literals, since that is the only form that can be encoded in read expressions
|
||||||
|
final String documentFragment;
|
||||||
|
if ( arguments.xmlDocument() instanceof Literal documentLiteral ) {
|
||||||
|
documentFragment = documentLiteral.getJdbcMapping().getJdbcLiteralFormatter().toJdbcLiteral(
|
||||||
|
documentLiteral.getLiteralValue(),
|
||||||
|
converter.getCreationContext().getSessionFactory().getJdbcServices().getDialect(),
|
||||||
|
converter.getCreationContext().getSessionFactory().getWrapperOptions()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if ( arguments.xmlDocument() instanceof ColumnReference columnReference ) {
|
||||||
|
documentFragment = columnReference.getExpressionText();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new QueryException( "Sybase ASE only supports passing a literal or column reference as XML document for xmltable() when using query columns, but got: " + arguments.xmlDocument() );
|
||||||
|
}
|
||||||
|
if ( !( arguments.xpath() instanceof Literal literal)) {
|
||||||
|
throw new QueryException( "Sybase ASE only supports passing an XPath literal to xmltable() when using query columns, but got: " + arguments.xpath() );
|
||||||
|
}
|
||||||
|
final String xpathString = (String) literal.getLiteralValue();
|
||||||
|
final String definitionPath = definition.xpath() == null ? definition.name() : definition.xpath();
|
||||||
|
|
||||||
|
selectableMappings.add( new SelectableMappingImpl(
|
||||||
|
"",
|
||||||
|
definition.name(),
|
||||||
|
new SelectablePath( definition.name() ),
|
||||||
|
"xmlextract('" + xpathString + "['||cast(" + Template.TEMPLATE + "." + definition.name() + " as varchar)||']/" + definitionPath + "'," + documentFragment + ")",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
converter.getCreationContext().getTypeConfiguration().getBasicTypeRegistry()
|
||||||
|
.resolve( String.class, SqlTypes.SQLXML )
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void addSelectableMapping(List<SelectableMapping> selectableMappings, String name, JdbcMapping type, SqmToSqlAstConverter converter) {
|
||||||
|
if ( isBoolean( type ) ) {
|
||||||
|
//noinspection unchecked
|
||||||
|
final JdbcLiteralFormatter<Object> jdbcLiteralFormatter = type.getJdbcLiteralFormatter();
|
||||||
|
final SessionFactoryImplementor sessionFactory = converter.getCreationContext().getSessionFactory();
|
||||||
|
final Dialect dialect = sessionFactory.getJdbcServices().getDialect();
|
||||||
|
final WrapperOptions wrapperOptions = sessionFactory.getWrapperOptions();
|
||||||
|
final Object trueValue = type.convertToRelationalValue( true );
|
||||||
|
final Object falseValue = type.convertToRelationalValue( false );
|
||||||
|
final String trueFragment = jdbcLiteralFormatter.toJdbcLiteral( trueValue, dialect, wrapperOptions );
|
||||||
|
final String falseFragment = jdbcLiteralFormatter.toJdbcLiteral( falseValue, dialect, wrapperOptions );
|
||||||
|
selectableMappings.add( new SelectableMappingImpl(
|
||||||
|
"",
|
||||||
|
name,
|
||||||
|
new SelectablePath( name ),
|
||||||
|
"case " + Template.TEMPLATE + "." + name + " when 'true' then " + trueFragment + " when 'false' then " + falseFragment + " end",
|
||||||
|
null,
|
||||||
|
"varchar(5)",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
type
|
||||||
|
));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
super.addSelectableMapping( selectableMappings, name, type, converter );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isBoolean(JdbcMapping type) {
|
||||||
|
return type.getJavaTypeDescriptor().getJavaTypeClass() == Boolean.class;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,233 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
* Copyright Red Hat Inc. and Hibernate Authors
|
||||||
|
*/
|
||||||
|
package org.hibernate.dialect.function.xml;
|
||||||
|
|
||||||
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
import org.hibernate.dialect.function.array.DdlTypeHelper;
|
||||||
|
import org.hibernate.query.derived.AnonymousTupleTableGroupProducer;
|
||||||
|
import org.hibernate.query.spi.QueryEngine;
|
||||||
|
import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingSetReturningFunctionDescriptor;
|
||||||
|
import org.hibernate.query.sqm.function.SelfRenderingSqmSetReturningFunction;
|
||||||
|
import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator;
|
||||||
|
import org.hibernate.query.sqm.produce.function.FunctionParameterType;
|
||||||
|
import org.hibernate.query.sqm.produce.function.SetReturningFunctionTypeResolver;
|
||||||
|
import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators;
|
||||||
|
import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers;
|
||||||
|
import org.hibernate.query.sqm.tree.SqmTypedNode;
|
||||||
|
import org.hibernate.query.sqm.tree.expression.SqmExpression;
|
||||||
|
import org.hibernate.query.sqm.tree.expression.SqmXmlTableFunction;
|
||||||
|
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
|
||||||
|
import org.hibernate.sql.ast.SqlAstTranslator;
|
||||||
|
import org.hibernate.sql.ast.spi.SqlAppender;
|
||||||
|
import org.hibernate.sql.ast.tree.SqlAstNode;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.CastTarget;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.Expression;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableColumnDefinition;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableColumnsClause;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableOrdinalityColumnDefinition;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableQueryColumnDefinition;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableValueColumnDefinition;
|
||||||
|
import org.hibernate.type.spi.TypeConfiguration;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING;
|
||||||
|
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.XML;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard xmltable function.
|
||||||
|
*/
|
||||||
|
public class XmlTableFunction extends AbstractSqmSelfRenderingSetReturningFunctionDescriptor {
|
||||||
|
|
||||||
|
protected final boolean supportsParametersInDefault;
|
||||||
|
|
||||||
|
public XmlTableFunction(boolean supportsParametersInDefault, TypeConfiguration typeConfiguration) {
|
||||||
|
this(
|
||||||
|
supportsParametersInDefault,
|
||||||
|
new XmlTableSetReturningFunctionTypeResolver(),
|
||||||
|
typeConfiguration
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected XmlTableFunction(boolean supportsParametersInDefault, SetReturningFunctionTypeResolver setReturningFunctionTypeResolver, TypeConfiguration typeConfiguration) {
|
||||||
|
super(
|
||||||
|
"xmltable",
|
||||||
|
new ArgumentTypesValidator(
|
||||||
|
StandardArgumentsValidators.exactly( 2 ),
|
||||||
|
FunctionParameterType.STRING,
|
||||||
|
FunctionParameterType.IMPLICIT_XML
|
||||||
|
),
|
||||||
|
setReturningFunctionTypeResolver,
|
||||||
|
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, STRING, XML )
|
||||||
|
);
|
||||||
|
this.supportsParametersInDefault = supportsParametersInDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected <T> SelfRenderingSqmSetReturningFunction<T> generateSqmSetReturningFunctionExpression(List<? extends SqmTypedNode<?>> arguments, QueryEngine queryEngine) {
|
||||||
|
//noinspection unchecked
|
||||||
|
return new SqmXmlTableFunction<>(
|
||||||
|
this,
|
||||||
|
this,
|
||||||
|
getArgumentsValidator(),
|
||||||
|
getSetReturningTypeResolver(),
|
||||||
|
queryEngine.getCriteriaBuilder(),
|
||||||
|
(SqmExpression<String>) arguments.get( 0 ),
|
||||||
|
(SqmExpression<?>) arguments.get( 1 )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(
|
||||||
|
SqlAppender sqlAppender,
|
||||||
|
List<? extends SqlAstNode> sqlAstArguments,
|
||||||
|
AnonymousTupleTableGroupProducer tupleType,
|
||||||
|
String tableIdentifierVariable,
|
||||||
|
SqlAstTranslator<?> walker) {
|
||||||
|
renderXmlTable( sqlAppender, XmlTableArguments.extract( sqlAstArguments ), tupleType, tableIdentifierVariable, walker );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void renderXmlTable(
|
||||||
|
SqlAppender sqlAppender,
|
||||||
|
XmlTableArguments arguments,
|
||||||
|
AnonymousTupleTableGroupProducer tupleType,
|
||||||
|
String tableIdentifierVariable,
|
||||||
|
SqlAstTranslator<?> walker) {
|
||||||
|
sqlAppender.appendSql( "xmltable(" );
|
||||||
|
arguments.xpath().accept( walker );
|
||||||
|
sqlAppender.appendSql( " passing " );
|
||||||
|
if ( !arguments.isXmlType() ) {
|
||||||
|
sqlAppender.appendSql( "xmlparse(document " );
|
||||||
|
}
|
||||||
|
arguments.xmlDocument().accept( walker );
|
||||||
|
if ( !arguments.isXmlType() ) {
|
||||||
|
sqlAppender.appendSql( ')' );
|
||||||
|
}
|
||||||
|
renderColumns( sqlAppender, arguments.columnsClause(), walker );
|
||||||
|
sqlAppender.appendSql( ')' );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String determineColumnType(CastTarget castTarget, SqlAstTranslator<?> walker) {
|
||||||
|
return determineColumnType( castTarget, walker.getSessionFactory().getTypeConfiguration() );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static String determineColumnType(CastTarget castTarget, TypeConfiguration typeConfiguration) {
|
||||||
|
final String columnDefinition = castTarget.getColumnDefinition();
|
||||||
|
if ( columnDefinition != null ) {
|
||||||
|
return columnDefinition;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
final String typeName = DdlTypeHelper.getTypeName(
|
||||||
|
castTarget.getJdbcMapping(),
|
||||||
|
castTarget.toSize(),
|
||||||
|
typeConfiguration
|
||||||
|
);
|
||||||
|
final int parenthesisIndex = typeName.indexOf( '(' );
|
||||||
|
if ( parenthesisIndex != -1 && typeName.charAt( parenthesisIndex + 1 ) == '$' ) {
|
||||||
|
// Remove length/precision and scale arguments if it contains unresolved variables
|
||||||
|
return typeName.substring( 0, parenthesisIndex );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return typeName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void renderColumns(SqlAppender sqlAppender, XmlTableColumnsClause xmlTableColumnsClause, SqlAstTranslator<?> walker) {
|
||||||
|
sqlAppender.appendSql( " columns" );
|
||||||
|
char separator = ' ';
|
||||||
|
for ( XmlTableColumnDefinition columnDefinition : xmlTableColumnsClause.getColumnDefinitions() ) {
|
||||||
|
sqlAppender.appendSql( separator );
|
||||||
|
if ( columnDefinition instanceof XmlTableQueryColumnDefinition definition ) {
|
||||||
|
renderXmlQueryColumnDefinition( sqlAppender, definition, walker );
|
||||||
|
}
|
||||||
|
else if ( columnDefinition instanceof XmlTableValueColumnDefinition definition ) {
|
||||||
|
renderXmlValueColumnDefinition( sqlAppender, definition, walker );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
renderXmlOrdinalityColumnDefinition(
|
||||||
|
sqlAppender,
|
||||||
|
(XmlTableOrdinalityColumnDefinition) columnDefinition,
|
||||||
|
walker
|
||||||
|
);
|
||||||
|
}
|
||||||
|
separator = ',';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void renderXmlOrdinalityColumnDefinition(SqlAppender sqlAppender, XmlTableOrdinalityColumnDefinition definition, SqlAstTranslator<?> walker) {
|
||||||
|
sqlAppender.appendSql( definition.name() );
|
||||||
|
sqlAppender.appendSql( " for ordinality" );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void renderXmlValueColumnDefinition(SqlAppender sqlAppender, XmlTableValueColumnDefinition definition, SqlAstTranslator<?> walker) {
|
||||||
|
sqlAppender.appendSql( definition.name() );
|
||||||
|
sqlAppender.appendSql( ' ' );
|
||||||
|
sqlAppender.appendSql( determineColumnType( definition.type(), walker ) );
|
||||||
|
|
||||||
|
renderColumnPath( definition.name(), definition.xpath(), sqlAppender, walker );
|
||||||
|
renderDefaultExpression( definition.defaultExpression(), sqlAppender, walker );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void renderColumnPath(String name, @Nullable String xpath, SqlAppender sqlAppender, SqlAstTranslator<?> walker) {
|
||||||
|
if ( xpath != null ) {
|
||||||
|
sqlAppender.appendSql( " path " );
|
||||||
|
sqlAppender.appendSingleQuoteEscapedString( xpath );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// To avoid case sensitivity issues, just pass the path always
|
||||||
|
sqlAppender.appendSql( " path " );
|
||||||
|
sqlAppender.appendSingleQuoteEscapedString( name );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void renderDefaultExpression(@Nullable Expression expression, SqlAppender sqlAppender, SqlAstTranslator<?> walker) {
|
||||||
|
if ( expression != null ) {
|
||||||
|
sqlAppender.appendSql( " default " );
|
||||||
|
if ( supportsParametersInDefault ) {
|
||||||
|
expression.accept( walker );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
walker.render( expression, SqlAstNodeRenderingMode.INLINE_PARAMETERS );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void renderXmlQueryColumnDefinition(SqlAppender sqlAppender, XmlTableQueryColumnDefinition definition, SqlAstTranslator<?> walker) {
|
||||||
|
sqlAppender.appendSql( definition.name() );
|
||||||
|
sqlAppender.appendSql( ' ' );
|
||||||
|
sqlAppender.appendSql( determineColumnType( new CastTarget( definition.type() ), walker ) );
|
||||||
|
|
||||||
|
renderColumnPath( definition.name(), definition.xpath(), sqlAppender, walker );
|
||||||
|
renderDefaultExpression( definition.defaultExpression(), sqlAppender, walker );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected record XmlTableArguments(
|
||||||
|
Expression xpath,
|
||||||
|
Expression xmlDocument,
|
||||||
|
boolean isXmlType,
|
||||||
|
XmlTableColumnsClause columnsClause
|
||||||
|
){
|
||||||
|
public static XmlTableArguments extract(List<? extends SqlAstNode> sqlAstArguments) {
|
||||||
|
final Expression xpath = (Expression) sqlAstArguments.get( 0 );
|
||||||
|
final Expression xmlDocument = (Expression) sqlAstArguments.get( 1 );
|
||||||
|
XmlTableColumnsClause columnsClause = null;
|
||||||
|
int nextIndex = 2;
|
||||||
|
if ( nextIndex < sqlAstArguments.size() ) {
|
||||||
|
final SqlAstNode node = sqlAstArguments.get( nextIndex );
|
||||||
|
if ( node instanceof XmlTableColumnsClause ) {
|
||||||
|
columnsClause = (XmlTableColumnsClause) node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new XmlTableArguments(
|
||||||
|
xpath,
|
||||||
|
xmlDocument,
|
||||||
|
xmlDocument.getExpressionType() != null
|
||||||
|
&& xmlDocument.getExpressionType().getSingleJdbcMapping().getJdbcType().isXml(),
|
||||||
|
columnsClause
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
* Copyright Red Hat Inc. and Hibernate Authors
|
||||||
|
*/
|
||||||
|
package org.hibernate.dialect.function.xml;
|
||||||
|
|
||||||
|
import org.hibernate.metamodel.mapping.JdbcMapping;
|
||||||
|
import org.hibernate.metamodel.mapping.SelectableMapping;
|
||||||
|
import org.hibernate.metamodel.mapping.SelectablePath;
|
||||||
|
import org.hibernate.metamodel.mapping.internal.SelectableMappingImpl;
|
||||||
|
import org.hibernate.query.derived.AnonymousTupleType;
|
||||||
|
import org.hibernate.query.sqm.produce.function.SetReturningFunctionTypeResolver;
|
||||||
|
import org.hibernate.query.sqm.sql.SqmToSqlAstConverter;
|
||||||
|
import org.hibernate.query.sqm.tree.SqmTypedNode;
|
||||||
|
import org.hibernate.query.sqm.tree.expression.SqmXmlTableFunction;
|
||||||
|
import org.hibernate.sql.ast.tree.SqlAstNode;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableColumnDefinition;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableColumnsClause;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableOrdinalityColumnDefinition;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableQueryColumnDefinition;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableValueColumnDefinition;
|
||||||
|
import org.hibernate.type.SqlTypes;
|
||||||
|
import org.hibernate.type.spi.TypeConfiguration;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
public class XmlTableSetReturningFunctionTypeResolver implements SetReturningFunctionTypeResolver {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AnonymousTupleType<?> resolveTupleType(List<? extends SqmTypedNode<?>> arguments, TypeConfiguration typeConfiguration) {
|
||||||
|
final SqmXmlTableFunction.Columns columns = (SqmXmlTableFunction.Columns) arguments.get( arguments.size() - 1 );
|
||||||
|
return columns.createTupleType();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SelectableMapping[] resolveFunctionReturnType(
|
||||||
|
List<? extends SqlAstNode> arguments,
|
||||||
|
String tableIdentifierVariable,
|
||||||
|
boolean lateral,
|
||||||
|
boolean withOrdinality,
|
||||||
|
SqmToSqlAstConverter converter) {
|
||||||
|
XmlTableColumnsClause columnsClause = null;
|
||||||
|
for ( SqlAstNode argument : arguments ) {
|
||||||
|
if ( argument instanceof XmlTableColumnsClause ) {
|
||||||
|
columnsClause = (XmlTableColumnsClause) argument;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert columnsClause != null;
|
||||||
|
|
||||||
|
final List<XmlTableColumnDefinition> columnDefinitions = columnsClause.getColumnDefinitions();
|
||||||
|
final List<SelectableMapping> selectableMappings = new ArrayList<>( columnDefinitions.size() );
|
||||||
|
addSelectableMappings( selectableMappings, columnsClause, converter );
|
||||||
|
return selectableMappings.toArray( new SelectableMapping[0] );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addSelectableMappings(List<SelectableMapping> selectableMappings, XmlTableColumnsClause columnsClause, SqmToSqlAstConverter converter) {
|
||||||
|
for ( XmlTableColumnDefinition columnDefinition : columnsClause.getColumnDefinitions() ) {
|
||||||
|
if ( columnDefinition instanceof XmlTableQueryColumnDefinition definition ) {
|
||||||
|
addSelectableMappings( selectableMappings, definition, converter );
|
||||||
|
}
|
||||||
|
else if ( columnDefinition instanceof XmlTableValueColumnDefinition definition ) {
|
||||||
|
addSelectableMappings( selectableMappings, definition, converter );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
final XmlTableOrdinalityColumnDefinition definition
|
||||||
|
= (XmlTableOrdinalityColumnDefinition) columnDefinition;
|
||||||
|
addSelectableMappings( selectableMappings, definition, converter );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addSelectableMappings(List<SelectableMapping> selectableMappings, XmlTableOrdinalityColumnDefinition definition, SqmToSqlAstConverter converter) {
|
||||||
|
addSelectableMapping(
|
||||||
|
selectableMappings,
|
||||||
|
definition.name(),
|
||||||
|
converter.getCreationContext().getTypeConfiguration().getBasicTypeForJavaType( Long.class ),
|
||||||
|
converter );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addSelectableMappings(List<SelectableMapping> selectableMappings, XmlTableValueColumnDefinition definition, SqmToSqlAstConverter converter) {
|
||||||
|
addSelectableMapping(
|
||||||
|
selectableMappings,
|
||||||
|
definition.name(),
|
||||||
|
definition.type().getJdbcMapping(),
|
||||||
|
converter );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addSelectableMappings(List<SelectableMapping> selectableMappings, XmlTableQueryColumnDefinition definition, SqmToSqlAstConverter converter) {
|
||||||
|
addSelectableMapping(
|
||||||
|
selectableMappings,
|
||||||
|
definition.name(),
|
||||||
|
converter.getCreationContext().getTypeConfiguration().getBasicTypeRegistry()
|
||||||
|
.resolve( String.class, SqlTypes.SQLXML ),
|
||||||
|
converter );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addSelectableMapping(List<SelectableMapping> selectableMappings, String name, JdbcMapping type, SqmToSqlAstConverter converter) {
|
||||||
|
selectableMappings.add( new SelectableMappingImpl(
|
||||||
|
"",
|
||||||
|
name,
|
||||||
|
new SelectablePath( name ),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
type
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
@ -4463,6 +4463,26 @@ JpaExpression<String> xmlagg(
|
|||||||
@Incubating
|
@Incubating
|
||||||
JpaJsonTableFunction jsonTable(Expression<?> jsonDocument, Expression<String> jsonPath);
|
JpaJsonTableFunction jsonTable(Expression<?> jsonDocument, Expression<String> jsonPath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@code xmltable} function expression to generate rows from XML elements.
|
||||||
|
*
|
||||||
|
* @since 7.0
|
||||||
|
* @see JpaSelectCriteria#from(JpaSetReturningFunction)
|
||||||
|
* @see JpaFrom#join(JpaSetReturningFunction)
|
||||||
|
*/
|
||||||
|
@Incubating
|
||||||
|
JpaXmlTableFunction xmlTable(String xpath, Expression<?> xmlDocument);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@code xmltable} function expression to generate rows from XML elements.
|
||||||
|
*
|
||||||
|
* @since 7.0
|
||||||
|
* @see JpaSelectCriteria#from(JpaSetReturningFunction)
|
||||||
|
* @see JpaFrom#join(JpaSetReturningFunction)
|
||||||
|
*/
|
||||||
|
@Incubating
|
||||||
|
JpaXmlTableFunction xmlTable(Expression<String> xpath, Expression<?> xmlDocument);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
JpaPredicate and(List<Predicate> restrictions);
|
JpaPredicate and(List<Predicate> restrictions);
|
||||||
|
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
* Copyright Red Hat Inc. and Hibernate Authors
|
||||||
|
*/
|
||||||
|
package org.hibernate.query.criteria;
|
||||||
|
|
||||||
|
import jakarta.persistence.criteria.Expression;
|
||||||
|
import org.hibernate.Incubating;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A special node for column defined for a {@code xmltable} function.
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
@Incubating
|
||||||
|
public interface JpaXmlTableColumnNode<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the default value to use if resolving the XPath expression doesn't produce results.
|
||||||
|
*
|
||||||
|
* @return {@code this} for method chaining
|
||||||
|
*/
|
||||||
|
JpaXmlTableColumnNode<T> defaultValue(T value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the default value to use if resolving the XPath expression doesn't produce results.
|
||||||
|
*
|
||||||
|
* @return {@code this} for method chaining
|
||||||
|
*/
|
||||||
|
JpaXmlTableColumnNode<T> defaultExpression(Expression<T> expression);
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
* Copyright Red Hat Inc. and Hibernate Authors
|
||||||
|
*/
|
||||||
|
package org.hibernate.query.criteria;
|
||||||
|
|
||||||
|
import org.hibernate.Incubating;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A special expression for the {@code xmltable} function.
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
@Incubating
|
||||||
|
public interface JpaXmlTableFunction {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like {@link #queryColumn(String, String)}, but uses the column name as XPath expression.
|
||||||
|
*
|
||||||
|
* @return The {@link JpaXmlTableColumnNode} for the column
|
||||||
|
*/
|
||||||
|
JpaXmlTableColumnNode<String> queryColumn(String columnName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a string column on the result type with the given name for which the value can be obtained
|
||||||
|
* by evaluating {@code xmlquery} with the given XPath expression on the XML document.
|
||||||
|
*
|
||||||
|
* @return The {@link JpaXmlTableColumnNode} for the column
|
||||||
|
*/
|
||||||
|
JpaXmlTableColumnNode<String> queryColumn(String columnName, String xpath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like {@link #valueColumn(String, Class, String)} but uses the column name as XPath expression.
|
||||||
|
*
|
||||||
|
* @return The {@link JpaXmlTableColumnNode} for the column
|
||||||
|
*/
|
||||||
|
<X> JpaXmlTableColumnNode<X> valueColumn(String columnName, Class<X> type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like {@link #valueColumn(String, JpaCastTarget, String)} but uses the column name as XPath expression.
|
||||||
|
*
|
||||||
|
* @return The {@link JpaXmlTableColumnNode} for the column
|
||||||
|
*/
|
||||||
|
<X> JpaXmlTableColumnNode<X> valueColumn(String columnName, JpaCastTarget<X> castTarget);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like {@link #valueColumn(String, JpaCastTarget, String)}, but converting the {@link Class}
|
||||||
|
* to {@link JpaCastTarget} via {@link HibernateCriteriaBuilder#castTarget(Class)}.
|
||||||
|
*
|
||||||
|
* @return The {@link JpaXmlTableColumnNode} for the column
|
||||||
|
*/
|
||||||
|
<X> JpaXmlTableColumnNode<X> valueColumn(String columnName, Class<X> type, String xpath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines an column on the result type with the given name and type for which the value can be obtained by the given XPath path expression.
|
||||||
|
*
|
||||||
|
* @return The {@link JpaXmlTableColumnNode} for the column
|
||||||
|
*/
|
||||||
|
<X> JpaXmlTableColumnNode<X> valueColumn(String columnName, JpaCastTarget<X> castTarget, String xpath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a long column on the result type with the given name which is set to the ordinality i.e.
|
||||||
|
* the 1-based position of the processed element. Ordinality starts again at 1 within nested paths.
|
||||||
|
*
|
||||||
|
* @return {@code this} for method chaining
|
||||||
|
*/
|
||||||
|
JpaXmlTableFunction ordinalityColumn(String columnName);
|
||||||
|
}
|
@ -3878,4 +3878,16 @@ public JpaJsonTableFunction jsonTable(Expression<?> jsonDocument, String jsonPat
|
|||||||
public JpaJsonTableFunction jsonTable(Expression<?> jsonDocument, Expression<String> jsonPath) {
|
public JpaJsonTableFunction jsonTable(Expression<?> jsonDocument, Expression<String> jsonPath) {
|
||||||
return criteriaBuilder.jsonTable( jsonDocument, jsonPath );
|
return criteriaBuilder.jsonTable( jsonDocument, jsonPath );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Incubating
|
||||||
|
@Override
|
||||||
|
public JpaXmlTableFunction xmlTable(String xpath, Expression<?> xmlDocument) {
|
||||||
|
return criteriaBuilder.xmlTable( xpath, xmlDocument );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Incubating
|
||||||
|
@Override
|
||||||
|
public JpaXmlTableFunction xmlTable(Expression<String> xpath, Expression<?> xmlDocument) {
|
||||||
|
return criteriaBuilder.xmlTable( xpath, xmlDocument );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,6 +69,7 @@
|
|||||||
import org.hibernate.query.criteria.JpaJsonValueNode;
|
import org.hibernate.query.criteria.JpaJsonValueNode;
|
||||||
import org.hibernate.query.criteria.JpaRoot;
|
import org.hibernate.query.criteria.JpaRoot;
|
||||||
import org.hibernate.query.criteria.JpaSearchOrder;
|
import org.hibernate.query.criteria.JpaSearchOrder;
|
||||||
|
import org.hibernate.query.criteria.JpaXmlTableColumnNode;
|
||||||
import org.hibernate.query.derived.AnonymousTupleType;
|
import org.hibernate.query.derived.AnonymousTupleType;
|
||||||
import org.hibernate.query.hql.HqlLogging;
|
import org.hibernate.query.hql.HqlLogging;
|
||||||
import org.hibernate.query.hql.spi.DotIdentifierConsumer;
|
import org.hibernate.query.hql.spi.DotIdentifierConsumer;
|
||||||
@ -3204,6 +3205,64 @@ public SqmExpression<?> visitXmlaggFunction(HqlParser.XmlaggFunctionContext ctx)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object visitXmltableFunction(HqlParser.XmltableFunctionContext ctx) {
|
||||||
|
checkXmlFunctionsEnabled( ctx );
|
||||||
|
final List<HqlParser.ExpressionContext> argumentsContexts = ctx.expression();
|
||||||
|
//noinspection unchecked
|
||||||
|
final SqmExpression<String> xpath = (SqmExpression<String>) argumentsContexts.get( 0 ).accept( this );
|
||||||
|
final SqmExpression<?> document = (SqmExpression<?>) argumentsContexts.get( 1 ).accept( this );
|
||||||
|
final SqmXmlTableFunction<?> xmlTable = creationContext.getNodeBuilder().xmlTable( xpath, document);
|
||||||
|
visitColumns( xmlTable, ctx.xmltableColumnsClause().xmltableColumn() );
|
||||||
|
return xmlTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void visitColumns(SqmXmlTableFunction<?> xmlTable, List<HqlParser.XmltableColumnContext> columnContexts) {
|
||||||
|
for ( HqlParser.XmltableColumnContext columnContext : columnContexts ) {
|
||||||
|
if ( columnContext instanceof HqlParser.XmlTableQueryColumnContext queryColumnContext ) {
|
||||||
|
final String columnName = visitIdentifier( queryColumnContext.identifier() );
|
||||||
|
final TerminalNode pathNode = queryColumnContext.STRING_LITERAL();
|
||||||
|
final String xpath;
|
||||||
|
if ( pathNode == null ) {
|
||||||
|
xpath = null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
xpath = unquoteStringLiteral( pathNode.getText() );
|
||||||
|
}
|
||||||
|
final JpaXmlTableColumnNode<String> node = xmlTable.queryColumn( columnName, xpath );
|
||||||
|
final HqlParser.XmltableDefaultClauseContext defaultClause = queryColumnContext.xmltableDefaultClause();
|
||||||
|
if ( defaultClause != null ) {
|
||||||
|
//noinspection unchecked
|
||||||
|
node.defaultExpression( (Expression<String>) defaultClause.expression().accept( this ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ( columnContext instanceof HqlParser.XmlTableValueColumnContext valueColumnContext ) {
|
||||||
|
final String columnName = visitIdentifier( valueColumnContext.identifier() );
|
||||||
|
//noinspection unchecked
|
||||||
|
final SqmCastTarget<Object> castTarget = (SqmCastTarget<Object>) visitCastTarget( valueColumnContext.castTarget() );
|
||||||
|
final TerminalNode pathNode = valueColumnContext.STRING_LITERAL();
|
||||||
|
final String xpath;
|
||||||
|
if ( pathNode == null ) {
|
||||||
|
xpath = null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
xpath = unquoteStringLiteral( pathNode.getText() );
|
||||||
|
}
|
||||||
|
final JpaXmlTableColumnNode<Object> node = xmlTable.valueColumn( columnName, castTarget, xpath );
|
||||||
|
final HqlParser.XmltableDefaultClauseContext defaultClause = valueColumnContext.xmltableDefaultClause();
|
||||||
|
if ( defaultClause != null ) {
|
||||||
|
//noinspection unchecked
|
||||||
|
node.defaultExpression( (Expression<Object>) defaultClause.expression().accept( this ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
final HqlParser.XmlTableOrdinalityColumnContext ordinalityColumnContext
|
||||||
|
= (HqlParser.XmlTableOrdinalityColumnContext) columnContext;
|
||||||
|
xmlTable.ordinalityColumn( visitIdentifier( ordinalityColumnContext.identifier() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void checkXmlFunctionsEnabled(ParserRuleContext ctx) {
|
private void checkXmlFunctionsEnabled(ParserRuleContext ctx) {
|
||||||
if ( !creationOptions.isXmlFunctionsEnabled() ) {
|
if ( !creationOptions.isXmlFunctionsEnabled() ) {
|
||||||
throw new SemanticException(
|
throw new SemanticException(
|
||||||
|
@ -54,6 +54,7 @@
|
|||||||
import org.hibernate.query.sqm.tree.expression.SqmSetReturningFunction;
|
import org.hibernate.query.sqm.tree.expression.SqmSetReturningFunction;
|
||||||
import org.hibernate.query.sqm.tree.expression.SqmTuple;
|
import org.hibernate.query.sqm.tree.expression.SqmTuple;
|
||||||
import org.hibernate.query.sqm.tree.expression.SqmXmlElementExpression;
|
import org.hibernate.query.sqm.tree.expression.SqmXmlElementExpression;
|
||||||
|
import org.hibernate.query.sqm.tree.expression.SqmXmlTableFunction;
|
||||||
import org.hibernate.query.sqm.tree.from.SqmRoot;
|
import org.hibernate.query.sqm.tree.from.SqmRoot;
|
||||||
import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement;
|
import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement;
|
||||||
import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement;
|
import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement;
|
||||||
@ -885,6 +886,12 @@ <T> SqmJsonValueExpression<T> jsonValue(
|
|||||||
@Override
|
@Override
|
||||||
SqmJsonTableFunction<?> jsonTable(Expression<?> jsonDocument, Expression<String> jsonPath);
|
SqmJsonTableFunction<?> jsonTable(Expression<?> jsonDocument, Expression<String> jsonPath);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
SqmXmlTableFunction<?> xmlTable(String xpath, Expression<?> xmlDocument);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
SqmXmlTableFunction<?> xmlTable(Expression<String> xpath, Expression<?> xmlDocument);
|
||||||
|
|
||||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// Covariant overrides
|
// Covariant overrides
|
||||||
|
|
||||||
|
@ -5956,7 +5956,7 @@ public SqmJsonTableFunction<?> jsonTable(Expression<?> jsonDocument, String json
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SqmJsonTableFunction<?> jsonTable(Expression<?> jsonDocument, Expression<String> jsonPath) {
|
public SqmJsonTableFunction<?> jsonTable(Expression<?> jsonDocument, @Nullable Expression<String> jsonPath) {
|
||||||
return (SqmJsonTableFunction<?>) getSetReturningFunctionDescriptor( "json_table" ).generateSqmExpression(
|
return (SqmJsonTableFunction<?>) getSetReturningFunctionDescriptor( "json_table" ).generateSqmExpression(
|
||||||
jsonPath == null
|
jsonPath == null
|
||||||
? asList( (SqmTypedNode<?>) jsonDocument )
|
? asList( (SqmTypedNode<?>) jsonDocument )
|
||||||
@ -5964,4 +5964,17 @@ public SqmJsonTableFunction<?> jsonTable(Expression<?> jsonDocument, Expression<
|
|||||||
queryEngine
|
queryEngine
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SqmXmlTableFunction<?> xmlTable(String xpath, Expression<?> xmlDocument) {
|
||||||
|
return xmlTable( value( xpath ), xmlDocument );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SqmXmlTableFunction<?> xmlTable(Expression<String> xpath, Expression<?> xmlDocument) {
|
||||||
|
return (SqmXmlTableFunction<?>) getSetReturningFunctionDescriptor( "xmltable" ).generateSqmExpression(
|
||||||
|
asList( (SqmTypedNode<?>) xpath, (SqmTypedNode<?>) xmlDocument ),
|
||||||
|
queryEngine
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,7 @@ public static FunctionArgumentTypeResolver invariant(
|
|||||||
return new AbstractFunctionArgumentTypeResolver() {
|
return new AbstractFunctionArgumentTypeResolver() {
|
||||||
@Override
|
@Override
|
||||||
public @Nullable MappingModelExpressible<?> resolveFunctionArgumentType(List<? extends SqmTypedNode<?>> arguments, int argumentIndex, SqmToSqlAstConverter converter) {
|
public @Nullable MappingModelExpressible<?> resolveFunctionArgumentType(List<? extends SqmTypedNode<?>> arguments, int argumentIndex, SqmToSqlAstConverter converter) {
|
||||||
return expressibles[argumentIndex];
|
return argumentIndex < expressibles.length ? expressibles[argumentIndex] : null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -103,6 +103,9 @@ public static FunctionArgumentTypeResolver invariant(FunctionParameterType... ty
|
|||||||
return new AbstractFunctionArgumentTypeResolver() {
|
return new AbstractFunctionArgumentTypeResolver() {
|
||||||
@Override
|
@Override
|
||||||
public @Nullable MappingModelExpressible<?> resolveFunctionArgumentType(List<? extends SqmTypedNode<?>> arguments, int argumentIndex, SqmToSqlAstConverter converter) {
|
public @Nullable MappingModelExpressible<?> resolveFunctionArgumentType(List<? extends SqmTypedNode<?>> arguments, int argumentIndex, SqmToSqlAstConverter converter) {
|
||||||
|
if ( argumentIndex >= types.length ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return getMappingModelExpressible(
|
return getMappingModelExpressible(
|
||||||
converter.getCreationContext().getTypeConfiguration(),
|
converter.getCreationContext().getTypeConfiguration(),
|
||||||
types[argumentIndex]
|
types[argumentIndex]
|
||||||
@ -188,7 +191,9 @@ public static FunctionArgumentTypeResolver byArgument(FunctionArgumentTypeResolv
|
|||||||
return new AbstractFunctionArgumentTypeResolver() {
|
return new AbstractFunctionArgumentTypeResolver() {
|
||||||
@Override
|
@Override
|
||||||
public @Nullable MappingModelExpressible<?> resolveFunctionArgumentType(List<? extends SqmTypedNode<?>> arguments, int argumentIndex, SqmToSqlAstConverter converter) {
|
public @Nullable MappingModelExpressible<?> resolveFunctionArgumentType(List<? extends SqmTypedNode<?>> arguments, int argumentIndex, SqmToSqlAstConverter converter) {
|
||||||
return resolvers[argumentIndex].resolveFunctionArgumentType( arguments, argumentIndex, converter );
|
return argumentIndex < resolvers.length
|
||||||
|
? resolvers[argumentIndex].resolveFunctionArgumentType( arguments, argumentIndex, converter )
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,486 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
* Copyright Red Hat Inc. and Hibernate Authors
|
||||||
|
*/
|
||||||
|
package org.hibernate.query.sqm.tree.expression;
|
||||||
|
|
||||||
|
import jakarta.persistence.criteria.Expression;
|
||||||
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
import org.hibernate.internal.util.QuotingHelper;
|
||||||
|
import org.hibernate.query.criteria.JpaCastTarget;
|
||||||
|
import org.hibernate.query.criteria.JpaXmlTableColumnNode;
|
||||||
|
import org.hibernate.query.criteria.JpaXmlTableFunction;
|
||||||
|
import org.hibernate.query.derived.AnonymousTupleType;
|
||||||
|
import org.hibernate.query.sqm.NodeBuilder;
|
||||||
|
import org.hibernate.query.sqm.SemanticQueryWalker;
|
||||||
|
import org.hibernate.query.sqm.SqmExpressible;
|
||||||
|
import org.hibernate.query.sqm.function.SelfRenderingSqmSetReturningFunction;
|
||||||
|
import org.hibernate.query.sqm.function.SetReturningFunctionRenderer;
|
||||||
|
import org.hibernate.query.sqm.function.SqmSetReturningFunctionDescriptor;
|
||||||
|
import org.hibernate.query.sqm.produce.function.ArgumentsValidator;
|
||||||
|
import org.hibernate.query.sqm.produce.function.SetReturningFunctionTypeResolver;
|
||||||
|
import org.hibernate.query.sqm.sql.SqmToSqlAstConverter;
|
||||||
|
import org.hibernate.query.sqm.tree.SqmCopyContext;
|
||||||
|
import org.hibernate.query.sqm.tree.SqmTypedNode;
|
||||||
|
import org.hibernate.sql.ast.tree.SqlAstNode;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.CastTarget;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableColumnDefinition;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableColumnsClause;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableOrdinalityColumnDefinition;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableQueryColumnDefinition;
|
||||||
|
import org.hibernate.sql.ast.tree.expression.XmlTableValueColumnDefinition;
|
||||||
|
import org.hibernate.type.BasicType;
|
||||||
|
import org.hibernate.type.SqlTypes;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
public class SqmXmlTableFunction<T> extends SelfRenderingSqmSetReturningFunction<T> implements JpaXmlTableFunction {
|
||||||
|
|
||||||
|
private final Columns columns;
|
||||||
|
|
||||||
|
public SqmXmlTableFunction(
|
||||||
|
SqmSetReturningFunctionDescriptor descriptor,
|
||||||
|
SetReturningFunctionRenderer renderer,
|
||||||
|
@Nullable ArgumentsValidator argumentsValidator,
|
||||||
|
SetReturningFunctionTypeResolver setReturningTypeResolver,
|
||||||
|
NodeBuilder nodeBuilder,
|
||||||
|
SqmExpression<String> xpath,
|
||||||
|
SqmExpression<?> document) {
|
||||||
|
this(
|
||||||
|
descriptor,
|
||||||
|
renderer,
|
||||||
|
Arrays.asList( xpath, document, null ),
|
||||||
|
argumentsValidator,
|
||||||
|
setReturningTypeResolver,
|
||||||
|
nodeBuilder,
|
||||||
|
new ArrayList<>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SqmXmlTableFunction(
|
||||||
|
SqmSetReturningFunctionDescriptor descriptor,
|
||||||
|
SetReturningFunctionRenderer renderer,
|
||||||
|
List<SqmTypedNode<?>> arguments,
|
||||||
|
@Nullable ArgumentsValidator argumentsValidator,
|
||||||
|
SetReturningFunctionTypeResolver setReturningTypeResolver,
|
||||||
|
NodeBuilder nodeBuilder,
|
||||||
|
ArrayList<ColumnDefinition> columnDefinitions) {
|
||||||
|
super( descriptor, renderer, arguments, argumentsValidator, setReturningTypeResolver, nodeBuilder, "xmltable" );
|
||||||
|
this.columns = new Columns( this, columnDefinitions );
|
||||||
|
arguments.set( arguments.size() - 1, this.columns );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SqmXmlTableFunction<T> copy(SqmCopyContext context) {
|
||||||
|
final SqmXmlTableFunction<T> existing = context.getCopy( this );
|
||||||
|
if ( existing != null ) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
final List<? extends SqmTypedNode<?>> arguments = getArguments();
|
||||||
|
final List<SqmTypedNode<?>> argumentsCopy = new ArrayList<>( arguments.size() );
|
||||||
|
for ( int i = 0; i < arguments.size() - 1; i++ ) {
|
||||||
|
argumentsCopy.add( arguments.get( i ).copy( context ) );
|
||||||
|
}
|
||||||
|
final SqmXmlTableFunction<T> tableFunction = new SqmXmlTableFunction<>(
|
||||||
|
getFunctionDescriptor(),
|
||||||
|
getFunctionRenderer(),
|
||||||
|
argumentsCopy,
|
||||||
|
getArgumentsValidator(),
|
||||||
|
getSetReturningTypeResolver(),
|
||||||
|
nodeBuilder(),
|
||||||
|
columns.columnDefinitions
|
||||||
|
);
|
||||||
|
context.registerCopy( this, tableFunction );
|
||||||
|
tableFunction.columns.columnDefinitions.ensureCapacity( columns.columnDefinitions.size() );
|
||||||
|
for ( ColumnDefinition columnDefinition : columns.columnDefinitions ) {
|
||||||
|
tableFunction.columns.columnDefinitions.add( columnDefinition.copy( context ) );
|
||||||
|
}
|
||||||
|
return tableFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<SqlAstNode> resolveSqlAstArguments(List<? extends SqmTypedNode<?>> sqmArguments, SqmToSqlAstConverter walker) {
|
||||||
|
final List<SqlAstNode> sqlAstNodes = super.resolveSqlAstArguments( sqmArguments, walker );
|
||||||
|
// The last argument is the SqmXmlTableFunction.Columns which will convert to null, so remove that
|
||||||
|
sqlAstNodes.remove( sqlAstNodes.size() - 1 );
|
||||||
|
|
||||||
|
final List<XmlTableColumnDefinition> definitions = new ArrayList<>( columns.columnDefinitions.size() );
|
||||||
|
for ( ColumnDefinition columnDefinition : columns.columnDefinitions ) {
|
||||||
|
definitions.add( columnDefinition.convertToSqlAst( walker ) );
|
||||||
|
}
|
||||||
|
sqlAstNodes.add( new XmlTableColumnsClause( definitions ) );
|
||||||
|
return sqlAstNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JpaXmlTableColumnNode<String> queryColumn(String columnName) {
|
||||||
|
return queryColumn( columnName, null );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JpaXmlTableColumnNode<String> queryColumn(String columnName, @Nullable String xpath) {
|
||||||
|
final QueryColumnDefinition definition = new QueryColumnDefinition(
|
||||||
|
this,
|
||||||
|
columnName,
|
||||||
|
nodeBuilder().getTypeConfiguration().getBasicTypeRegistry().resolve( String.class, SqlTypes.SQLXML ),
|
||||||
|
xpath
|
||||||
|
);
|
||||||
|
columns.addColumn( definition );
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <X> JpaXmlTableColumnNode<X> valueColumn(String columnName, Class<X> type) {
|
||||||
|
return valueColumn( columnName, type, null );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <X> JpaXmlTableColumnNode<X> valueColumn(String columnName, JpaCastTarget<X> castTarget) {
|
||||||
|
return valueColumn( columnName, castTarget, null );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <X> JpaXmlTableColumnNode<X> valueColumn(String columnName, Class<X> type, String xpath) {
|
||||||
|
return valueColumn( columnName, nodeBuilder().castTarget( type ), xpath );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <X> JpaXmlTableColumnNode<X> valueColumn(String columnName, JpaCastTarget<X> castTarget, @Nullable String xpath) {
|
||||||
|
final ValueColumnDefinition<X> definition = new ValueColumnDefinition<>(
|
||||||
|
this,
|
||||||
|
columnName,
|
||||||
|
(SqmCastTarget<X>) castTarget,
|
||||||
|
xpath
|
||||||
|
);
|
||||||
|
columns.addColumn( definition );
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SqmXmlTableFunction<T> ordinalityColumn(String columnName) {
|
||||||
|
columns.addColumn( new OrdinalityColumnDefinition( columnName, nodeBuilder().getLongType() ) );
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void appendHqlString(StringBuilder sb) {
|
||||||
|
sb.append( "xmltable(" );
|
||||||
|
getArguments().get( 0 ).appendHqlString( sb );
|
||||||
|
sb.append( " passing " );
|
||||||
|
getArguments().get( 1 ).appendHqlString( sb );
|
||||||
|
columns.appendHqlString( sb );
|
||||||
|
sb.append( ')' );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkTypeResolved() {
|
||||||
|
if ( isTypeResolved() ) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Type for xmltable function is already resolved. Mutation is not allowed anymore" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface ColumnDefinition {
|
||||||
|
|
||||||
|
String name();
|
||||||
|
|
||||||
|
ColumnDefinition copy(SqmCopyContext context);
|
||||||
|
|
||||||
|
XmlTableColumnDefinition convertToSqlAst(SqmToSqlAstConverter walker);
|
||||||
|
|
||||||
|
void appendHqlString(StringBuilder sb);
|
||||||
|
|
||||||
|
int populateTupleType(int offset, String[] componentNames, SqmExpressible<?>[] componentTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class QueryColumnDefinition implements ColumnDefinition, JpaXmlTableColumnNode<String> {
|
||||||
|
private final SqmXmlTableFunction<?> table;
|
||||||
|
private final String name;
|
||||||
|
private final BasicType<String> type;
|
||||||
|
private final @Nullable String xpath;
|
||||||
|
private @Nullable SqmExpression<String> defaultExpression;
|
||||||
|
|
||||||
|
QueryColumnDefinition(SqmXmlTableFunction<?> table, String name, BasicType<String> type, @Nullable String xpath) {
|
||||||
|
this.table = table;
|
||||||
|
this.name = name;
|
||||||
|
this.type = type;
|
||||||
|
this.xpath = xpath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private QueryColumnDefinition(SqmXmlTableFunction<?> table, String name, BasicType<String> type, @Nullable String xpath, @Nullable SqmExpression<String> defaultExpression) {
|
||||||
|
this.table = table;
|
||||||
|
this.name = name;
|
||||||
|
this.type = type;
|
||||||
|
this.xpath = xpath;
|
||||||
|
this.defaultExpression = defaultExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ColumnDefinition copy(SqmCopyContext context) {
|
||||||
|
return new QueryColumnDefinition(
|
||||||
|
table.copy( context ),
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
xpath,
|
||||||
|
defaultExpression == null ? null : defaultExpression.copy( context )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public XmlTableColumnDefinition convertToSqlAst(SqmToSqlAstConverter walker) {
|
||||||
|
return new XmlTableQueryColumnDefinition(
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
xpath,
|
||||||
|
defaultExpression == null
|
||||||
|
? null
|
||||||
|
: (org.hibernate.sql.ast.tree.expression.Expression) defaultExpression.accept( walker )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JpaXmlTableColumnNode<String> defaultValue(String value) {
|
||||||
|
return defaultExpression( table.nodeBuilder().value( value ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JpaXmlTableColumnNode<String> defaultExpression(Expression<String> expression) {
|
||||||
|
table.checkTypeResolved();
|
||||||
|
this.defaultExpression = (SqmExpression<String>) expression;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void appendHqlString(StringBuilder sb) {
|
||||||
|
sb.append( name );
|
||||||
|
sb.append( " xml" );
|
||||||
|
if ( xpath != null ) {
|
||||||
|
sb.append( " path " );
|
||||||
|
QuotingHelper.appendSingleQuoteEscapedString( sb, xpath );
|
||||||
|
}
|
||||||
|
if ( defaultExpression != null ) {
|
||||||
|
sb.append( " default " );
|
||||||
|
defaultExpression.appendHqlString( sb );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int populateTupleType(int offset, String[] componentNames, SqmExpressible<?>[] componentTypes) {
|
||||||
|
componentNames[offset] = name;
|
||||||
|
componentTypes[offset] = type;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class ValueColumnDefinition<X> implements ColumnDefinition, JpaXmlTableColumnNode<X> {
|
||||||
|
private final SqmXmlTableFunction<?> table;
|
||||||
|
private final String name;
|
||||||
|
private final SqmCastTarget<X> type;
|
||||||
|
private final @Nullable String xpath;
|
||||||
|
private @Nullable SqmExpression<X> defaultExpression;
|
||||||
|
|
||||||
|
ValueColumnDefinition(SqmXmlTableFunction<?> table, String name, SqmCastTarget<X> type, @Nullable String xpath) {
|
||||||
|
this.table = table;
|
||||||
|
this.name = name;
|
||||||
|
this.type = type;
|
||||||
|
this.xpath = xpath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValueColumnDefinition(SqmXmlTableFunction<?> table, String name, SqmCastTarget<X> type, @Nullable String xpath, @Nullable SqmExpression<X> defaultExpression) {
|
||||||
|
this.table = table;
|
||||||
|
this.name = name;
|
||||||
|
this.type = type;
|
||||||
|
this.xpath = xpath;
|
||||||
|
this.defaultExpression = defaultExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ColumnDefinition copy(SqmCopyContext context) {
|
||||||
|
return new ValueColumnDefinition<>(
|
||||||
|
table.copy( context ),
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
xpath,
|
||||||
|
defaultExpression == null ? null : defaultExpression.copy( context )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public XmlTableColumnDefinition convertToSqlAst(SqmToSqlAstConverter walker) {
|
||||||
|
return new XmlTableValueColumnDefinition(
|
||||||
|
name,
|
||||||
|
(CastTarget) type.accept( walker ),
|
||||||
|
xpath,
|
||||||
|
defaultExpression == null
|
||||||
|
? null
|
||||||
|
: (org.hibernate.sql.ast.tree.expression.Expression) defaultExpression.accept( walker )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JpaXmlTableColumnNode<X> defaultValue(X value) {
|
||||||
|
return defaultExpression( table.nodeBuilder().value( value ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JpaXmlTableColumnNode<X> defaultExpression(Expression<X> expression) {
|
||||||
|
table.checkTypeResolved();
|
||||||
|
this.defaultExpression = (SqmExpression<X>) expression;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void appendHqlString(StringBuilder sb) {
|
||||||
|
sb.append( name );
|
||||||
|
sb.append( ' ' );
|
||||||
|
type.appendHqlString( sb );
|
||||||
|
if ( xpath != null ) {
|
||||||
|
sb.append( " path " );
|
||||||
|
QuotingHelper.appendSingleQuoteEscapedString( sb, xpath );
|
||||||
|
}
|
||||||
|
if ( defaultExpression != null ) {
|
||||||
|
sb.append( " default " );
|
||||||
|
defaultExpression.appendHqlString( sb );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int populateTupleType(int offset, String[] componentNames, SqmExpressible<?>[] componentTypes) {
|
||||||
|
componentNames[offset] = name;
|
||||||
|
componentTypes[offset] = type.getNodeType();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
record OrdinalityColumnDefinition(String name, BasicType<Long> type) implements ColumnDefinition {
|
||||||
|
@Override
|
||||||
|
public ColumnDefinition copy(SqmCopyContext context) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public XmlTableColumnDefinition convertToSqlAst(SqmToSqlAstConverter walker) {
|
||||||
|
return new XmlTableOrdinalityColumnDefinition( name );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void appendHqlString(StringBuilder sb) {
|
||||||
|
sb.append( name );
|
||||||
|
sb.append( " for ordinality" );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int populateTupleType(int offset, String[] componentNames, SqmExpressible<?>[] componentTypes) {
|
||||||
|
componentNames[offset] = name;
|
||||||
|
componentTypes[offset] = type;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Columns implements SqmTypedNode<Object> {
|
||||||
|
|
||||||
|
private final SqmXmlTableFunction<?> table;
|
||||||
|
private final Set<String> columnNames;
|
||||||
|
private final ArrayList<ColumnDefinition> columnDefinitions;
|
||||||
|
|
||||||
|
private Columns(SqmXmlTableFunction<?> table, ArrayList<ColumnDefinition> columnDefinitions) {
|
||||||
|
this.table = table;
|
||||||
|
this.columnDefinitions = columnDefinitions;
|
||||||
|
this.columnNames = new HashSet<>( columnDefinitions.size() );
|
||||||
|
for ( ColumnDefinition columnDefinition : columnDefinitions ) {
|
||||||
|
columnNames.add( columnDefinition.name() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnonymousTupleType<?> createTupleType() {
|
||||||
|
if ( columnDefinitions.isEmpty() ) {
|
||||||
|
throw new IllegalArgumentException( "Couldn't determine types of columns of function 'xmltable'" );
|
||||||
|
}
|
||||||
|
final SqmExpressible<?>[] componentTypes = new SqmExpressible<?>[columnDefinitions.size()];
|
||||||
|
final String[] componentNames = new String[columnDefinitions.size()];
|
||||||
|
int offset = 0;
|
||||||
|
for ( ColumnDefinition columnDefinition : columnDefinitions ) {
|
||||||
|
offset += columnDefinition.populateTupleType( offset, componentNames, componentTypes );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check
|
||||||
|
assert offset == componentTypes.length;
|
||||||
|
|
||||||
|
return new AnonymousTupleType<>( componentTypes, componentNames );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Columns copy(SqmCopyContext context) {
|
||||||
|
final ArrayList<ColumnDefinition> definitions = new ArrayList<>( columnDefinitions.size() );
|
||||||
|
for ( ColumnDefinition columnDefinition : columnDefinitions ) {
|
||||||
|
definitions.add( columnDefinition.copy( context ) );
|
||||||
|
}
|
||||||
|
return new Columns( context.getCopy( table ), definitions );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addColumn(ColumnDefinition columnDefinition) {
|
||||||
|
table.checkTypeResolved();
|
||||||
|
if ( !columnNames.add( columnDefinition.name() ) ) {
|
||||||
|
throw new IllegalStateException( "Duplicate column: " + columnDefinition.name() );
|
||||||
|
}
|
||||||
|
columnDefinitions.add( columnDefinition );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void appendHqlString(StringBuilder sb) {
|
||||||
|
String separator = " columns ";
|
||||||
|
for ( ColumnDefinition columnDefinition : columnDefinitions ) {
|
||||||
|
sb.append( separator );
|
||||||
|
columnDefinition.appendHqlString( sb );
|
||||||
|
separator = ", ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable SqmExpressible<Object> getNodeType() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NodeBuilder nodeBuilder() {
|
||||||
|
return table.nodeBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <X> X accept(SemanticQueryWalker<X> walker) {
|
||||||
|
for ( ColumnDefinition columnDefinition : columnDefinitions ) {
|
||||||
|
if ( columnDefinition instanceof SqmXmlTableFunction.ValueColumnDefinition<?> definition ) {
|
||||||
|
if ( definition.defaultExpression != null ) {
|
||||||
|
definition.defaultExpression.accept( walker );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ( columnDefinition instanceof SqmXmlTableFunction.QueryColumnDefinition definition ) {
|
||||||
|
if ( definition.defaultExpression != null ) {
|
||||||
|
definition.defaultExpression.accept( walker );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No-op since this object is going to be visible as function argument
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5592,7 +5592,7 @@ protected void renderRowNumber(SelectClause selectClause, QueryPart queryPart) {
|
|||||||
visitOverClause( Collections.emptyList(), getSortSpecificationsRowNumbering( selectClause, queryPart ) );
|
visitOverClause( Collections.emptyList(), getSortSpecificationsRowNumbering( selectClause, queryPart ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final boolean isParameter(Expression expression) {
|
public static final boolean isParameter(Expression expression) {
|
||||||
return expression instanceof JdbcParameter || expression instanceof SqmParameterInterpretation;
|
return expression instanceof JdbcParameter || expression instanceof SqmParameterInterpretation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
* Copyright Red Hat Inc. and Hibernate Authors
|
||||||
|
*/
|
||||||
|
package org.hibernate.sql.ast.tree.expression;
|
||||||
|
|
||||||
|
import org.hibernate.sql.ast.SqlAstWalker;
|
||||||
|
import org.hibernate.sql.ast.tree.SqlAstNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
public sealed interface XmlTableColumnDefinition extends SqlAstNode
|
||||||
|
permits XmlTableOrdinalityColumnDefinition, XmlTableQueryColumnDefinition, XmlTableValueColumnDefinition {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default void accept(SqlAstWalker sqlTreeWalker) {
|
||||||
|
throw new UnsupportedOperationException("XmlTableColumnDefinition doesn't support walking");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
* Copyright Red Hat Inc. and Hibernate Authors
|
||||||
|
*/
|
||||||
|
package org.hibernate.sql.ast.tree.expression;
|
||||||
|
|
||||||
|
import org.hibernate.sql.ast.SqlAstWalker;
|
||||||
|
import org.hibernate.sql.ast.tree.SqlAstNode;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
public class XmlTableColumnsClause implements SqlAstNode {
|
||||||
|
|
||||||
|
private final List<XmlTableColumnDefinition> columnDefinitions;
|
||||||
|
|
||||||
|
public XmlTableColumnsClause(List<XmlTableColumnDefinition> columnDefinitions) {
|
||||||
|
this.columnDefinitions = columnDefinitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<XmlTableColumnDefinition> getColumnDefinitions() {
|
||||||
|
return columnDefinitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(SqlAstWalker sqlTreeWalker) {
|
||||||
|
throw new UnsupportedOperationException("XmlTableColumnsClause doesn't support walking");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
* Copyright Red Hat Inc. and Hibernate Authors
|
||||||
|
*/
|
||||||
|
package org.hibernate.sql.ast.tree.expression;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
public record XmlTableOrdinalityColumnDefinition(
|
||||||
|
String name
|
||||||
|
) implements XmlTableColumnDefinition {
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
* Copyright Red Hat Inc. and Hibernate Authors
|
||||||
|
*/
|
||||||
|
package org.hibernate.sql.ast.tree.expression;
|
||||||
|
|
||||||
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
import org.hibernate.type.BasicType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
public record XmlTableQueryColumnDefinition(
|
||||||
|
String name,
|
||||||
|
BasicType<String> type,
|
||||||
|
@Nullable String xpath,
|
||||||
|
@Nullable Expression defaultExpression
|
||||||
|
) implements XmlTableColumnDefinition {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
* Copyright Red Hat Inc. and Hibernate Authors
|
||||||
|
*/
|
||||||
|
package org.hibernate.sql.ast.tree.expression;
|
||||||
|
|
||||||
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
public record XmlTableValueColumnDefinition(
|
||||||
|
String name,
|
||||||
|
CastTarget type,
|
||||||
|
@Nullable String xpath,
|
||||||
|
@Nullable Expression defaultExpression
|
||||||
|
) implements XmlTableColumnDefinition {
|
||||||
|
|
||||||
|
}
|
@ -57,6 +57,14 @@ protected <X> X fromString(String string, JavaType<X> javaType, WrapperOptions o
|
|||||||
if ( string == null ) {
|
if ( string == null ) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if ( javaType.getJavaType() == SQLXML.class ) {
|
||||||
|
SQLXML sqlxml = options.getSession().getJdbcCoordinator().getLogicalConnection()
|
||||||
|
.getPhysicalConnection()
|
||||||
|
.createSQLXML();
|
||||||
|
sqlxml.setString( string );
|
||||||
|
//noinspection unchecked
|
||||||
|
return (X) sqlxml;
|
||||||
|
}
|
||||||
return options.getSessionFactory().getFastSessionServices().getXmlFormatMapper().fromString(
|
return options.getSessionFactory().getFastSessionServices().getXmlFormatMapper().fromString(
|
||||||
string,
|
string,
|
||||||
javaType,
|
javaType,
|
||||||
|
@ -97,6 +97,14 @@ protected <X> X fromString(String string, JavaType<X> javaType, WrapperOptions o
|
|||||||
options
|
options
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if ( javaType.getJavaType() == SQLXML.class ) {
|
||||||
|
SQLXML sqlxml = options.getSession().getJdbcCoordinator().getLogicalConnection()
|
||||||
|
.getPhysicalConnection()
|
||||||
|
.createSQLXML();
|
||||||
|
sqlxml.setString( string );
|
||||||
|
//noinspection unchecked
|
||||||
|
return (X) sqlxml;
|
||||||
|
}
|
||||||
return options.getSessionFactory().getFastSessionServices().getXmlFormatMapper().fromString(
|
return options.getSessionFactory().getFastSessionServices().getXmlFormatMapper().fromString(
|
||||||
string,
|
string,
|
||||||
javaType,
|
javaType,
|
||||||
|
@ -0,0 +1,317 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
* Copyright Red Hat Inc. and Hibernate Authors
|
||||||
|
*/
|
||||||
|
package org.hibernate.orm.test.function.xml;
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Tuple;
|
||||||
|
import org.hibernate.annotations.JdbcTypeCode;
|
||||||
|
import org.hibernate.cfg.QuerySettings;
|
||||||
|
import org.hibernate.dialect.HANADialect;
|
||||||
|
import org.hibernate.dialect.OracleDialect;
|
||||||
|
import org.hibernate.dialect.SybaseASEDialect;
|
||||||
|
import org.hibernate.query.criteria.JpaFunctionRoot;
|
||||||
|
import org.hibernate.query.sqm.NodeBuilder;
|
||||||
|
import org.hibernate.query.sqm.tree.expression.SqmXmlTableFunction;
|
||||||
|
import org.hibernate.query.sqm.tree.select.SqmSelectStatement;
|
||||||
|
import org.hibernate.testing.orm.junit.DialectContext;
|
||||||
|
import org.hibernate.testing.orm.junit.DialectFeatureChecks;
|
||||||
|
import org.hibernate.testing.orm.junit.DomainModel;
|
||||||
|
import org.hibernate.testing.orm.junit.RequiresDialectFeature;
|
||||||
|
import org.hibernate.testing.orm.junit.ServiceRegistry;
|
||||||
|
import org.hibernate.testing.orm.junit.SessionFactory;
|
||||||
|
import org.hibernate.testing.orm.junit.SessionFactoryScope;
|
||||||
|
import org.hibernate.testing.orm.junit.Setting;
|
||||||
|
import org.hibernate.testing.orm.junit.SkipForDialect;
|
||||||
|
import org.hibernate.type.SqlTypes;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
import org.xml.sax.InputSource;
|
||||||
|
import org.xml.sax.SAXException;
|
||||||
|
|
||||||
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
|
import javax.xml.transform.OutputKeys;
|
||||||
|
import javax.xml.transform.Transformer;
|
||||||
|
import javax.xml.transform.TransformerException;
|
||||||
|
import javax.xml.transform.TransformerFactory;
|
||||||
|
import javax.xml.transform.dom.DOMSource;
|
||||||
|
import javax.xml.transform.stream.StreamResult;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Christian Beikov
|
||||||
|
*/
|
||||||
|
@DomainModel(annotatedClasses = XmlTableTest.XmlHolder.class)
|
||||||
|
@SessionFactory
|
||||||
|
@ServiceRegistry(settings = @Setting(name = QuerySettings.XML_FUNCTIONS_ENABLED, value = "true"))
|
||||||
|
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsXmlTable.class)
|
||||||
|
public class XmlTableTest {
|
||||||
|
|
||||||
|
private static final String XML = """
|
||||||
|
<root>
|
||||||
|
<elem>
|
||||||
|
<theInt>1</theInt>
|
||||||
|
<theFloat>0.1</theFloat>
|
||||||
|
<theString>abc</theString>
|
||||||
|
<theBoolean>true</theBoolean>
|
||||||
|
<theNull/>
|
||||||
|
<theObject>
|
||||||
|
<nested>Abc</nested>
|
||||||
|
</theObject>
|
||||||
|
</elem>
|
||||||
|
</root>
|
||||||
|
""";
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void prepareData(SessionFactoryScope scope) {
|
||||||
|
scope.inTransaction(
|
||||||
|
em -> {
|
||||||
|
XmlHolder entity = new XmlHolder();
|
||||||
|
entity.id = 1L;
|
||||||
|
entity.xml = new HashMap<>();
|
||||||
|
entity.xml.put( "theInt", 1 );
|
||||||
|
entity.xml.put( "theFloat", 0.1 );
|
||||||
|
entity.xml.put( "theString", "abc" );
|
||||||
|
entity.xml.put( "theBoolean", true );
|
||||||
|
entity.xml.put( "theNull", null );
|
||||||
|
entity.xml.put( "theArray", new String[] { "a", "b", "c" } );
|
||||||
|
entity.xml.put( "theObject", new HashMap<>( entity.xml ) );
|
||||||
|
entity.xml.put(
|
||||||
|
"theNestedObjects",
|
||||||
|
List.of(
|
||||||
|
Map.of( "id", 1, "name", "val1" ),
|
||||||
|
Map.of( "id", 2, "name", "val2" ),
|
||||||
|
Map.of( "id", 3, "name", "val3" )
|
||||||
|
)
|
||||||
|
);
|
||||||
|
em.persist(entity);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void cleanupData(SessionFactoryScope scope) {
|
||||||
|
scope.inTransaction(
|
||||||
|
em -> {
|
||||||
|
em.createMutationQuery( "delete from XmlHolder" ).executeUpdate();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSimple(SessionFactoryScope scope) {
|
||||||
|
scope.inSession( em -> {
|
||||||
|
//tag::hql-xml-table-example[]
|
||||||
|
final String query = """
|
||||||
|
select
|
||||||
|
t.theInt,
|
||||||
|
t.theFloat,
|
||||||
|
t.theString,
|
||||||
|
t.theBoolean,
|
||||||
|
t.theNull,
|
||||||
|
t.theObject,
|
||||||
|
t.theNestedString,
|
||||||
|
t.nonExisting,
|
||||||
|
t.nonExistingWithDefault
|
||||||
|
from xmltable('/root/elem' passing :xml columns
|
||||||
|
theInt Integer,
|
||||||
|
theFloat Float,
|
||||||
|
theString String,
|
||||||
|
theBoolean Boolean,
|
||||||
|
theNull String,
|
||||||
|
theObject XML,
|
||||||
|
theNestedString String path 'theObject/nested',
|
||||||
|
nonExisting String,
|
||||||
|
nonExistingWithDefault String default 'none'
|
||||||
|
) t
|
||||||
|
"""
|
||||||
|
//end::hql-xml-table-example[]
|
||||||
|
.replace( ":xml", "'" + XML + "'" );
|
||||||
|
//tag::hql-xml-table-example[]
|
||||||
|
List<Tuple> resultList = em.createQuery( query, Tuple.class )
|
||||||
|
.getResultList();
|
||||||
|
//end::hql-xml-table-example[]
|
||||||
|
|
||||||
|
assertEquals( 1, resultList.size() );
|
||||||
|
|
||||||
|
assertTupleEquals( resultList.get( 0 ) );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SkipForDialect(dialectClass = SybaseASEDialect.class, reason = "Sybase ASE needs a special emulation for query columns that is impossible with parameters")
|
||||||
|
public void testNodeBuilderXmlTableObject(SessionFactoryScope scope) {
|
||||||
|
scope.inSession( em -> {
|
||||||
|
final NodeBuilder cb = (NodeBuilder) em.getCriteriaBuilder();
|
||||||
|
final SqmSelectStatement<Tuple> cq = cb.createTupleQuery();
|
||||||
|
final SqmXmlTableFunction<?> xmlTable = cb.xmlTable( "/root/elem", cb.value( XML ) );
|
||||||
|
|
||||||
|
xmlTable.valueColumn( "theInt", Integer.class );
|
||||||
|
xmlTable.valueColumn( "theFloat", Float.class );
|
||||||
|
xmlTable.valueColumn( "theString", String.class );
|
||||||
|
xmlTable.valueColumn( "theBoolean", Boolean.class );
|
||||||
|
xmlTable.valueColumn( "theNull", String.class );
|
||||||
|
xmlTable.queryColumn( "theObject" );
|
||||||
|
xmlTable.valueColumn( "theNestedString", String.class, "theObject/nested" );
|
||||||
|
xmlTable.valueColumn( "nonExisting", String.class );
|
||||||
|
xmlTable.valueColumn( "nonExistingWithDefault", String.class ).defaultValue( "none" );
|
||||||
|
|
||||||
|
final JpaFunctionRoot<?> root = cq.from( xmlTable );
|
||||||
|
cq.multiselect(
|
||||||
|
root.get( "theInt" ),
|
||||||
|
root.get( "theFloat" ),
|
||||||
|
root.get( "theString" ),
|
||||||
|
root.get( "theBoolean" ),
|
||||||
|
root.get( "theNull" ),
|
||||||
|
root.get( "theObject" ),
|
||||||
|
root.get( "theNestedString" ),
|
||||||
|
root.get( "nonExisting" ),
|
||||||
|
root.get( "nonExistingWithDefault" )
|
||||||
|
);
|
||||||
|
List<Tuple> resultList = em.createQuery( cq ).getResultList();
|
||||||
|
|
||||||
|
assertEquals( 1, resultList.size() );
|
||||||
|
|
||||||
|
assertTupleEquals( resultList.get( 0 ) );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCorrelateXmlTable(SessionFactoryScope scope) {
|
||||||
|
scope.inSession( em -> {
|
||||||
|
final String query = """
|
||||||
|
select
|
||||||
|
t.theInt,
|
||||||
|
t.theFloat,
|
||||||
|
t.theString,
|
||||||
|
t.theBoolean
|
||||||
|
from XmlHolder e join lateral xmltable('/Map' passing e.xml columns
|
||||||
|
theInt Integer,
|
||||||
|
theFloat Float,
|
||||||
|
theString String,
|
||||||
|
theBoolean Boolean
|
||||||
|
) t
|
||||||
|
""";
|
||||||
|
List<Tuple> resultList = em.createQuery( query, Tuple.class ).getResultList();
|
||||||
|
|
||||||
|
assertEquals( 1, resultList.size() );
|
||||||
|
|
||||||
|
Tuple tuple = resultList.get( 0 );
|
||||||
|
assertEquals( 1, tuple.get( 0 ) );
|
||||||
|
assertEquals( 0.1F, tuple.get( 1 ) );
|
||||||
|
assertEquals( "abc", tuple.get( 2 ) );
|
||||||
|
assertEquals( true, tuple.get( 3 ) );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertTupleEquals(Tuple tuple) {
|
||||||
|
assertEquals( 1, tuple.get( 0 ) );
|
||||||
|
assertEquals( 0.1F, tuple.get( 1 ) );
|
||||||
|
assertEquals( "abc", tuple.get( 2 ) );
|
||||||
|
assertEquals( true, tuple.get( 3 ) );
|
||||||
|
if ( DialectContext.getDialect() instanceof OracleDialect
|
||||||
|
|| DialectContext.getDialect() instanceof HANADialect
|
||||||
|
|| DialectContext.getDialect() instanceof SybaseASEDialect ) {
|
||||||
|
// Some databases return null for empty tags rather than an empty string
|
||||||
|
assertNull( tuple.get( 4 ) );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Other DBs returns an empty string for an empty tag
|
||||||
|
assertEquals( "", tuple.get( 4 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
assertXmlEquals("<theObject><nested>Abc</nested></theObject>", tuple.get( 5, String.class ) );
|
||||||
|
|
||||||
|
assertEquals( "Abc", tuple.get( 6 ) );
|
||||||
|
assertNull( tuple.get( 7 ) );
|
||||||
|
assertEquals( "none", tuple.get( 8 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertXmlEquals(String expected, String actual) {
|
||||||
|
final Document expectedDoc = parseXml( xmlNormalize( expected ) );
|
||||||
|
final Document actualDoc = parseXml( xmlNormalize( actual ) );
|
||||||
|
normalize( expectedDoc );
|
||||||
|
normalize( actualDoc );
|
||||||
|
assertEquals( toXml( expectedDoc ).trim(), toXml( actualDoc ).trim() );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void normalize(Document document) {
|
||||||
|
normalize( document.getChildNodes() );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void normalize(NodeList childNodes) {
|
||||||
|
for ( int i = 0; i < childNodes.getLength(); i++ ) {
|
||||||
|
final Node childNode = childNodes.item( i );
|
||||||
|
if ( childNode.getNodeType() == Node.ELEMENT_NODE ) {
|
||||||
|
normalize( childNode.getChildNodes() );
|
||||||
|
}
|
||||||
|
else if ( childNode.getNodeType() == Node.TEXT_NODE ) {
|
||||||
|
if ( childNode.getNodeValue().isBlank() ) {
|
||||||
|
childNode.getParentNode().removeChild( childNode );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
childNode.setNodeValue( childNode.getNodeValue().trim() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ( childNode.getNodeType() == Node.COMMENT_NODE ) {
|
||||||
|
childNode.setNodeValue( childNode.getNodeValue().trim() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String xmlNormalize(String doc) {
|
||||||
|
final String prefix = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
|
||||||
|
return doc.startsWith( "<?xml" ) ? doc : prefix + doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Document parseXml(String document) {
|
||||||
|
final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||||
|
try {
|
||||||
|
final DocumentBuilder db = dbf.newDocumentBuilder();
|
||||||
|
return db.parse( new InputSource( new StringReader( document ) ) );
|
||||||
|
}
|
||||||
|
catch (ParserConfigurationException | IOException | SAXException e) {
|
||||||
|
throw new RuntimeException( e );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String toXml(Document document) {
|
||||||
|
final TransformerFactory tf = TransformerFactory.newInstance();
|
||||||
|
try {
|
||||||
|
final Transformer transformer = tf.newTransformer();
|
||||||
|
transformer.setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, "yes");
|
||||||
|
transformer.setOutputProperty( OutputKeys.INDENT, "yes" );
|
||||||
|
final StringWriter writer = new StringWriter();
|
||||||
|
transformer.transform( new DOMSource( document ), new StreamResult( writer ) );
|
||||||
|
return writer.toString();
|
||||||
|
}
|
||||||
|
catch (TransformerException e) {
|
||||||
|
throw new RuntimeException( e );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity(name = "XmlHolder")
|
||||||
|
public static class XmlHolder {
|
||||||
|
@Id
|
||||||
|
Long id;
|
||||||
|
@JdbcTypeCode(SqlTypes.SQLXML)
|
||||||
|
Map<String, Object> xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -847,6 +847,12 @@ public boolean apply(Dialect dialect) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class SupportsXmlTable implements DialectFeatureCheck {
|
||||||
|
public boolean apply(Dialect dialect) {
|
||||||
|
return definesSetReturningFunction( dialect, "xmltable" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static class SupportsArrayAgg implements DialectFeatureCheck {
|
public static class SupportsArrayAgg implements DialectFeatureCheck {
|
||||||
public boolean apply(Dialect dialect) {
|
public boolean apply(Dialect dialect) {
|
||||||
return definesFunction( dialect, "array_agg" );
|
return definesFunction( dialect, "array_agg" );
|
||||||
|
@ -88,6 +88,7 @@ Out-of-the-box, some common set-returning functions are already supported or emu
|
|||||||
* `unnest()` - allows to turn an array into rows
|
* `unnest()` - allows to turn an array into rows
|
||||||
* `generate_series()` - can be used to create a series of values as rows
|
* `generate_series()` - can be used to create a series of values as rows
|
||||||
* `json_table()` - turns a JSON document into rows
|
* `json_table()` - turns a JSON document into rows
|
||||||
|
* `xmltable()` - turns an XML document into rows
|
||||||
|
|
||||||
[[cleanup]]
|
[[cleanup]]
|
||||||
== Clean-up
|
== Clean-up
|
||||||
|
Loading…
x
Reference in New Issue
Block a user