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
|
||||
|
||||
| `xmlelement()` | Constructs an XML element from arguments
|
||||
| `xmlcomment()` | Constructs an XML comment from the single argument
|
||||
| `xmlforest()` | Constructs an XML forest from the arguments
|
||||
| `xmlconcat()` | Concatenates multiple XML fragments to each other
|
||||
| `xmlpi()` | Constructs an XML processing instruction
|
||||
| `xmlquery()` | Extracts content from XML document using XQuery or XPath
|
||||
| `xmlexists()` | Checks if an XQuery or XPath expression exists in an XML document
|
||||
| `xmlagg()` | Aggregates XML elements by concatenation
|
||||
| <<hql-xmlelement-function,`xmlelement()`>> | Constructs an XML element from arguments
|
||||
| <<hql-xmlcomment-function,`xmlcomment()`>> | Constructs an XML comment from the single argument
|
||||
| <<hql-xmlforest-function,`xmlforest()`>> | Constructs an XML forest from the arguments
|
||||
| <<hql-xmlconcat-function,`xmlconcat()`>> | Concatenates multiple XML fragments to each other
|
||||
| <<hql-xmlpi-function,`xmlpi()`>> | Constructs an XML processing instruction
|
||||
| <<hql-xmlquery-function,`xmlquery()`>> | Extracts content from XML document using XQuery or XPath
|
||||
| <<hql-xmlexists-function,`xmlexists()`>> | Checks if an XQuery or XPath expression exists in an XML document
|
||||
| <<hql-xmlagg-function,`xmlagg()`>> | Aggregates XML elements by concatenation
|
||||
| <<hql-xmltable-function,`xmltable()`>> | Turns an XML document into rows
|
||||
|===
|
||||
|
||||
|
||||
|
@ -2461,6 +2462,39 @@ include::{xml-example-dir-hql}/XmlAggTest.java[tags=hql-xmlagg-example]
|
|||
|
||||
WARNING: SAP HANA, MySQL, MariaDB and HSQLDB do not support this function.
|
||||
|
||||
[[hql-xmltable-function]]
|
||||
===== `xmltable()`
|
||||
|
||||
A <<hql-from-set-returning-functions,set-returning function>>, which turns an XML document argument into rows.
|
||||
Returns no rows if the document is `null` or the XPath expression resolves to no nodes.
|
||||
|
||||
[[hql-xmltable-bnf]]
|
||||
[source, antlrv4, indent=0]
|
||||
----
|
||||
include::{extrasdir}/xmltable_bnf.txt[]
|
||||
----
|
||||
|
||||
The first argument is the XPath expression. The second argument represents the XML document expression.
|
||||
|
||||
Columns that ought to be accessible via the `from` node alias are defined in the `columns` clause,
|
||||
which can be of varying forms:
|
||||
|
||||
* Value attributes - denoted by a `castTarget` after the name, will cast the content of the XML node matching the XPath expression of the column
|
||||
* Query attributes - denoted by the `xml` type after the name, returns the XML node matching the XPath expression of the column
|
||||
* Ordinal attributes - denoted by the `for ordinality` syntax after the name, gives access to the 1-based index of the currently processed XML node
|
||||
|
||||
[[hql-xmltable-simple-example]]
|
||||
====
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{xml-example-dir-hql}/XmlTableTest.java[tags=hql-xml-table-example]
|
||||
----
|
||||
====
|
||||
|
||||
The `lateral` keyword is mandatory if one of the arguments refer to a from node item of the same query level.
|
||||
|
||||
WARNING: H2, MySQL, MariaDB and HSQLDB do not support this function.
|
||||
|
||||
[[hql-user-defined-functions]]
|
||||
==== Native and user-defined functions
|
||||
|
||||
|
@ -3001,7 +3035,8 @@ The following set-returning functions are available on many platforms:
|
|||
|
||||
| <<hql-array-unnest,`unnest()`>> | Turns an array into rows
|
||||
| <<hql-from-set-returning-functions-generate-series,`generate_series()`>> | Creates a series of values as rows
|
||||
| <<hql-json-table,`json_table()`>> | Turns a JSON document into rows
|
||||
| <<hql-json-table-function,`json_table()`>> | Turns a JSON document into rows
|
||||
| <<hql-xmltable-function,`xmltable()`>> | Turns an XML document into rows
|
||||
|===
|
||||
|
||||
To use set returning functions defined in the database, it is required to register them in a `FunctionContributor`:
|
||||
|
|
|
@ -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 class DB2LegacyDialect extends Dialect {
|
|||
functionFactory.xmlexists_db2_legacy();
|
||||
}
|
||||
functionFactory.xmlagg();
|
||||
functionFactory.xmltable_db2();
|
||||
|
||||
functionFactory.unnest_emulated();
|
||||
if ( supportsRecursiveCTE() ) {
|
||||
|
|
|
@ -508,7 +508,7 @@ public class HANALegacyDialect extends Dialect {
|
|||
functionFactory.jsonObjectAgg_hana();
|
||||
}
|
||||
|
||||
// functionFactory.xmltable();
|
||||
functionFactory.xmltable_hana();
|
||||
}
|
||||
|
||||
// functionFactory.xmlextract();
|
||||
|
|
|
@ -334,6 +334,7 @@ public class OracleLegacyDialect extends Dialect {
|
|||
functionFactory.xmlquery_oracle();
|
||||
functionFactory.xmlexists();
|
||||
functionFactory.xmlagg();
|
||||
functionFactory.xmltable_oracle();
|
||||
|
||||
functionFactory.unnest_oracle();
|
||||
functionFactory.generateSeries_recursive( getMaximumSeriesSize(), true, false );
|
||||
|
|
|
@ -678,6 +678,7 @@ public class PostgreSQLLegacyDialect extends Dialect {
|
|||
functionFactory.xmlquery_postgresql();
|
||||
functionFactory.xmlexists();
|
||||
functionFactory.xmlagg();
|
||||
functionFactory.xmltable( true );
|
||||
|
||||
if ( getVersion().isSameOrAfter( 9, 4 ) ) {
|
||||
functionFactory.makeDateTimeTimestamp();
|
||||
|
|
|
@ -429,6 +429,7 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect {
|
|||
functionFactory.xmlquery_sqlserver();
|
||||
functionFactory.xmlexists_sqlserver();
|
||||
functionFactory.xmlagg_sqlserver();
|
||||
functionFactory.xmltable_sqlserver();
|
||||
|
||||
functionFactory.unnest_sqlserver();
|
||||
|
||||
|
|
|
@ -167,6 +167,7 @@ public class SybaseASELegacyDialect extends SybaseLegacyDialect {
|
|||
|
||||
functionFactory.unnest_sybasease();
|
||||
functionFactory.generateSeries_sybasease( getMaximumSeriesSize() );
|
||||
functionFactory.xmltable_sybasease();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -336,6 +336,7 @@ WITH : [wW] [iI] [tT] [hH];
|
|||
WITHIN : [wW] [iI] [tT] [hH] [iI] [nN];
|
||||
WITHOUT : [wW] [iI] [tT] [hH] [oO] [uU] [tT];
|
||||
WRAPPER : [wW] [rR] [aA] [pP] [pP] [eE] [rR];
|
||||
XML : [xX] [mM] [lL];
|
||||
XMLAGG : [xX] [mM] [lL] [aA] [gG] [gG];
|
||||
XMLATTRIBUTES : [xX] [mM] [lL] [aA] [tT] [tT] [rR] [iI] [bB] [uU] [tT] [eE] [sS];
|
||||
XMLELEMENT : [xX] [mM] [lL] [eE] [lL] [eE] [mM] [eE] [nN] [tT];
|
||||
|
@ -343,6 +344,7 @@ XMLEXISTS : [xX] [mM] [lL] [eE] [xX] [iI] [sS] [tT] [sS];
|
|||
XMLFOREST : [xX] [mM] [lL] [fF] [oO] [rR] [eE] [sS] [tT];
|
||||
XMLPI : [xX] [mM] [lL] [pP] [iI];
|
||||
XMLQUERY : [xX] [mM] [lL] [qQ] [uU] [eE] [rR] [yY];
|
||||
XMLTABLE : [xX] [mM] [lL] [tT] [aA] [bB] [lL] [eE];
|
||||
YEAR : [yY] [eE] [aA] [rR];
|
||||
ZONED : [zZ] [oO] [nN] [eE] [dD];
|
||||
|
||||
|
|
|
@ -1119,6 +1119,7 @@ function
|
|||
setReturningFunction
|
||||
: simpleSetReturningFunction
|
||||
| jsonTableFunction
|
||||
| xmltableFunction
|
||||
;
|
||||
|
||||
/**
|
||||
|
@ -1813,6 +1814,24 @@ xmlaggFunction
|
|||
: XMLAGG LEFT_PAREN expression orderByClause? RIGHT_PAREN filterClause? overClause?
|
||||
;
|
||||
|
||||
xmltableFunction
|
||||
: XMLTABLE LEFT_PAREN expression PASSING expression xmltableColumnsClause RIGHT_PAREN
|
||||
;
|
||||
|
||||
xmltableColumnsClause
|
||||
: COLUMNS xmltableColumn (COMMA xmltableColumn)*
|
||||
;
|
||||
|
||||
xmltableColumn
|
||||
: identifier XML (PATH STRING_LITERAL)? xmltableDefaultClause? # XmlTableQueryColumn
|
||||
| identifier FOR ORDINALITY # XmlTableOrdinalityColumn
|
||||
| identifier castTarget (PATH STRING_LITERAL)? xmltableDefaultClause? # XmlTableValueColumn
|
||||
;
|
||||
|
||||
xmltableDefaultClause
|
||||
: DEFAULT expression
|
||||
;
|
||||
|
||||
/**
|
||||
* Support for "soft" keywords which may be used as identifiers
|
||||
*
|
||||
|
@ -2025,6 +2044,7 @@ xmlaggFunction
|
|||
| WITHIN
|
||||
| WITHOUT
|
||||
| WRAPPER
|
||||
| XML
|
||||
| XMLAGG
|
||||
| XMLATTRIBUTES
|
||||
| XMLELEMENT
|
||||
|
@ -2032,6 +2052,7 @@ xmlaggFunction
|
|||
| XMLFOREST
|
||||
| XMLPI
|
||||
| XMLQUERY
|
||||
| XMLTABLE
|
||||
| YEAR
|
||||
| ZONED) {
|
||||
logUseOfReservedWordAsIdentifier( getCurrentToken() );
|
||||
|
|
|
@ -442,6 +442,7 @@ public class DB2Dialect extends Dialect {
|
|||
functionFactory.xmlexists_db2_legacy();
|
||||
}
|
||||
functionFactory.xmlagg();
|
||||
functionFactory.xmltable_db2();
|
||||
|
||||
functionFactory.unnest_emulated();
|
||||
functionFactory.generateSeries_recursive( getMaximumSeriesSize(), false, true );
|
||||
|
|
|
@ -511,7 +511,7 @@ public class HANADialect extends Dialect {
|
|||
functionFactory.jsonArrayAgg_hana();
|
||||
functionFactory.jsonObjectAgg_hana();
|
||||
|
||||
// functionFactory.xmltable();
|
||||
functionFactory.xmltable_hana();
|
||||
|
||||
// functionFactory.xmlextract();
|
||||
functionFactory.generateSeries_hana( getMaximumSeriesSize() );
|
||||
|
|
|
@ -421,6 +421,7 @@ public class OracleDialect extends Dialect {
|
|||
functionFactory.xmlquery_oracle();
|
||||
functionFactory.xmlexists();
|
||||
functionFactory.xmlagg();
|
||||
functionFactory.xmltable_oracle();
|
||||
|
||||
functionFactory.unnest_oracle();
|
||||
functionFactory.generateSeries_recursive( getMaximumSeriesSize(), true, false );
|
||||
|
|
|
@ -640,6 +640,7 @@ public class PostgreSQLDialect extends Dialect {
|
|||
functionFactory.xmlquery_postgresql();
|
||||
functionFactory.xmlexists();
|
||||
functionFactory.xmlagg();
|
||||
functionFactory.xmltable( true );
|
||||
|
||||
functionFactory.makeDateTimeTimestamp();
|
||||
// Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions
|
||||
|
|
|
@ -446,6 +446,7 @@ public class SQLServerDialect extends AbstractTransactSQLDialect {
|
|||
functionFactory.xmlquery_sqlserver();
|
||||
functionFactory.xmlexists_sqlserver();
|
||||
functionFactory.xmlagg_sqlserver();
|
||||
functionFactory.xmltable_sqlserver();
|
||||
|
||||
functionFactory.unnest_sqlserver();
|
||||
|
||||
|
|
|
@ -184,6 +184,7 @@ public class SybaseASEDialect extends SybaseDialect {
|
|||
|
||||
functionFactory.unnest_sybasease();
|
||||
functionFactory.generateSeries_sybasease( getMaximumSeriesSize() );
|
||||
functionFactory.xmltable_sybasease();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,12 +13,15 @@ import org.hibernate.dialect.Dialect;
|
|||
|
||||
import org.hibernate.dialect.function.array.*;
|
||||
import org.hibernate.dialect.function.json.*;
|
||||
import org.hibernate.dialect.function.xml.DB2XmlTableFunction;
|
||||
import org.hibernate.dialect.function.xml.H2XmlConcatFunction;
|
||||
import org.hibernate.dialect.function.xml.H2XmlElementFunction;
|
||||
import org.hibernate.dialect.function.xml.H2XmlForestFunction;
|
||||
import org.hibernate.dialect.function.xml.H2XmlPiFunction;
|
||||
import org.hibernate.dialect.function.xml.HANAXmlTableFunction;
|
||||
import org.hibernate.dialect.function.xml.LegacyDB2XmlExistsFunction;
|
||||
import org.hibernate.dialect.function.xml.LegacyDB2XmlQueryFunction;
|
||||
import org.hibernate.dialect.function.xml.OracleXmlTableFunction;
|
||||
import org.hibernate.dialect.function.xml.PostgreSQLXmlQueryFunction;
|
||||
import org.hibernate.dialect.function.xml.SQLServerXmlAggFunction;
|
||||
import org.hibernate.dialect.function.xml.SQLServerXmlConcatFunction;
|
||||
|
@ -27,6 +30,8 @@ import org.hibernate.dialect.function.xml.SQLServerXmlExistsFunction;
|
|||
import org.hibernate.dialect.function.xml.SQLServerXmlForestFunction;
|
||||
import org.hibernate.dialect.function.xml.SQLServerXmlPiFunction;
|
||||
import org.hibernate.dialect.function.xml.SQLServerXmlQueryFunction;
|
||||
import org.hibernate.dialect.function.xml.SQLServerXmlTableFunction;
|
||||
import org.hibernate.dialect.function.xml.SybaseASEXmlTableFunction;
|
||||
import org.hibernate.dialect.function.xml.XmlAggFunction;
|
||||
import org.hibernate.dialect.function.xml.XmlConcatFunction;
|
||||
import org.hibernate.dialect.function.xml.XmlElementFunction;
|
||||
|
@ -34,6 +39,7 @@ import org.hibernate.dialect.function.xml.XmlExistsFunction;
|
|||
import org.hibernate.dialect.function.xml.XmlForestFunction;
|
||||
import org.hibernate.dialect.function.xml.XmlPiFunction;
|
||||
import org.hibernate.dialect.function.xml.XmlQueryFunction;
|
||||
import org.hibernate.dialect.function.xml.XmlTableFunction;
|
||||
import org.hibernate.query.sqm.function.SqmFunctionRegistry;
|
||||
import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator;
|
||||
import org.hibernate.query.sqm.produce.function.FunctionParameterType;
|
||||
|
@ -4323,4 +4329,46 @@ public class CommonFunctionFactory {
|
|||
public void jsonTable_h2(int maximumArraySize) {
|
||||
functionRegistry.register( "json_table", new H2JsonTableFunction( maximumArraySize, typeConfiguration ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard xmltable() function
|
||||
*/
|
||||
public void xmltable(boolean supportsParametersInDefault) {
|
||||
functionRegistry.register( "xmltable", new XmlTableFunction( supportsParametersInDefault, typeConfiguration ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Oracle xmltable() function
|
||||
*/
|
||||
public void xmltable_oracle() {
|
||||
functionRegistry.register( "xmltable", new OracleXmlTableFunction( typeConfiguration ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* DB2 xmltable() function
|
||||
*/
|
||||
public void xmltable_db2() {
|
||||
functionRegistry.register( "xmltable", new DB2XmlTableFunction( typeConfiguration ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* HANA xmltable() function
|
||||
*/
|
||||
public void xmltable_hana() {
|
||||
functionRegistry.register( "xmltable", new HANAXmlTableFunction( typeConfiguration ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL Server xmltable() function
|
||||
*/
|
||||
public void xmltable_sqlserver() {
|
||||
functionRegistry.register( "xmltable", new SQLServerXmlTableFunction( typeConfiguration ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sybase ASE xmltable() function
|
||||
*/
|
||||
public void xmltable_sybasease() {
|
||||
functionRegistry.register( "xmltable", new SybaseASEXmlTableFunction( typeConfiguration ) );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -411,7 +411,7 @@ public class HANAUnnestFunction extends UnnestFunction {
|
|||
sessionFactory
|
||||
);
|
||||
|
||||
// Produce a XML string e.g. <root id="1">...</root>
|
||||
// Produce an XML string e.g. <root id="1">...</root>
|
||||
// which will contain the original XML as well as id column information for correlation
|
||||
sqlAppender.appendSql( "trim('/>' from (select" );
|
||||
char separator = ' ';
|
||||
|
@ -424,8 +424,9 @@ public class HANAUnnestFunction extends UnnestFunction {
|
|||
sqlAppender.appendDoubleQuoteEscapedString( columnInfo.name() );
|
||||
separator = ',';
|
||||
}
|
||||
sqlAppender.appendSql( " from sys.dummy for xml('root'='no','columnstyle'='attribute','rowname'='Strings','format'='no')))||" );
|
||||
sqlAppender.appendSql( "substring(" );
|
||||
sqlAppender.appendSql( " from sys.dummy for xml('root'='no','columnstyle'='attribute','rowname'='" );
|
||||
sqlAppender.appendSql( collectionTags.rootName() );
|
||||
sqlAppender.appendSql( "','format'='no')))||substring(" );
|
||||
argument.accept( walker );
|
||||
sqlAppender.appendSql( ",locate('<" );
|
||||
sqlAppender.appendSql( collectionTags.rootName() );
|
||||
|
|
|
@ -63,7 +63,7 @@ public class OracleJsonValueFunction extends JsonValueFunction {
|
|||
}
|
||||
}
|
||||
|
||||
static boolean isEncodedBoolean(JdbcMapping type) {
|
||||
public static boolean isEncodedBoolean(JdbcMapping type) {
|
||||
return type.getJdbcType().isBoolean() && type.getJdbcType().getDdlTypeCode() != SqlTypes.BOOLEAN;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder {
|
|||
@Incubating
|
||||
JpaJsonTableFunction jsonTable(Expression<?> jsonDocument, Expression<String> jsonPath);
|
||||
|
||||
/**
|
||||
* Creates a {@code xmltable} function expression to generate rows from XML elements.
|
||||
*
|
||||
* @since 7.0
|
||||
* @see JpaSelectCriteria#from(JpaSetReturningFunction)
|
||||
* @see JpaFrom#join(JpaSetReturningFunction)
|
||||
*/
|
||||
@Incubating
|
||||
JpaXmlTableFunction xmlTable(String xpath, Expression<?> xmlDocument);
|
||||
|
||||
/**
|
||||
* Creates a {@code xmltable} function expression to generate rows from XML elements.
|
||||
*
|
||||
* @since 7.0
|
||||
* @see JpaSelectCriteria#from(JpaSetReturningFunction)
|
||||
* @see JpaFrom#join(JpaSetReturningFunction)
|
||||
*/
|
||||
@Incubating
|
||||
JpaXmlTableFunction xmlTable(Expression<String> xpath, Expression<?> xmlDocument);
|
||||
|
||||
@Override
|
||||
JpaPredicate and(List<Predicate> restrictions);
|
||||
|
||||
|
|
|
@ -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 class HibernateCriteriaBuilderDelegate implements HibernateCriteriaBuilde
|
|||
public JpaJsonTableFunction jsonTable(Expression<?> jsonDocument, Expression<String> jsonPath) {
|
||||
return criteriaBuilder.jsonTable( jsonDocument, jsonPath );
|
||||
}
|
||||
|
||||
@Incubating
|
||||
@Override
|
||||
public JpaXmlTableFunction xmlTable(String xpath, Expression<?> xmlDocument) {
|
||||
return criteriaBuilder.xmlTable( xpath, xmlDocument );
|
||||
}
|
||||
|
||||
@Incubating
|
||||
@Override
|
||||
public JpaXmlTableFunction xmlTable(Expression<String> xpath, Expression<?> xmlDocument) {
|
||||
return criteriaBuilder.xmlTable( xpath, xmlDocument );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,6 +69,7 @@ import org.hibernate.query.criteria.JpaJsonTableColumnsNode;
|
|||
import org.hibernate.query.criteria.JpaJsonValueNode;
|
||||
import org.hibernate.query.criteria.JpaRoot;
|
||||
import org.hibernate.query.criteria.JpaSearchOrder;
|
||||
import org.hibernate.query.criteria.JpaXmlTableColumnNode;
|
||||
import org.hibernate.query.derived.AnonymousTupleType;
|
||||
import org.hibernate.query.hql.HqlLogging;
|
||||
import org.hibernate.query.hql.spi.DotIdentifierConsumer;
|
||||
|
@ -3204,6 +3205,64 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
|
|||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object visitXmltableFunction(HqlParser.XmltableFunctionContext ctx) {
|
||||
checkXmlFunctionsEnabled( ctx );
|
||||
final List<HqlParser.ExpressionContext> argumentsContexts = ctx.expression();
|
||||
//noinspection unchecked
|
||||
final SqmExpression<String> xpath = (SqmExpression<String>) argumentsContexts.get( 0 ).accept( this );
|
||||
final SqmExpression<?> document = (SqmExpression<?>) argumentsContexts.get( 1 ).accept( this );
|
||||
final SqmXmlTableFunction<?> xmlTable = creationContext.getNodeBuilder().xmlTable( xpath, document);
|
||||
visitColumns( xmlTable, ctx.xmltableColumnsClause().xmltableColumn() );
|
||||
return xmlTable;
|
||||
}
|
||||
|
||||
private void visitColumns(SqmXmlTableFunction<?> xmlTable, List<HqlParser.XmltableColumnContext> columnContexts) {
|
||||
for ( HqlParser.XmltableColumnContext columnContext : columnContexts ) {
|
||||
if ( columnContext instanceof HqlParser.XmlTableQueryColumnContext queryColumnContext ) {
|
||||
final String columnName = visitIdentifier( queryColumnContext.identifier() );
|
||||
final TerminalNode pathNode = queryColumnContext.STRING_LITERAL();
|
||||
final String xpath;
|
||||
if ( pathNode == null ) {
|
||||
xpath = null;
|
||||
}
|
||||
else {
|
||||
xpath = unquoteStringLiteral( pathNode.getText() );
|
||||
}
|
||||
final JpaXmlTableColumnNode<String> node = xmlTable.queryColumn( columnName, xpath );
|
||||
final HqlParser.XmltableDefaultClauseContext defaultClause = queryColumnContext.xmltableDefaultClause();
|
||||
if ( defaultClause != null ) {
|
||||
//noinspection unchecked
|
||||
node.defaultExpression( (Expression<String>) defaultClause.expression().accept( this ) );
|
||||
}
|
||||
}
|
||||
else if ( columnContext instanceof HqlParser.XmlTableValueColumnContext valueColumnContext ) {
|
||||
final String columnName = visitIdentifier( valueColumnContext.identifier() );
|
||||
//noinspection unchecked
|
||||
final SqmCastTarget<Object> castTarget = (SqmCastTarget<Object>) visitCastTarget( valueColumnContext.castTarget() );
|
||||
final TerminalNode pathNode = valueColumnContext.STRING_LITERAL();
|
||||
final String xpath;
|
||||
if ( pathNode == null ) {
|
||||
xpath = null;
|
||||
}
|
||||
else {
|
||||
xpath = unquoteStringLiteral( pathNode.getText() );
|
||||
}
|
||||
final JpaXmlTableColumnNode<Object> node = xmlTable.valueColumn( columnName, castTarget, xpath );
|
||||
final HqlParser.XmltableDefaultClauseContext defaultClause = valueColumnContext.xmltableDefaultClause();
|
||||
if ( defaultClause != null ) {
|
||||
//noinspection unchecked
|
||||
node.defaultExpression( (Expression<Object>) defaultClause.expression().accept( this ) );
|
||||
}
|
||||
}
|
||||
else {
|
||||
final HqlParser.XmlTableOrdinalityColumnContext ordinalityColumnContext
|
||||
= (HqlParser.XmlTableOrdinalityColumnContext) columnContext;
|
||||
xmlTable.ordinalityColumn( visitIdentifier( ordinalityColumnContext.identifier() ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkXmlFunctionsEnabled(ParserRuleContext ctx) {
|
||||
if ( !creationOptions.isXmlFunctionsEnabled() ) {
|
||||
throw new SemanticException(
|
||||
|
|
|
@ -54,6 +54,7 @@ import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression;
|
|||
import org.hibernate.query.sqm.tree.expression.SqmSetReturningFunction;
|
||||
import org.hibernate.query.sqm.tree.expression.SqmTuple;
|
||||
import org.hibernate.query.sqm.tree.expression.SqmXmlElementExpression;
|
||||
import org.hibernate.query.sqm.tree.expression.SqmXmlTableFunction;
|
||||
import org.hibernate.query.sqm.tree.from.SqmRoot;
|
||||
import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement;
|
||||
import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement;
|
||||
|
@ -885,6 +886,12 @@ public interface NodeBuilder extends HibernateCriteriaBuilder, BindingContext {
|
|||
@Override
|
||||
SqmJsonTableFunction<?> jsonTable(Expression<?> jsonDocument, Expression<String> jsonPath);
|
||||
|
||||
@Override
|
||||
SqmXmlTableFunction<?> xmlTable(String xpath, Expression<?> xmlDocument);
|
||||
|
||||
@Override
|
||||
SqmXmlTableFunction<?> xmlTable(Expression<String> xpath, Expression<?> xmlDocument);
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Covariant overrides
|
||||
|
||||
|
|
|
@ -5956,7 +5956,7 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable {
|
|||
}
|
||||
|
||||
@Override
|
||||
public SqmJsonTableFunction<?> jsonTable(Expression<?> jsonDocument, Expression<String> jsonPath) {
|
||||
public SqmJsonTableFunction<?> jsonTable(Expression<?> jsonDocument, @Nullable Expression<String> jsonPath) {
|
||||
return (SqmJsonTableFunction<?>) getSetReturningFunctionDescriptor( "json_table" ).generateSqmExpression(
|
||||
jsonPath == null
|
||||
? asList( (SqmTypedNode<?>) jsonDocument )
|
||||
|
@ -5964,4 +5964,17 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable {
|
|||
queryEngine
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqmXmlTableFunction<?> xmlTable(String xpath, Expression<?> xmlDocument) {
|
||||
return xmlTable( value( xpath ), xmlDocument );
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqmXmlTableFunction<?> xmlTable(Expression<String> xpath, Expression<?> xmlDocument) {
|
||||
return (SqmXmlTableFunction<?>) getSetReturningFunctionDescriptor( "xmltable" ).generateSqmExpression(
|
||||
asList( (SqmTypedNode<?>) xpath, (SqmTypedNode<?>) xmlDocument ),
|
||||
queryEngine
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@ public final class StandardFunctionArgumentTypeResolvers {
|
|||
return new AbstractFunctionArgumentTypeResolver() {
|
||||
@Override
|
||||
public @Nullable MappingModelExpressible<?> resolveFunctionArgumentType(List<? extends SqmTypedNode<?>> arguments, int argumentIndex, SqmToSqlAstConverter converter) {
|
||||
return expressibles[argumentIndex];
|
||||
return argumentIndex < expressibles.length ? expressibles[argumentIndex] : null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -103,6 +103,9 @@ public final class StandardFunctionArgumentTypeResolvers {
|
|||
return new AbstractFunctionArgumentTypeResolver() {
|
||||
@Override
|
||||
public @Nullable MappingModelExpressible<?> resolveFunctionArgumentType(List<? extends SqmTypedNode<?>> arguments, int argumentIndex, SqmToSqlAstConverter converter) {
|
||||
if ( argumentIndex >= types.length ) {
|
||||
return null;
|
||||
}
|
||||
return getMappingModelExpressible(
|
||||
converter.getCreationContext().getTypeConfiguration(),
|
||||
types[argumentIndex]
|
||||
|
@ -188,7 +191,9 @@ public final class StandardFunctionArgumentTypeResolvers {
|
|||
return new AbstractFunctionArgumentTypeResolver() {
|
||||
@Override
|
||||
public @Nullable MappingModelExpressible<?> resolveFunctionArgumentType(List<? extends SqmTypedNode<?>> arguments, int argumentIndex, SqmToSqlAstConverter converter) {
|
||||
return resolvers[argumentIndex].resolveFunctionArgumentType( arguments, argumentIndex, converter );
|
||||
return argumentIndex < resolvers.length
|
||||
? resolvers[argumentIndex].resolveFunctionArgumentType( arguments, argumentIndex, converter )
|
||||
: null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
|
|||
visitOverClause( Collections.emptyList(), getSortSpecificationsRowNumbering( selectClause, queryPart ) );
|
||||
}
|
||||
|
||||
protected final boolean isParameter(Expression expression) {
|
||||
public static final boolean isParameter(Expression expression) {
|
||||
return expression instanceof JdbcParameter || expression instanceof SqmParameterInterpretation;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 @@ public class XmlArrayJdbcType extends ArrayJdbcType {
|
|||
if ( string == null ) {
|
||||
return null;
|
||||
}
|
||||
if ( javaType.getJavaType() == SQLXML.class ) {
|
||||
SQLXML sqlxml = options.getSession().getJdbcCoordinator().getLogicalConnection()
|
||||
.getPhysicalConnection()
|
||||
.createSQLXML();
|
||||
sqlxml.setString( string );
|
||||
//noinspection unchecked
|
||||
return (X) sqlxml;
|
||||
}
|
||||
return options.getSessionFactory().getFastSessionServices().getXmlFormatMapper().fromString(
|
||||
string,
|
||||
javaType,
|
||||
|
|
|
@ -97,6 +97,14 @@ public class XmlJdbcType implements AggregateJdbcType {
|
|||
options
|
||||
);
|
||||
}
|
||||
if ( javaType.getJavaType() == SQLXML.class ) {
|
||||
SQLXML sqlxml = options.getSession().getJdbcCoordinator().getLogicalConnection()
|
||||
.getPhysicalConnection()
|
||||
.createSQLXML();
|
||||
sqlxml.setString( string );
|
||||
//noinspection unchecked
|
||||
return (X) sqlxml;
|
||||
}
|
||||
return options.getSessionFactory().getFastSessionServices().getXmlFormatMapper().fromString(
|
||||
string,
|
||||
javaType,
|
||||
|
|
|
@ -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 @@ abstract public class DialectFeatureChecks {
|
|||
}
|
||||
}
|
||||
|
||||
public static class SupportsXmlTable implements DialectFeatureCheck {
|
||||
public boolean apply(Dialect dialect) {
|
||||
return definesSetReturningFunction( dialect, "xmltable" );
|
||||
}
|
||||
}
|
||||
|
||||
public static class SupportsArrayAgg implements DialectFeatureCheck {
|
||||
public boolean apply(Dialect dialect) {
|
||||
return definesFunction( dialect, "array_agg" );
|
||||
|
|
|
@ -88,6 +88,7 @@ Out-of-the-box, some common set-returning functions are already supported or emu
|
|||
* `unnest()` - allows to turn an array into rows
|
||||
* `generate_series()` - can be used to create a series of values as rows
|
||||
* `json_table()` - turns a JSON document into rows
|
||||
* `xmltable()` - turns an XML document into rows
|
||||
|
||||
[[cleanup]]
|
||||
== Clean-up
|
||||
|
|
Loading…
Reference in New Issue