HHH-18803 Add XML aggregate support for DB2

This commit is contained in:
Christian Beikov 2024-11-15 12:24:59 +01:00
parent d39aa6d162
commit 1077b6f0a9
5 changed files with 391 additions and 35 deletions

View File

@ -63,7 +63,9 @@ import org.hibernate.exception.spi.SQLExceptionConversionDelegate;
import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor;
import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.exception.spi.ViolatedConstraintNameExtractor;
import org.hibernate.internal.util.JdbcExceptionHelper; import org.hibernate.internal.util.JdbcExceptionHelper;
import org.hibernate.mapping.AggregateColumn;
import org.hibernate.mapping.Column; import org.hibernate.mapping.Column;
import org.hibernate.mapping.Table;
import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
import org.hibernate.procedure.internal.DB2CallableStatementSupport; import org.hibernate.procedure.internal.DB2CallableStatementSupport;
@ -87,6 +89,8 @@ import org.hibernate.sql.exec.spi.JdbcOperation;
import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorDB2DatabaseImpl; import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorDB2DatabaseImpl;
import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorNoOpImpl; import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorNoOpImpl;
import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor;
import org.hibernate.tool.schema.internal.StandardTableExporter;
import org.hibernate.tool.schema.spi.Exporter;
import org.hibernate.type.JavaObjectType; import org.hibernate.type.JavaObjectType;
import org.hibernate.type.SqlTypes; import org.hibernate.type.SqlTypes;
import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.StandardBasicTypes;
@ -96,6 +100,7 @@ import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType;
import org.hibernate.type.descriptor.jdbc.CharJdbcType; import org.hibernate.type.descriptor.jdbc.CharJdbcType;
import org.hibernate.type.descriptor.jdbc.ClobJdbcType; import org.hibernate.type.descriptor.jdbc.ClobJdbcType;
import org.hibernate.type.descriptor.jdbc.InstantJdbcType; import org.hibernate.type.descriptor.jdbc.InstantJdbcType;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.descriptor.jdbc.LocalDateJdbcType; import org.hibernate.type.descriptor.jdbc.LocalDateJdbcType;
import org.hibernate.type.descriptor.jdbc.LocalDateTimeJdbcType; import org.hibernate.type.descriptor.jdbc.LocalDateTimeJdbcType;
import org.hibernate.type.descriptor.jdbc.LocalTimeJdbcType; import org.hibernate.type.descriptor.jdbc.LocalTimeJdbcType;
@ -154,6 +159,17 @@ public class DB2LegacyDialect extends Dialect {
? LegacyDB2LimitHandler.INSTANCE ? LegacyDB2LimitHandler.INSTANCE
: DB2LimitHandler.INSTANCE; : DB2LimitHandler.INSTANCE;
private final UniqueDelegate uniqueDelegate = createUniqueDelegate(); private final UniqueDelegate uniqueDelegate = createUniqueDelegate();
private final StandardTableExporter db2TableExporter = new StandardTableExporter( this ) {
@Override
protected void applyAggregateColumnCheck(StringBuilder buf, AggregateColumn aggregateColumn) {
final JdbcType jdbcType = aggregateColumn.getType().getJdbcType();
if ( jdbcType.isLob() || jdbcType.isXml() ) {
// LOB or XML columns can't have check constraints
return;
}
super.applyAggregateColumnCheck( buf, aggregateColumn );
}
};
public DB2LegacyDialect() { public DB2LegacyDialect() {
this( DatabaseVersion.make( 9, 0 ) ); this( DatabaseVersion.make( 9, 0 ) );
@ -189,6 +205,11 @@ public class DB2LegacyDialect extends Dialect {
return this.getVersion(); return this.getVersion();
} }
@Override
public Exporter<Table> getTableExporter() {
return this.db2TableExporter;
}
@Override @Override
public int getDefaultStatementBatchSize() { public int getDefaultStatementBatchSize() {
return 0; return 0;

View File

@ -53,7 +53,9 @@ import org.hibernate.exception.LockTimeoutException;
import org.hibernate.exception.spi.SQLExceptionConversionDelegate; import org.hibernate.exception.spi.SQLExceptionConversionDelegate;
import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor;
import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.exception.spi.ViolatedConstraintNameExtractor;
import org.hibernate.mapping.AggregateColumn;
import org.hibernate.mapping.Column; import org.hibernate.mapping.Column;
import org.hibernate.mapping.Table;
import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
import org.hibernate.procedure.internal.DB2CallableStatementSupport; import org.hibernate.procedure.internal.DB2CallableStatementSupport;
@ -77,6 +79,8 @@ import org.hibernate.sql.exec.spi.JdbcOperation;
import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorDB2DatabaseImpl; import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorDB2DatabaseImpl;
import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorNoOpImpl; import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorNoOpImpl;
import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor;
import org.hibernate.tool.schema.internal.StandardTableExporter;
import org.hibernate.tool.schema.spi.Exporter;
import org.hibernate.type.JavaObjectType; import org.hibernate.type.JavaObjectType;
import org.hibernate.type.SqlTypes; import org.hibernate.type.SqlTypes;
import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.StandardBasicTypes;
@ -84,6 +88,7 @@ import org.hibernate.type.descriptor.ValueExtractor;
import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType; import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType;
import org.hibernate.type.descriptor.jdbc.InstantJdbcType; import org.hibernate.type.descriptor.jdbc.InstantJdbcType;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.descriptor.jdbc.LocalDateJdbcType; import org.hibernate.type.descriptor.jdbc.LocalDateJdbcType;
import org.hibernate.type.descriptor.jdbc.LocalDateTimeJdbcType; import org.hibernate.type.descriptor.jdbc.LocalDateTimeJdbcType;
import org.hibernate.type.descriptor.jdbc.LocalTimeJdbcType; import org.hibernate.type.descriptor.jdbc.LocalTimeJdbcType;
@ -146,6 +151,17 @@ public class DB2Dialect extends Dialect {
? LegacyDB2LimitHandler.INSTANCE ? LegacyDB2LimitHandler.INSTANCE
: DB2LimitHandler.INSTANCE; : DB2LimitHandler.INSTANCE;
private final UniqueDelegate uniqueDelegate = createUniqueDelegate(); private final UniqueDelegate uniqueDelegate = createUniqueDelegate();
private final StandardTableExporter db2TableExporter = new StandardTableExporter( this ) {
@Override
protected void applyAggregateColumnCheck(StringBuilder buf, AggregateColumn aggregateColumn) {
final JdbcType jdbcType = aggregateColumn.getType().getJdbcType();
if ( jdbcType.isLob() || jdbcType.isXml() ) {
// LOB or XML columns can't have check constraints
return;
}
super.applyAggregateColumnCheck( buf, aggregateColumn );
}
};
public DB2Dialect() { public DB2Dialect() {
this( MINIMUM_VERSION ); this( MINIMUM_VERSION );
@ -171,6 +187,11 @@ public class DB2Dialect extends Dialect {
return this.getVersion(); return this.getVersion();
} }
@Override
public Exporter<Table> getTableExporter() {
return this.db2TableExporter;
}
@Override @Override
public int getDefaultStatementBatchSize() { public int getDefaultStatementBatchSize() {
return 0; return 0;

View File

@ -45,6 +45,7 @@ import static org.hibernate.type.SqlTypes.JSON;
import static org.hibernate.type.SqlTypes.JSON_ARRAY; import static org.hibernate.type.SqlTypes.JSON_ARRAY;
import static org.hibernate.type.SqlTypes.LONG32VARBINARY; import static org.hibernate.type.SqlTypes.LONG32VARBINARY;
import static org.hibernate.type.SqlTypes.SMALLINT; 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;
import static org.hibernate.type.SqlTypes.TIME; import static org.hibernate.type.SqlTypes.TIME;
import static org.hibernate.type.SqlTypes.TIMESTAMP; import static org.hibernate.type.SqlTypes.TIMESTAMP;
@ -52,6 +53,7 @@ import static org.hibernate.type.SqlTypes.TIMESTAMP_UTC;
import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE;
import static org.hibernate.type.SqlTypes.UUID; import static org.hibernate.type.SqlTypes.UUID;
import static org.hibernate.type.SqlTypes.VARBINARY; import static org.hibernate.type.SqlTypes.VARBINARY;
import static org.hibernate.type.SqlTypes.XML_ARRAY;
public class DB2AggregateSupport extends AggregateSupportImpl { public class DB2AggregateSupport extends AggregateSupportImpl {
@ -59,6 +61,9 @@ public class DB2AggregateSupport extends AggregateSupportImpl {
public static final AggregateSupport JSON_INSTANCE = new DB2AggregateSupport( true ); public static final AggregateSupport JSON_INSTANCE = new DB2AggregateSupport( true );
private static final String JSON_QUERY_START = "json_query("; private static final String JSON_QUERY_START = "json_query(";
private static final String JSON_QUERY_JSON_END = "')"; private static final String JSON_QUERY_JSON_END = "')";
private static final String XML_EXTRACT_START = "xmlelement(name \"" + XmlHelper.ROOT_TAG + "\",xmlquery(";
private static final String XML_EXTRACT_SEPARATOR = "/*' passing ";
private static final String XML_EXTRACT_END = " as \"d\"))";
private final boolean jsonSupport; private final boolean jsonSupport;
@ -134,12 +139,91 @@ public class DB2AggregateSupport extends AggregateSupportImpl {
"json_value(" + parentPartExpression + columnExpression + "' returning " + column.getColumnDefinition() + ")" "json_value(" + parentPartExpression + columnExpression + "' returning " + column.getColumnDefinition() + ")"
); );
} }
case SQLXML:
case XML_ARRAY:
switch ( column.getJdbcMapping().getJdbcType().getDefaultSqlTypeCode() ) {
case BOOLEAN:
if ( SqlTypes.isNumericType( column.getJdbcMapping().getJdbcType().getDdlTypeCode() ) ) {
return template.replace(
placeholder,
"decode(xmlcast(xmlquery(" + xmlExtractArguments( aggregateParentReadExpression, columnExpression ) + ") as varchar(5)),'true',1,'false',0)"
);
}
else {
return template.replace(
placeholder,
"decode(xmlcast(xmlquery(" + xmlExtractArguments( aggregateParentReadExpression, columnExpression ) + ") as varchar(5)),'true',true,'false',false)"
);
}
case BINARY:
case VARBINARY:
case LONG32VARBINARY:
case BLOB:
// We encode binary data as hex, so we have to decode here
return template.replace(
placeholder,
"hextoraw(xmlcast(xmlquery(" + xmlExtractArguments( aggregateParentReadExpression, columnExpression ) + ") as clob))"
);
case TIMESTAMP_WITH_TIMEZONE:
case TIMESTAMP_UTC:
return template.replace(
placeholder,
"cast(trim(trailing 'Z' from xmlcast(xmlquery(" + xmlExtractArguments( aggregateParentReadExpression, columnExpression ) + ") as varchar(35))) as " + column.getColumnDefinition() + ")"
);
case SQLXML:
return template.replace(
placeholder,
XML_EXTRACT_START + xmlExtractArguments( aggregateParentReadExpression, columnExpression + "/*" ) + "))"
);
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\",xmlquery(" + xmlExtractArguments( aggregateParentReadExpression, columnExpression + "/*" ) + "))"
);
}
case UUID:
if ( SqlTypes.isBinaryType( column.getJdbcMapping().getJdbcType().getDdlTypeCode() ) ) {
return template.replace(
placeholder,
"hextoraw(replace(xmlcast(xmlquery(" + xmlExtractArguments( aggregateParentReadExpression, columnExpression ) + ") as varchar(36)),'-',''))"
);
}
// Fall-through intended
default:
return template.replace(
placeholder,
"xmlcast(xmlquery(" + xmlExtractArguments( aggregateParentReadExpression, columnExpression ) + ") as " + column.getColumnDefinition() + ")"
);
}
case STRUCT: case STRUCT:
return template.replace( placeholder, aggregateParentReadExpression + ".." + columnExpression ); return template.replace( placeholder, aggregateParentReadExpression + ".." + columnExpression );
} }
throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateColumnTypeCode ); throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateColumnTypeCode );
} }
private static String xmlExtractArguments(String aggregateParentReadExpression, String xpathFragment) {
final String extractArguments;
final 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() - 2 );
extractArguments = sb.toString();
}
else {
extractArguments = "'$d/" + XmlHelper.ROOT_TAG + "/" + xpathFragment + "' passing " + aggregateParentReadExpression + " as \"d\"";
}
return extractArguments;
}
private static String jsonCustomWriteExpression(String customWriteExpression, JdbcMapping jdbcMapping) { private static String jsonCustomWriteExpression(String customWriteExpression, JdbcMapping jdbcMapping) {
final int sqlTypeCode = jdbcMapping.getJdbcType().getDefaultSqlTypeCode(); final int sqlTypeCode = jdbcMapping.getJdbcType().getDefaultSqlTypeCode();
switch ( sqlTypeCode ) { switch ( sqlTypeCode ) {
@ -167,6 +251,33 @@ public class DB2AggregateSupport extends AggregateSupportImpl {
} }
} }
private static String xmlCustomWriteExpression(String customWriteExpression, JdbcMapping jdbcMapping) {
final int sqlTypeCode = jdbcMapping.getJdbcType().getDefaultSqlTypeCode();
switch ( sqlTypeCode ) {
case BINARY:
case VARBINARY:
case LONG32VARBINARY:
case BLOB:
// We encode binary data as hex
return "hex(" + customWriteExpression + ")";
case UUID:
return "regexp_replace(lower(hex(" + customWriteExpression + ")),'^(.{8})(.{4})(.{4})(.{4})(.{12})$','$1-$2-$3-$4-$5')";
// case ARRAY:
// case XML_ARRAY:
// return "(" + customWriteExpression + ") format json";
case BOOLEAN:
return "decode(" + customWriteExpression + ",true,'true',false,'false')";
case TIME:
return "varchar_format(timestamp('1970-01-01'," + customWriteExpression + "),'HH24:MI:SS')";
case TIMESTAMP:
return "replace(varchar_format(" + customWriteExpression + ",'YYYY-MM-DD HH24:MI:SS.FF9'),' ','T')";
case TIMESTAMP_UTC:
return "replace(varchar_format(" + customWriteExpression + ",'YYYY-MM-DD HH24:MI:SS.FF9'),' ','T')||'Z'";
default:
return customWriteExpression;
}
}
@Override @Override
public String aggregateComponentAssignmentExpression( public String aggregateComponentAssignmentExpression(
String aggregateParentAssignmentExpression, String aggregateParentAssignmentExpression,
@ -181,6 +292,9 @@ public class DB2AggregateSupport extends AggregateSupportImpl {
return aggregateParentAssignmentExpression; return aggregateParentAssignmentExpression;
} }
break; break;
case SQLXML:
case XML_ARRAY:
return aggregateParentAssignmentExpression;
case STRUCT: case STRUCT:
return aggregateParentAssignmentExpression + ".." + columnExpression; return aggregateParentAssignmentExpression + ".." + columnExpression;
} }
@ -201,6 +315,9 @@ public class DB2AggregateSupport extends AggregateSupportImpl {
return null; return null;
} }
break; break;
case SQLXML:
case XML_ARRAY:
return null;
case STRUCT: case STRUCT:
final StringBuilder sb = new StringBuilder(); final StringBuilder sb = new StringBuilder();
appendStructCustomWriteExpression( aggregateColumn, aggregatedColumns, sb ); appendStructCustomWriteExpression( aggregateColumn, aggregatedColumns, sb );
@ -240,6 +357,9 @@ public class DB2AggregateSupport extends AggregateSupportImpl {
else if ( aggregateColumnSqlTypeCode == JSON ) { else if ( aggregateColumnSqlTypeCode == JSON ) {
return columnSqlTypeCode == ARRAY ? JSON_ARRAY : columnSqlTypeCode; return columnSqlTypeCode == ARRAY ? JSON_ARRAY : columnSqlTypeCode;
} }
else if ( aggregateColumnSqlTypeCode == SQLXML ) {
return columnSqlTypeCode == ARRAY ? XML_ARRAY : columnSqlTypeCode;
}
else { else {
return columnSqlTypeCode; return columnSqlTypeCode;
} }
@ -247,7 +367,7 @@ public class DB2AggregateSupport extends AggregateSupportImpl {
@Override @Override
public boolean requiresAggregateCustomWriteExpressionRenderer(int aggregateSqlTypeCode) { public boolean requiresAggregateCustomWriteExpressionRenderer(int aggregateSqlTypeCode) {
return aggregateSqlTypeCode == STRUCT || aggregateSqlTypeCode == JSON; return aggregateSqlTypeCode == STRUCT || aggregateSqlTypeCode == JSON || aggregateSqlTypeCode == SQLXML;
} }
@Override @Override
@ -259,28 +379,17 @@ public class DB2AggregateSupport extends AggregateSupportImpl {
switch ( aggregateSqlTypeCode ) { switch ( aggregateSqlTypeCode ) {
case JSON: case JSON:
if ( jsonSupport ) { if ( jsonSupport ) {
return jsonAggregateColumnWriter( aggregateColumn, columnsToUpdate ); return new RootJsonWriteExpression( aggregateColumn, columnsToUpdate );
} }
break; break;
case SQLXML:
return new RootXmlWriteExpression( aggregateColumn, columnsToUpdate );
case STRUCT: case STRUCT:
return structAggregateColumnWriter( aggregateColumn, columnsToUpdate, typeConfiguration ); return new RootStructWriteExpression( aggregateColumn, columnsToUpdate, typeConfiguration );
} }
throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateSqlTypeCode ); throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateSqlTypeCode );
} }
private WriteExpressionRenderer jsonAggregateColumnWriter(
SelectableMapping aggregateColumn,
SelectableMapping[] columns) {
return new RootJsonWriteExpression( aggregateColumn, columns );
}
private WriteExpressionRenderer structAggregateColumnWriter(
SelectableMapping aggregateColumn,
SelectableMapping[] columns,
TypeConfiguration typeConfiguration) {
return new RootStructWriteExpression( aggregateColumn, columns, typeConfiguration );
}
private static String determineTypeName(SelectableMapping column, TypeConfiguration typeConfiguration) { private static String determineTypeName(SelectableMapping column, TypeConfiguration typeConfiguration) {
final String typeName; final String typeName;
if ( column.getColumnDefinition() == null ) { if ( column.getColumnDefinition() == null ) {
@ -470,21 +579,23 @@ public class DB2AggregateSupport extends AggregateSupportImpl {
return Collections.emptyList(); return Collections.emptyList();
} }
final String columnType = aggregateColumn.getTypeName(); final String columnType = aggregateColumn.getTypeName();
final boolean legacyXmlFormatEnabled = aggregateColumn.getValue().getBuildingContext().getBuildingOptions()
.isXmlFormatMapperLegacyFormatEnabled();
// The serialize and deserialize functions, as well as the transform are for supporting struct types in native queries and functions // The serialize and deserialize functions, as well as the transform are for supporting struct types in native queries and functions
var list = new ArrayList<AuxiliaryDatabaseObject>( 3 ); var list = new ArrayList<AuxiliaryDatabaseObject>( 3 );
var serializerSb = new StringBuilder(); var serializerSb = new StringBuilder();
var deserializerSb = new StringBuilder(); var deserializerSb = new StringBuilder();
serializerSb.append( "create function " ).append( columnType ).append( "_serializer(v " ).append( columnType ).append( ") returns xml language sql " ) serializerSb.append( "create function " ).append( columnType ).append( "_serializer(v " ).append( columnType ).append( ") returns xml language sql " )
.append( "return xmlelement(name \"").append( XmlHelper.ROOT_TAG ).append( "\"" ); .append( "return xmlelement(name \"").append( XmlHelper.ROOT_TAG ).append( "\"" );
appendSerializer( aggregatedColumns, serializerSb, "v.." ); appendSerializer( aggregatedColumns, serializerSb, "v..", legacyXmlFormatEnabled );
serializerSb.append( ')' ); serializerSb.append( ')' );
deserializerSb.append( "create function " ).append( columnType ).append( "_deserializer(v xml) returns " ).append( columnType ).append( " language sql " ) deserializerSb.append( "create function " ).append( columnType ).append( "_deserializer(v xml) returns " ).append( columnType ).append( " language sql " )
.append( "return select " ).append( columnType ).append( "()" ); .append( "return select " ).append( columnType ).append( "()" );
appendDeserializerConstructor( aggregatedColumns, deserializerSb, "" ); appendDeserializerConstructor( aggregatedColumns, deserializerSb, "", legacyXmlFormatEnabled );
deserializerSb.append( " from xmltable('$" ).append( XmlHelper.ROOT_TAG ).append( "' passing v as \"" ) deserializerSb.append( " from xmltable('$" ).append( XmlHelper.ROOT_TAG ).append( "' passing v as \"" )
.append( XmlHelper.ROOT_TAG ).append( "\" columns" ); .append( XmlHelper.ROOT_TAG ).append( "\" columns" );
appendDeserializerColumns( aggregatedColumns, deserializerSb, ' ', "" ); appendDeserializerColumns( aggregatedColumns, deserializerSb, ' ', "", legacyXmlFormatEnabled );
deserializerSb.append( ") as t" ); deserializerSb.append( ") as t" );
list.add( list.add(
new NamedAuxiliaryDatabaseObject( new NamedAuxiliaryDatabaseObject(
@ -516,7 +627,7 @@ public class DB2AggregateSupport extends AggregateSupportImpl {
return list; return list;
} }
private static void appendSerializer(List<Column> aggregatedColumns, StringBuilder serializerSb, String prefix) { private static void appendSerializer(List<Column> aggregatedColumns, StringBuilder serializerSb, String prefix, boolean legacyXmlFormatEnabled) {
char sep; char sep;
if ( aggregatedColumns.size() > 1 ) { if ( aggregatedColumns.size() > 1 ) {
serializerSb.append( ",xmlconcat" ); serializerSb.append( ",xmlconcat" );
@ -533,12 +644,26 @@ public class DB2AggregateSupport extends AggregateSupportImpl {
appendSerializer( appendSerializer(
aggregateColumn.getComponent().getAggregatedColumns(), aggregateColumn.getComponent().getAggregatedColumns(),
serializerSb, serializerSb,
prefix + udtColumn.getName() + ".." prefix + udtColumn.getName() + "..",
legacyXmlFormatEnabled
); );
} }
else if ( needsVarcharForBitDataCast( udtColumn.getSqlType() ) ) { else if ( needsVarcharForBitDataCast( udtColumn.getSqlType() ) ) {
serializerSb.append( ",cast(" ).append( prefix ).append( udtColumn.getName() ).append( " as varchar(" ) if ( legacyXmlFormatEnabled ) {
.append( udtColumn.getColumnSize( null, null ).getLength() ).append( ") for bit data)" ); serializerSb.append( ",cast(" ).append( prefix ).append( udtColumn.getName() ).append( " as " );
final long binaryLength = udtColumn.getColumnSize( null, null ).getLength();
// Legacy is Base64 encoded which is 4/3 bigger
final long varcharLength = ( binaryLength << 2 ) / 3;
if ( varcharLength < 32_672L ) {
serializerSb.append( "varchar(" ).append( varcharLength ).append( ") for bit data)" );
}
else {
serializerSb.append( "clob)" );
}
}
else {
serializerSb.append( ",hex(" ).append( prefix ).append( udtColumn.getName() ).append( ")" );
}
} }
else { else {
serializerSb.append( ',' ).append( prefix ).append( udtColumn.getName() ); serializerSb.append( ',' ).append( prefix ).append( udtColumn.getName() );
@ -554,7 +679,8 @@ public class DB2AggregateSupport extends AggregateSupportImpl {
private static void appendDeserializerConstructor( private static void appendDeserializerConstructor(
List<Column> aggregatedColumns, List<Column> aggregatedColumns,
StringBuilder deserializerSb, StringBuilder deserializerSb,
String prefix) { String prefix,
boolean legacyXmlFormatEnabled) {
for ( Column udtColumn : aggregatedColumns ) { for ( Column udtColumn : aggregatedColumns ) {
deserializerSb.append( ".." ).append( udtColumn.getName() ).append( '(' ); deserializerSb.append( ".." ).append( udtColumn.getName() ).append( '(' );
if ( udtColumn.getSqlTypeCode() == STRUCT ) { if ( udtColumn.getSqlTypeCode() == STRUCT ) {
@ -563,14 +689,21 @@ public class DB2AggregateSupport extends AggregateSupportImpl {
appendDeserializerConstructor( appendDeserializerConstructor(
aggregateColumn.getComponent().getAggregatedColumns(), aggregateColumn.getComponent().getAggregatedColumns(),
deserializerSb, deserializerSb,
udtColumn.getName() + "_" udtColumn.getName() + "_",
legacyXmlFormatEnabled
); );
deserializerSb.append( ')' ); deserializerSb.append( ')' );
} }
else if ( needsVarcharForBitDataCast( udtColumn.getSqlType() ) ) { else if ( needsVarcharForBitDataCast( udtColumn.getSqlType() ) ) {
if ( legacyXmlFormatEnabled ) {
deserializerSb.append( "cast(t." ).append( prefix ).append( udtColumn.getName() ).append( " as " ) deserializerSb.append( "cast(t." ).append( prefix ).append( udtColumn.getName() ).append( " as " )
.append( udtColumn.getSqlType() ).append( "))" ); .append( udtColumn.getSqlType() ).append( "))" );
} }
else {
deserializerSb.append( "cast(hextoraw(t." ).append( prefix ).append( udtColumn.getName() ).append( ") as " )
.append( udtColumn.getSqlType() ).append( "))" );
}
}
else { else {
deserializerSb.append( "t." ).append( prefix ).append( udtColumn.getName() ).append( ')' ); deserializerSb.append( "t." ).append( prefix ).append( udtColumn.getName() ).append( ')' );
} }
@ -581,7 +714,8 @@ public class DB2AggregateSupport extends AggregateSupportImpl {
List<Column> aggregatedColumns, List<Column> aggregatedColumns,
StringBuilder deserializerSb, StringBuilder deserializerSb,
char sep, char sep,
String prefix) { String prefix,
boolean legacyXmlFormatEnabled) {
for ( Column udtColumn : aggregatedColumns ) { for ( Column udtColumn : aggregatedColumns ) {
if ( udtColumn.getSqlTypeCode() == STRUCT ) { if ( udtColumn.getSqlTypeCode() == STRUCT ) {
final AggregateColumn aggregateColumn = (AggregateColumn) udtColumn; final AggregateColumn aggregateColumn = (AggregateColumn) udtColumn;
@ -589,15 +723,29 @@ public class DB2AggregateSupport extends AggregateSupportImpl {
aggregateColumn.getComponent().getAggregatedColumns(), aggregateColumn.getComponent().getAggregatedColumns(),
deserializerSb, deserializerSb,
sep, sep,
udtColumn.getName() + "_" udtColumn.getName() + "_",
legacyXmlFormatEnabled
); );
} }
else { else {
deserializerSb.append( sep ); deserializerSb.append( sep );
deserializerSb.append( prefix ).append( udtColumn.getName() ).append( ' ' ); deserializerSb.append( prefix ).append( udtColumn.getName() ).append( ' ' );
if ( needsVarcharForBitDataCast( udtColumn.getSqlType() ) ) { if ( needsVarcharForBitDataCast( udtColumn.getSqlType() ) ) {
deserializerSb.append( "varchar(" ) final long binaryLength = udtColumn.getColumnSize( null, null ).getLength();
.append( udtColumn.getColumnSize( null, null ).getLength() ).append( ") for bit data" ); final long varcharLength;
if ( legacyXmlFormatEnabled ) {
// Legacy is Base64 encoded which is 4/3 bigger
varcharLength = ( binaryLength << 2 ) / 3;
}
else {
varcharLength = binaryLength << 1;
}
if ( varcharLength < 32_672L ) {
deserializerSb.append( "varchar(" ).append( varcharLength ).append( ") for bit data" );
}
else {
deserializerSb.append( "clob" );
}
} }
else { else {
deserializerSb.append( udtColumn.getSqlType() ); deserializerSb.append( udtColumn.getSqlType() );
@ -659,7 +807,7 @@ public class DB2AggregateSupport extends AggregateSupportImpl {
if ( jsonWriteExpression == null ) { if ( jsonWriteExpression == null ) {
subExpressions.put( subExpressions.put(
selectableMapping.getSelectableName(), selectableMapping.getSelectableName(),
new PassThroughExpression( selectableMapping ) new PassThroughJsonWriteExpression( selectableMapping )
); );
} }
else if ( jsonWriteExpression instanceof AggregateJsonWriteExpression writeExpression ) { else if ( jsonWriteExpression instanceof AggregateJsonWriteExpression writeExpression ) {
@ -760,11 +908,11 @@ public class DB2AggregateSupport extends AggregateSupportImpl {
} }
} }
private static class PassThroughExpression implements JsonWriteExpression { private static class PassThroughJsonWriteExpression implements JsonWriteExpression {
private final SelectableMapping selectableMapping; private final SelectableMapping selectableMapping;
PassThroughExpression(SelectableMapping selectableMapping) { PassThroughJsonWriteExpression(SelectableMapping selectableMapping) {
this.selectableMapping = selectableMapping; this.selectableMapping = selectableMapping;
} }
@ -781,4 +929,158 @@ public class DB2AggregateSupport extends AggregateSupportImpl {
} }
} }
private static class RootXmlWriteExpression implements WriteExpressionRenderer {
private final SelectableMapping aggregateColumn;
private final SelectableMapping[] columns;
RootXmlWriteExpression(SelectableMapping aggregateColumn, SelectableMapping[] columns) {
this.aggregateColumn = aggregateColumn;
this.columns = columns;
}
@Override
public void render(
SqlAppender sqlAppender,
SqlAstTranslator<?> translator,
AggregateColumnWriteExpression aggregateColumnWriteExpression,
String qualifier) {
sqlAppender.append( "xmldocument(xmlquery('transform copy $d-out:=if(empty($d-in)) then <" );
sqlAppender.append( XmlHelper.ROOT_TAG );
sqlAppender.append( "/> else $d-in/" );
sqlAppender.append( XmlHelper.ROOT_TAG );
sqlAppender.append( " modify " );
char separator = '(';
for ( SelectableMapping column : columns ) {
final SelectablePath selectablePath = column.getSelectablePath();
final String tagXPath = columnXPath( selectablePath );
final String columnVariable = columnVariable( selectablePath );
sqlAppender.append( separator );
sqlAppender.append( "if(empty($" );
sqlAppender.append( columnVariable );
sqlAppender.append( ")) then do delete $d-out" );
sqlAppender.append( tagXPath );
sqlAppender.append( " else if(empty($d-out" );
sqlAppender.append( tagXPath );
sqlAppender.append( ")) then" );
SelectablePath parentPath = selectablePath.getParent();
assert parentPath != null;
renderParentInserts( sqlAppender, parentPath, "{$" + columnVariable + "}" );
sqlAppender.append( " do insert $" );
sqlAppender.append( columnVariable );
sqlAppender.append( " into $d-out" );
sqlAppender.append( columnXPath( selectablePath.getParent() ) );
sqlAppender.append( " else do replace $d-out" );
sqlAppender.append( tagXPath );
sqlAppender.append( " with $" );
sqlAppender.append( columnVariable );
separator = ',';
}
sqlAppender.append( ") return <" );
sqlAppender.append( XmlHelper.ROOT_TAG );
sqlAppender.append( ">{$d-out/*}</" );
sqlAppender.append( XmlHelper.ROOT_TAG );
sqlAppender.append( ">' passing " );
if ( qualifier != null && !qualifier.isBlank() ) {
sqlAppender.append( qualifier );
sqlAppender.append( '.' );
}
sqlAppender.append( aggregateColumn.getSelectionExpression() );
sqlAppender.append( " as \"d-in\"" );
for ( SelectableMapping column : columns ) {
sqlAppender.append( ",xmlelement(name " );
sqlAppender.appendDoubleQuoteEscapedString( column.getSelectableName() );
sqlAppender.append( ',' );
appendColumn(
sqlAppender,
column,
xmlCustomWriteExpression( column.getCustomWriteExpression(), column.getJdbcMapping() ),
translator,
aggregateColumnWriteExpression
);
sqlAppender.append( " option null on null) as " );
sqlAppender.appendDoubleQuoteEscapedString( columnVariable( column.getSelectablePath() ) );
}
sqlAppender.append( "))" );
}
private void renderParentInserts(SqlAppender sqlAppender, SelectablePath parentPath, String parentContent) {
if ( !parentPath.isRoot() ) {
final String newParentContent = "<" + parentPath.getSelectableName() + ">" + parentContent + "</" + parentPath.getSelectableName() + ">";
final SelectablePath grandParentPath = parentPath.getParent();
assert grandParentPath != null;
sqlAppender.append( " if(empty($d-out" );
sqlAppender.append( columnXPath( parentPath ) );
sqlAppender.append( ")) then" );
renderParentInserts( sqlAppender, grandParentPath, newParentContent );
sqlAppender.append( " do insert " );
sqlAppender.append( newParentContent );
sqlAppender.append( " into $d-out" );
sqlAppender.append( columnXPath( grandParentPath ) );
sqlAppender.append( " else" );
}
}
private String columnXPath(SelectablePath selectablePath) {
final SelectablePath[] parts = selectablePath.getParts();
final StringBuilder xpath = new StringBuilder();
for ( int i = 1; i < parts.length; i++ ) {
xpath.append( '/' );
xpath.append( parts[i].getSelectableName() );
}
return xpath.toString();
}
private String columnVariable(SelectablePath selectablePath) {
final SelectablePath[] parts = selectablePath.getParts();
final StringBuilder variable = new StringBuilder();
for ( int i = 1; i < parts.length; i++ ) {
variable.append( parts[i].getSelectableName() );
variable.append( '-' );
}
variable.append( "in" );
return variable.toString();
}
private void appendColumn(
SqlAppender sb,
SelectableMapping selectableMapping,
String customWriteExpression,
SqlAstTranslator<?> translator,
AggregateColumnWriteExpression expression) {
final String customWriteExpressionStart;
final String customWriteExpressionEnd;
if ( customWriteExpression.equals( "?" ) ) {
customWriteExpressionStart = "";
customWriteExpressionEnd = "";
}
else {
final String[] parts = StringHelper.split( "?", customWriteExpression );
assert parts.length == 2;
customWriteExpressionStart = parts[0];
customWriteExpressionEnd = parts[1];
}
final boolean isArray = selectableMapping.getJdbcMapping().getJdbcType().getDefaultSqlTypeCode() == XML_ARRAY;
if ( isArray ) {
sb.append( "xmlquery('$d/*/*' passing " );
}
sb.append( customWriteExpressionStart );
// 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( customWriteExpressionEnd );
if ( isArray ) {
sb.append( " as \"d\")" );
}
}
}
} }

View File

@ -207,7 +207,7 @@ public class StandardTableExporter implements Exporter<Table> {
return false; return false;
} }
private void applyAggregateColumnCheck(StringBuilder buf, AggregateColumn aggregateColumn) { protected void applyAggregateColumnCheck(StringBuilder buf, AggregateColumn aggregateColumn) {
final AggregateSupport aggregateSupport = dialect.getAggregateSupport(); final AggregateSupport aggregateSupport = dialect.getAggregateSupport();
final int checkStart = buf.length(); final int checkStart = buf.length();
buf.append( ", check (" ); buf.append( ", check (" );

View File

@ -304,6 +304,18 @@ public class NestedXmlEmbeddableTest extends BaseSessionFactoryFunctionalTest {
); );
} }
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsXmlComponentUpdate.class)
public void testUpdateAggregateMemberOnNestedNull() {
sessionFactoryScope().inTransaction(
entityManager -> {
entityManager.createMutationQuery( "update XmlHolder b set b.theXml.simpleEmbeddable.doubleNested = null" ).executeUpdate();
entityManager.createMutationQuery( "update XmlHolder b set b.theXml.simpleEmbeddable.doubleNested.theNested.theLeaf.stringField = 'Abc'" ).executeUpdate();
assertEquals( "Abc", entityManager.find( XmlHolder.class, 1L ).getTheXml().simpleEmbeddable.doubleNested.theNested.theLeaf.stringField );
}
);
}
@Test @Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsXmlComponentUpdate.class) @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsXmlComponentUpdate.class)
public void testUpdateMultipleAggregateMembers() { public void testUpdateMultipleAggregateMembers() {