From 1959fab1fab64ee4fd01376c75eb0e99aff4425d Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Wed, 20 Nov 2024 13:02:41 +0100 Subject: [PATCH] HHH-18800 Add XML aggregate support for PostgreSQL --- .../dialect/PostgreSQLLegacyDialect.java | 20 ++ .../hibernate/dialect/PostgreSQLDialect.java | 20 ++ .../aggregate/PostgreSQLAggregateSupport.java | 309 +++++++++++++++++- .../array/PostgreSQLUnnestFunction.java | 25 ++ .../function/array/UnnestFunction.java | 7 +- 5 files changed, 371 insertions(+), 10 deletions(-) diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java index 73f12a3357..e906643aa9 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java @@ -53,6 +53,8 @@ import org.hibernate.exception.spi.SQLExceptionConversionDelegate; import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.internal.util.JdbcExceptionHelper; +import org.hibernate.mapping.AggregateColumn; +import org.hibernate.mapping.Table; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.procedure.internal.PostgreSQLCallableStatementSupport; @@ -76,6 +78,8 @@ import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.tool.schema.internal.StandardTableExporter; +import org.hibernate.tool.schema.spi.Exporter; import org.hibernate.type.JavaObjectType; import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType; import org.hibernate.type.descriptor.jdbc.BlobJdbcType; @@ -143,6 +147,17 @@ public class PostgreSQLLegacyDialect extends Dialect { protected final PostgreSQLDriverKind driverKind; private final UniqueDelegate uniqueDelegate = new CreateTableUniqueDelegate(this); + private final StandardTableExporter postgresqlTableExporter = new StandardTableExporter( this ) { + @Override + protected void applyAggregateColumnCheck(StringBuilder buf, AggregateColumn aggregateColumn) { + final JdbcType jdbcType = aggregateColumn.getType().getJdbcType(); + if ( jdbcType.isXml() ) { + // Requires the use of xmltable which is not supported in check constraints + return; + } + super.applyAggregateColumnCheck( buf, aggregateColumn ); + } + }; public PostgreSQLLegacyDialect() { this( DatabaseVersion.make( 8, 0 ) ); @@ -1536,6 +1551,11 @@ public class PostgreSQLLegacyDialect extends Dialect { return uniqueDelegate; } + @Override + public Exporter getTableExporter() { + return postgresqlTableExporter; + } + /** * @return {@code true}, but only because we can "batch" truncate */ diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index a46cd13311..3707cee2bc 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -50,6 +50,8 @@ import org.hibernate.exception.spi.SQLExceptionConversionDelegate; import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.internal.util.JdbcExceptionHelper; +import org.hibernate.mapping.AggregateColumn; +import org.hibernate.mapping.Table; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.entity.mutation.EntityMutationTarget; @@ -78,6 +80,8 @@ import org.hibernate.sql.model.MutationOperation; import org.hibernate.sql.model.internal.OptionalTableUpdate; import org.hibernate.sql.model.jdbc.OptionalTableUpdateOperation; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.tool.schema.internal.StandardTableExporter; +import org.hibernate.tool.schema.spi.Exporter; import org.hibernate.type.JavaObjectType; import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType; import org.hibernate.type.descriptor.jdbc.BlobJdbcType; @@ -145,6 +149,17 @@ public class PostgreSQLDialect extends Dialect { protected final static DatabaseVersion MINIMUM_VERSION = DatabaseVersion.make( 12 ); private final UniqueDelegate uniqueDelegate = new CreateTableUniqueDelegate(this); + private final StandardTableExporter postgresqlTableExporter = new StandardTableExporter( this ) { + @Override + protected void applyAggregateColumnCheck(StringBuilder buf, AggregateColumn aggregateColumn) { + final JdbcType jdbcType = aggregateColumn.getType().getJdbcType(); + if ( jdbcType.isXml() ) { + // Requires the use of xmltable which is not supported in check constraints + return; + } + super.applyAggregateColumnCheck( buf, aggregateColumn ); + } + }; protected final PostgreSQLDriverKind driverKind; private final OptionalTableUpdateStrategy optionalTableUpdateStrategy; @@ -1474,6 +1489,11 @@ public class PostgreSQLDialect extends Dialect { return uniqueDelegate; } + @Override + public Exporter
getTableExporter() { + return postgresqlTableExporter; + } + /** * @return {@code true}, but only because we can "batch" truncate */ diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/PostgreSQLAggregateSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/PostgreSQLAggregateSupport.java index c1fa526bb1..a9743f0f26 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/PostgreSQLAggregateSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/PostgreSQLAggregateSupport.java @@ -8,8 +8,10 @@ import java.util.LinkedHashMap; import java.util.Map; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.XmlHelper; import org.hibernate.internal.util.StringHelper; import org.hibernate.mapping.Column; +import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.metamodel.mapping.SelectablePath; @@ -18,6 +20,8 @@ import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.type.BasicPluralType; +import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.spi.TypeConfiguration; import static org.hibernate.type.SqlTypes.ARRAY; @@ -31,16 +35,25 @@ import static org.hibernate.type.SqlTypes.JSON; import static org.hibernate.type.SqlTypes.JSON_ARRAY; import static org.hibernate.type.SqlTypes.LONG32VARBINARY; import static org.hibernate.type.SqlTypes.SMALLINT; +import static org.hibernate.type.SqlTypes.SQLXML; import static org.hibernate.type.SqlTypes.STRUCT; import static org.hibernate.type.SqlTypes.STRUCT_ARRAY; import static org.hibernate.type.SqlTypes.STRUCT_TABLE; import static org.hibernate.type.SqlTypes.TINYINT; import static org.hibernate.type.SqlTypes.VARBINARY; +import static org.hibernate.type.SqlTypes.XML_ARRAY; public class PostgreSQLAggregateSupport extends AggregateSupportImpl { private static final AggregateSupport INSTANCE = new PostgreSQLAggregateSupport(); + private static final String XML_EXTRACT_START = "xmlelement(name \"" + XmlHelper.ROOT_TAG + "\",(select xmlagg(t.v) from xmltable("; + private static final String XML_EXTRACT_SEPARATOR = "/*' passing "; + private static final String XML_EXTRACT_END = " columns v xml path '.')t))"; + private static final String XML_QUERY_START = "(select xmlagg(t.v) from xmltable("; + private static final String XML_QUERY_SEPARATOR = "' passing "; + private static final String XML_QUERY_END = " columns v xml path '.')t)"; + public static AggregateSupport valueOf(Dialect dialect) { return PostgreSQLAggregateSupport.INSTANCE; } @@ -109,6 +122,40 @@ public class PostgreSQLAggregateSupport extends AggregateSupportImpl { "cast(" + aggregateParentReadExpression + "->>'" + columnExpression + "' as " + column.getColumnDefinition() + ')' ); } + case XML_ARRAY: + case SQLXML: + switch ( column.getJdbcMapping().getJdbcType().getDefaultSqlTypeCode() ) { + case SQLXML: + return template.replace( + placeholder, + XML_EXTRACT_START + xmlExtractArguments( aggregateParentReadExpression, columnExpression + "/*" ) + XML_EXTRACT_END + ); + case XML_ARRAY: + if ( typeConfiguration.getCurrentBaseSqlTypeIndicators().isXmlFormatMapperLegacyFormatEnabled() ) { + throw new IllegalArgumentException( "XML array '" + columnExpression + "' in '" + aggregateParentReadExpression + "' is not supported with legacy format enabled." ); + } + else { + return template.replace( + placeholder, + "xmlelement(name \"Collection\",(select xmlagg(t.v order by t.i) from xmltable(" + xmlExtractArguments( aggregateParentReadExpression, columnExpression + "/*" ) + " columns v xml path '.', i for ordinality)t))" + ); + } + case BINARY: + case VARBINARY: + case LONG32VARBINARY: + // We encode binary data as hex, so we have to decode here + return template.replace( + placeholder, + "decode((select t.v from xmltable(" + xmlExtractArguments( aggregateParentReadExpression, columnExpression )+ " columns v text path '.') t),'hex')" + ); + case ARRAY: + throw new UnsupportedOperationException( "Transforming XML_ARRAY to native arrays is not supported on PostgreSQL!" ); + default: + return template.replace( + placeholder, + "(select t.v from xmltable(" + xmlExtractArguments( aggregateParentReadExpression, columnExpression ) + " columns v " + column.getColumnDefinition() + " path '.') t)" + ); + } case STRUCT: case STRUCT_ARRAY: case STRUCT_TABLE: @@ -117,6 +164,35 @@ public class PostgreSQLAggregateSupport extends AggregateSupportImpl { throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateColumnTypeCode ); } + private static String xmlExtractArguments(String aggregateParentReadExpression, String xpathFragment) { + final String extractArguments; + int separatorIndex; + if ( aggregateParentReadExpression.startsWith( XML_EXTRACT_START ) + && aggregateParentReadExpression.endsWith( XML_EXTRACT_END ) + && (separatorIndex = aggregateParentReadExpression.indexOf( XML_EXTRACT_SEPARATOR )) != -1 ) { + final StringBuilder sb = new StringBuilder( aggregateParentReadExpression.length() - XML_EXTRACT_START.length() + xpathFragment.length() ); + sb.append( aggregateParentReadExpression, XML_EXTRACT_START.length(), separatorIndex ); + sb.append( '/' ); + sb.append( xpathFragment ); + sb.append( aggregateParentReadExpression, separatorIndex + 2, aggregateParentReadExpression.length() - XML_EXTRACT_END.length() ); + extractArguments = sb.toString(); + } + else if ( aggregateParentReadExpression.startsWith( XML_QUERY_START ) + && aggregateParentReadExpression.endsWith( XML_QUERY_END ) + && (separatorIndex = aggregateParentReadExpression.indexOf( XML_QUERY_SEPARATOR )) != -1 ) { + final StringBuilder sb = new StringBuilder( aggregateParentReadExpression.length() - XML_QUERY_START.length() + xpathFragment.length() ); + sb.append( aggregateParentReadExpression, XML_QUERY_START.length(), separatorIndex ); + sb.append( '/' ); + sb.append( xpathFragment ); + sb.append( aggregateParentReadExpression, separatorIndex, aggregateParentReadExpression.length() - XML_QUERY_END.length() ); + extractArguments = sb.toString(); + } + else { + extractArguments = "'/" + XmlHelper.ROOT_TAG + "/" + xpathFragment + "' passing " + aggregateParentReadExpression; + } + return extractArguments; + } + private static String jsonCustomWriteExpression(String customWriteExpression, JdbcMapping jdbcMapping) { final int sqlTypeCode = jdbcMapping.getJdbcType().getDefaultSqlTypeCode(); switch ( sqlTypeCode ) { @@ -141,6 +217,30 @@ public class PostgreSQLAggregateSupport extends AggregateSupportImpl { } } + private static String xmlCustomWriteExpression(String customWriteExpression, JdbcMapping jdbcMapping) { + final int sqlTypeCode = jdbcMapping.getJdbcType().getDefaultSqlTypeCode(); + switch ( sqlTypeCode ) { + case BINARY: + case VARBINARY: + case LONG32VARBINARY: + // We encode binary data as hex + return "encode(" + customWriteExpression + ",'hex')"; +// case ARRAY: +// final BasicPluralType pluralType = (BasicPluralType) jdbcMapping; +// switch ( pluralType.getElementType().getJdbcType().getDefaultSqlTypeCode() ) { +// case BINARY: +// case VARBINARY: +// case LONG32VARBINARY: +// // We encode binary data as hex +// return "to_jsonb(array(select encode(unnest(" + customWriteExpression + "),'hex')))"; +// default: +// return "to_jsonb(" + customWriteExpression + ")"; +// } + default: + return customWriteExpression; + } + } + @Override public String aggregateComponentAssignmentExpression( String aggregateParentAssignmentExpression, @@ -150,7 +250,9 @@ public class PostgreSQLAggregateSupport extends AggregateSupportImpl { switch ( aggregateColumnTypeCode ) { case JSON: case JSON_ARRAY: - // For JSON we always have to replace the whole object + case SQLXML: + case XML_ARRAY: + // For JSON/XML we always have to replace the whole object return aggregateParentAssignmentExpression; case STRUCT: case STRUCT_ARRAY: @@ -164,6 +266,7 @@ public class PostgreSQLAggregateSupport extends AggregateSupportImpl { public boolean requiresAggregateCustomWriteExpressionRenderer(int aggregateSqlTypeCode) { switch ( aggregateSqlTypeCode ) { case JSON: + case SQLXML: return true; } return false; @@ -183,17 +286,13 @@ public class PostgreSQLAggregateSupport extends AggregateSupportImpl { final int aggregateSqlTypeCode = aggregateColumn.getJdbcMapping().getJdbcType().getDefaultSqlTypeCode(); switch ( aggregateSqlTypeCode ) { case JSON: - return jsonAggregateColumnWriter( aggregateColumn, columnsToUpdate ); + return new RootJsonWriteExpression( aggregateColumn, columnsToUpdate ); + case SQLXML: + return new RootXmlWriteExpression( aggregateColumn, columnsToUpdate ); } throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateSqlTypeCode ); } - private WriteExpressionRenderer jsonAggregateColumnWriter( - SelectableMapping aggregateColumn, - SelectableMapping[] columns) { - return new RootJsonWriteExpression( aggregateColumn, columns ); - } - interface JsonWriteExpression { void append( SqlAppender sb, @@ -329,4 +428,198 @@ public class PostgreSQLAggregateSupport extends AggregateSupportImpl { } } + interface XmlWriteExpression { + void append( + SqlAppender sb, + String path, + SqlAstTranslator translator, + AggregateColumnWriteExpression expression); + } + private static class AggregateXmlWriteExpression implements XmlWriteExpression { + + private final SelectableMapping selectableMapping; + private final String columnDefinition; + private final LinkedHashMap subExpressions = new LinkedHashMap<>(); + + private AggregateXmlWriteExpression(SelectableMapping selectableMapping, String columnDefinition) { + this.selectableMapping = selectableMapping; + this.columnDefinition = columnDefinition; + } + + protected void initializeSubExpressions(SelectableMapping aggregateColumn, SelectableMapping[] columns) { + for ( SelectableMapping column : columns ) { + final SelectablePath selectablePath = column.getSelectablePath(); + final SelectablePath[] parts = selectablePath.getParts(); + AggregateXmlWriteExpression currentAggregate = this; + for ( int i = 1; i < parts.length - 1; i++ ) { + final AggregateJdbcType aggregateJdbcType = (AggregateJdbcType) currentAggregate.selectableMapping.getJdbcMapping().getJdbcType(); + final EmbeddableMappingType embeddableMappingType = aggregateJdbcType.getEmbeddableMappingType(); + final int selectableIndex = embeddableMappingType.getSelectableIndex( parts[i].getSelectableName() ); + currentAggregate = (AggregateXmlWriteExpression) currentAggregate.subExpressions.computeIfAbsent( + parts[i].getSelectableName(), + k -> new AggregateXmlWriteExpression( embeddableMappingType.getJdbcValueSelectable( selectableIndex ), columnDefinition ) + ); + } + final String customWriteExpression = column.getWriteExpression(); + currentAggregate.subExpressions.put( + parts[parts.length - 1].getSelectableName(), + new BasicXmlWriteExpression( + column, + xmlCustomWriteExpression( customWriteExpression, column.getJdbcMapping() ) + ) + ); + } + passThroughUnsetSubExpressions( aggregateColumn ); + } + + protected void passThroughUnsetSubExpressions(SelectableMapping aggregateColumn) { + final AggregateJdbcType aggregateJdbcType = (AggregateJdbcType) aggregateColumn.getJdbcMapping().getJdbcType(); + final EmbeddableMappingType embeddableMappingType = aggregateJdbcType.getEmbeddableMappingType(); + final int jdbcValueCount = embeddableMappingType.getJdbcValueCount(); + for ( int i = 0; i < jdbcValueCount; i++ ) { + final SelectableMapping selectableMapping = embeddableMappingType.getJdbcValueSelectable( i ); + + final XmlWriteExpression xmlWriteExpression = subExpressions.get( selectableMapping.getSelectableName() ); + if ( xmlWriteExpression == null ) { + subExpressions.put( + selectableMapping.getSelectableName(), + new PassThroughXmlWriteExpression( selectableMapping ) + ); + } + else if ( xmlWriteExpression instanceof AggregateXmlWriteExpression writeExpression ) { + writeExpression.passThroughUnsetSubExpressions( selectableMapping ); + } + } + } + + protected String getTagName() { + return selectableMapping.getSelectableName(); + } + + @Override + public void append( + SqlAppender sb, + String path, + SqlAstTranslator translator, + AggregateColumnWriteExpression expression) { + sb.append( "xmlelement(name " ); + sb.appendDoubleQuoteEscapedString( getTagName() ); + sb.append( ",xmlconcat" ); + char separator = '('; + for ( Map.Entry entry : subExpressions.entrySet() ) { + sb.append( separator ); + + final XmlWriteExpression value = entry.getValue(); + if ( value instanceof AggregateXmlWriteExpression ) { + final String subPath = XML_QUERY_START + xmlExtractArguments( path, entry.getKey() ) + XML_QUERY_END; + value.append( sb, subPath, translator, expression ); + } + else { + value.append( sb, path, translator, expression ); + } + separator = ','; + } + sb.append( "))" ); + } + } + + private static class RootXmlWriteExpression extends AggregateXmlWriteExpression + implements WriteExpressionRenderer { + private final String path; + + RootXmlWriteExpression(SelectableMapping aggregateColumn, SelectableMapping[] columns) { + super( aggregateColumn, aggregateColumn.getColumnDefinition() ); + path = aggregateColumn.getSelectionExpression(); + initializeSubExpressions( aggregateColumn, columns ); + } + + @Override + protected String getTagName() { + return XmlHelper.ROOT_TAG; + } + + @Override + public void render( + SqlAppender sqlAppender, + SqlAstTranslator translator, + AggregateColumnWriteExpression aggregateColumnWriteExpression, + String qualifier) { + final String basePath; + if ( qualifier == null || qualifier.isBlank() ) { + basePath = path; + } + else { + basePath = qualifier + "." + path; + } + append( sqlAppender, XML_QUERY_START + "'/" + getTagName() + "' passing " + basePath + XML_QUERY_END, translator, aggregateColumnWriteExpression ); + } + } + private static class BasicXmlWriteExpression implements XmlWriteExpression { + + private final SelectableMapping selectableMapping; + private final String[] customWriteExpressionParts; + + BasicXmlWriteExpression(SelectableMapping selectableMapping, String customWriteExpression) { + this.selectableMapping = selectableMapping; + if ( customWriteExpression.equals( "?" ) ) { + this.customWriteExpressionParts = new String[]{ "", "" }; + } + else { + assert !customWriteExpression.startsWith( "?" ); + final String[] parts = StringHelper.split( "?", customWriteExpression ); + assert parts.length == 2 || (parts.length & 1) == 1; + this.customWriteExpressionParts = parts; + } + } + + @Override + public void append( + SqlAppender sb, + String path, + SqlAstTranslator translator, + AggregateColumnWriteExpression expression) { + final JdbcType jdbcType = selectableMapping.getJdbcMapping().getJdbcType(); + final boolean isArray = jdbcType.getDefaultSqlTypeCode() == XML_ARRAY; + sb.append( "xmlelement(name " ); + sb.appendDoubleQuoteEscapedString( selectableMapping.getSelectableName() ); + sb.append( ',' ); + if ( isArray ) { + // Remove the tag to wrap the value into the selectable specific tag + sb.append( "(select xmlagg(t.v order by t.i) from xmltable('/Collection/*' passing " ); + } + sb.append( customWriteExpressionParts[0] ); + for ( int i = 1; i < customWriteExpressionParts.length; i++ ) { + // We use NO_UNTYPED here so that expressions which require type inference are casted explicitly, + // since we don't know how the custom write expression looks like where this is embedded, + // so we have to be pessimistic and avoid ambiguities + translator.render( expression.getValueExpression( selectableMapping ), SqlAstNodeRenderingMode.NO_UNTYPED ); + sb.append( customWriteExpressionParts[i] ); + } + if ( isArray ) { + sb.append( " columns v xml path '.', i for ordinality)t)" ); + } + sb.append( ')' ); + } + } + + private static class PassThroughXmlWriteExpression implements XmlWriteExpression { + + private final SelectableMapping selectableMapping; + + PassThroughXmlWriteExpression(SelectableMapping selectableMapping) { + this.selectableMapping = selectableMapping; + } + + @Override + public void append( + SqlAppender sb, + String path, + SqlAstTranslator translator, + AggregateColumnWriteExpression expression) { + sb.append( XML_QUERY_START ); + sb.append( xmlExtractArguments( path, selectableMapping.getSelectableName() ) ); + sb.append( XML_QUERY_END ); + } + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLUnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLUnnestFunction.java index 859f3cdb7b..4bb275393f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLUnnestFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLUnnestFunction.java @@ -5,6 +5,7 @@ package org.hibernate.dialect.function.array; import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.dialect.XmlHelper; import org.hibernate.dialect.aggregate.AggregateSupport; import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.SqlTypedMapping; @@ -14,6 +15,7 @@ import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.type.BasicPluralType; import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.java.BasicPluralJavaType; /** @@ -71,4 +73,27 @@ public class PostgreSQLUnnestFunction extends UnnestFunction { sqlAppender.appendSql( " t(v))" ); } } + + protected void renderXmlTable( + SqlAppender sqlAppender, + Expression array, + BasicPluralType pluralType, + @Nullable SqlTypedMapping sqlTypedMapping, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + final XmlHelper.CollectionTags collectionTags = XmlHelper.determineCollectionTags( + (BasicPluralJavaType) pluralType.getJavaTypeDescriptor(), walker.getSessionFactory() + ); + + sqlAppender.appendSql( "xmltable('/" ); + sqlAppender.appendSql( collectionTags.rootName() ); + sqlAppender.appendSql( '/' ); + sqlAppender.appendSql( collectionTags.elementName() ); + sqlAppender.appendSql( "' passing " ); + array.accept( walker ); + sqlAppender.appendSql( " columns" ); + renderXmlTableColumns( sqlAppender, tupleType, walker ); + sqlAppender.appendSql( ')' ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/UnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/UnnestFunction.java index f70e4bf6f5..563a1adec3 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/UnnestFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/UnnestFunction.java @@ -162,6 +162,11 @@ public class UnnestFunction extends AbstractSqmSelfRenderingSetReturningFunction sqlAppender.appendSql( "' passing " ); array.accept( walker ); sqlAppender.appendSql( " as \"d\" columns" ); + renderXmlTableColumns( sqlAppender, tupleType, walker ); + sqlAppender.appendSql( ')' ); + } + + protected void renderXmlTableColumns(SqlAppender sqlAppender, AnonymousTupleTableGroupProducer tupleType, SqlAstTranslator walker) { if ( tupleType.findSubPart( CollectionPart.Nature.ELEMENT.getName(), null ) == null ) { tupleType.forEachSelectable( 0, (selectionIndex, selectableMapping) -> { if ( selectionIndex == 0 ) { @@ -205,8 +210,6 @@ public class UnnestFunction extends AbstractSqmSelfRenderingSetReturningFunction } } ); } - - sqlAppender.appendSql( ')' ); } protected void renderUnnest(