From f7c5ff28f0a5d20e4f12118fcd56d0d698b691e7 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Thu, 31 Oct 2024 18:05:45 +0100 Subject: [PATCH] HHH-18792 Add JSON aggregate support for H2 --- .../java/org/hibernate/dialect/H2Dialect.java | 7 + .../dialect/aggregate/H2AggregateSupport.java | 356 ++++++++++++++++++ .../function/json/H2JsonQueryFunction.java | 39 +- .../function/json/H2JsonTableFunction.java | 40 +- .../function/json/H2JsonValueFunction.java | 46 ++- .../type/descriptor/jdbc/EnumJdbcType.java | 10 + 6 files changed, 466 insertions(+), 32 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/aggregate/H2AggregateSupport.java diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index f380c66a72..7774051fcd 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -19,6 +19,8 @@ import org.hibernate.PessimisticLockException; import org.hibernate.QueryTimeoutException; import org.hibernate.boot.model.FunctionContributions; import org.hibernate.boot.model.TypeContributions; +import org.hibernate.dialect.aggregate.AggregateSupport; +import org.hibernate.dialect.aggregate.H2AggregateSupport; import org.hibernate.dialect.function.CommonFunctionFactory; import org.hibernate.dialect.identity.H2FinalTableIdentityColumnSupport; import org.hibernate.dialect.identity.IdentityColumnSupport; @@ -250,6 +252,11 @@ public class H2Dialect extends Dialect { jdbcTypeRegistry.addDescriptor( OrdinalEnumJdbcType.INSTANCE ); } + @Override + public AggregateSupport getAggregateSupport() { + return H2AggregateSupport.valueOf( this ); + } + @Override public int getDefaultStatementBatchSize() { return 15; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/H2AggregateSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/H2AggregateSupport.java new file mode 100644 index 0000000000..001b01e0c1 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/H2AggregateSupport.java @@ -0,0 +1,356 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.aggregate; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.hibernate.dialect.Dialect; +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; +import org.hibernate.metamodel.mapping.SqlTypedMapping; +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.spi.TypeConfiguration; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import static org.hibernate.type.SqlTypes.ARRAY; +import static org.hibernate.type.SqlTypes.BINARY; +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.VARBINARY; + +public class H2AggregateSupport extends AggregateSupportImpl { + + private static final AggregateSupport INSTANCE = new H2AggregateSupport(); + + public static @Nullable AggregateSupport valueOf(Dialect dialect) { + return dialect.getVersion().isSameOrAfter( 2, 2, 220 ) + ? H2AggregateSupport.INSTANCE + : null; + } + + @Override + public String aggregateComponentCustomReadExpression( + String template, + String placeholder, + String aggregateParentReadExpression, + String columnExpression, + int aggregateColumnTypeCode, + SqlTypedMapping column) { + switch ( aggregateColumnTypeCode ) { + case JSON_ARRAY: + case JSON: + switch ( column.getJdbcMapping().getJdbcType().getDefaultSqlTypeCode() ) { + case JSON: + case JSON_ARRAY: + return template.replace( + placeholder, + "(" + aggregateParentReadExpression + ").\"" + columnExpression + "\"" + ); + case BINARY: + case VARBINARY: + case LONG32VARBINARY: + // We encode binary data as hex, so we have to decode here + return template.replace( + placeholder, + hexDecodeExpression( queryExpression( "(" + aggregateParentReadExpression + ").\"" + columnExpression + "\"" ), column.getColumnDefinition() ) + ); + case ARRAY: + final BasicPluralType pluralType = (BasicPluralType) column.getJdbcMapping(); + final String elementTypeName = getElementTypeName( column.getColumnDefinition() ); + switch ( pluralType.getElementType().getJdbcType().getDefaultSqlTypeCode() ) { + case BINARY: + case VARBINARY: + case LONG32VARBINARY: + // We encode binary data as hex, so we have to decode here + return template.replace( + placeholder, + "(select array_agg(" + hexDecodeExpression( queryExpression( "(" + aggregateParentReadExpression + ").\"" + columnExpression + "\"[i.x]" ), elementTypeName ) + ") from system_range(1,10000) i where i.x<=coalesce(array_length((" + aggregateParentReadExpression + ").\"" + columnExpression + "\"),0))" + ); + default: + return template.replace( + placeholder, + "(select array_agg(" + valueExpression( "(" + aggregateParentReadExpression + ").\"" + columnExpression + "\"[i.x]", elementTypeName ) + ") from system_range(1,10000) i where i.x<=coalesce(array_length((" + aggregateParentReadExpression + ").\"" + columnExpression + "\"),0))" + ); + } + default: + return template.replace( + placeholder, + columnExpression( aggregateParentReadExpression, columnExpression, column.getColumnDefinition() ) + ); + } + } + throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateColumnTypeCode ); + } + + private static String getElementTypeName(String arrayTypeName) { + final String elementTypeName = arrayTypeName.substring( 0, arrayTypeName.lastIndexOf( " array" ) ); + // Doing array_agg on clob produces funky results + return elementTypeName.equals( "clob" ) ? "varchar" : elementTypeName; + } + + private static String columnExpression(String aggregateParentReadExpression, String columnExpression, String columnType) { + return valueExpression( "(" + aggregateParentReadExpression + ").\"" + columnExpression + "\"", columnType ); + } + + private static String hexDecodeExpression(String valueExpression, String columnType) { + return "cast(hextoraw(regexp_replace(" + valueExpression + ",'([0-9a-f][0-9a-f])','00$1')) as " + columnType + ")"; + } + + private static String valueExpression(String valueExpression, String columnType) { + return "cast(" + queryExpression( valueExpression ) + " as " + columnType + ')'; + } + + private static String queryExpression(String valueExpression) { + // First we produce a SQL null if we see a JSON null + // Next, we replace quotes that surround the value + // Finally, we undo escaping that was done to a string + return "stringdecode(regexp_replace(nullif(" + valueExpression + ",JSON'null'),'^\"(.*)\"$','$1'))"; + } + + private static String jsonCustomWriteExpression(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 "rawtohex(" + customWriteExpression + ")"; + 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 "(select array_agg(rawtohex(t.c1)) from unnest(" + customWriteExpression + ") t)"; + default: + return customWriteExpression; + } + default: + return customWriteExpression; + } + } + + @Override + public String aggregateComponentAssignmentExpression( + String aggregateParentAssignmentExpression, + String columnExpression, + int aggregateColumnTypeCode, + Column column) { + switch ( aggregateColumnTypeCode ) { + case JSON: + case JSON_ARRAY: + // For JSON we always have to replace the whole object + return aggregateParentAssignmentExpression; + } + throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateColumnTypeCode ); + } + + @Override + public boolean requiresAggregateCustomWriteExpressionRenderer(int aggregateSqlTypeCode) { + switch ( aggregateSqlTypeCode ) { + case JSON: + return true; + } + return false; + } + + @Override + public WriteExpressionRenderer aggregateCustomWriteExpressionRenderer( + SelectableMapping aggregateColumn, + SelectableMapping[] columnsToUpdate, + TypeConfiguration typeConfiguration) { + final int aggregateSqlTypeCode = aggregateColumn.getJdbcMapping().getJdbcType().getDefaultSqlTypeCode(); + switch ( aggregateSqlTypeCode ) { + case JSON: + return jsonAggregateColumnWriter( 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, + String path, + SqlAstTranslator translator, + AggregateColumnWriteExpression expression); + } + private static class AggregateJsonWriteExpression implements JsonWriteExpression { + private final LinkedHashMap subExpressions = new LinkedHashMap<>(); + + protected void initializeSubExpressions(SelectableMapping aggregateColumn, SelectableMapping[] columns) { + for ( SelectableMapping column : columns ) { + final SelectablePath selectablePath = column.getSelectablePath(); + final SelectablePath[] parts = selectablePath.getParts(); + AggregateJsonWriteExpression currentAggregate = this; + for ( int i = 1; i < parts.length - 1; i++ ) { + currentAggregate = (AggregateJsonWriteExpression) currentAggregate.subExpressions.computeIfAbsent( + parts[i].getSelectableName(), + k -> new AggregateJsonWriteExpression() + ); + } + final String customWriteExpression = column.getWriteExpression(); + currentAggregate.subExpressions.put( + parts[parts.length - 1].getSelectableName(), + new BasicJsonWriteExpression( + column, + jsonCustomWriteExpression( 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 JsonWriteExpression jsonWriteExpression = subExpressions.get( selectableMapping.getSelectableName() ); + if ( jsonWriteExpression == null ) { + subExpressions.put( + selectableMapping.getSelectableName(), + new PassThroughExpression( selectableMapping ) + ); + } + else if ( jsonWriteExpression instanceof AggregateJsonWriteExpression writeExpression ) { + writeExpression.passThroughUnsetSubExpressions( selectableMapping ); + } + } + } + + @Override + public void append( + SqlAppender sb, + String path, + SqlAstTranslator translator, + AggregateColumnWriteExpression expression) { + sb.append( "json_object" ); + char separator = '('; + for ( Map.Entry entry : subExpressions.entrySet() ) { + final String column = entry.getKey(); + final JsonWriteExpression value = entry.getValue(); + final String subPath = path + ".\"" + column + "\""; + sb.append( separator ); + if ( value instanceof AggregateJsonWriteExpression ) { + sb.append( '\'' ); + sb.append( column ); + sb.append( "':coalesce(" ); + value.append( sb, subPath, translator, expression ); + sb.append( ",json_object())" ); + } + else { + value.append( sb, subPath, translator, expression ); + } + separator = ','; + } + sb.append( ')' ); + } + } + + private static class RootJsonWriteExpression extends AggregateJsonWriteExpression + implements WriteExpressionRenderer { + private final String path; + + RootJsonWriteExpression(SelectableMapping aggregateColumn, SelectableMapping[] columns) { + this.path = aggregateColumn.getSelectionExpression(); + initializeSubExpressions( aggregateColumn, columns ); + } + + @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, "(" + basePath + ")", translator, aggregateColumnWriteExpression ); + } + } + private static class BasicJsonWriteExpression implements JsonWriteExpression { + + private final SelectableMapping selectableMapping; + private final String customWriteExpressionStart; + private final String customWriteExpressionEnd; + + BasicJsonWriteExpression(SelectableMapping selectableMapping, String customWriteExpression) { + this.selectableMapping = selectableMapping; + if ( customWriteExpression.equals( "?" ) ) { + this.customWriteExpressionStart = ""; + this.customWriteExpressionEnd = ""; + } + else { + final String[] parts = StringHelper.split( "?", customWriteExpression ); + assert parts.length == 2; + this.customWriteExpressionStart = parts[0]; + this.customWriteExpressionEnd = parts[1]; + } + } + + @Override + public void append( + SqlAppender sb, + String path, + SqlAstTranslator translator, + AggregateColumnWriteExpression expression) { + sb.append( '\'' ); + sb.append( selectableMapping.getSelectableName() ); + sb.append( "':" ); + 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 ); + } + } + + private static class PassThroughExpression implements JsonWriteExpression { + + private final SelectableMapping selectableMapping; + + PassThroughExpression(SelectableMapping selectableMapping) { + this.selectableMapping = selectableMapping; + } + + @Override + public void append( + SqlAppender sb, + String path, + SqlAstTranslator translator, + AggregateColumnWriteExpression expression) { + sb.append( '\'' ); + sb.append( selectableMapping.getSelectableName() ); + sb.append( "':" ); + sb.append( path ); + } + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonQueryFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonQueryFunction.java index 298710b30c..2f1f9986fb 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonQueryFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonQueryFunction.java @@ -45,11 +45,20 @@ public class H2JsonQueryFunction extends JsonQueryFunction { arguments.jsonPath(), arguments.passingClause(), arguments.wrapMode(), + arguments.emptyBehavior(), walker ); } - static void appendJsonQuery(SqlAppender sqlAppender, Expression jsonDocument, boolean isJsonType, Expression jsonPathExpression, @Nullable JsonPathPassingClause passingClause, @Nullable JsonQueryWrapMode wrapMode, SqlAstTranslator walker) { + static void appendJsonQuery( + SqlAppender sqlAppender, + Expression jsonDocument, + boolean isJsonType, + Expression jsonPathExpression, + @Nullable JsonPathPassingClause passingClause, + @Nullable JsonQueryWrapMode wrapMode, + @Nullable JsonQueryEmptyBehavior emptyBehavior, + SqlAstTranslator walker) { final String jsonPath; try { jsonPath = walker.getLiteralValue( jsonPathExpression ); @@ -57,16 +66,27 @@ public class H2JsonQueryFunction extends JsonQueryFunction { catch (Exception ex) { throw new QueryException( "H2 json_query only support literal json paths, but got " + jsonPathExpression ); } - appendJsonQuery( sqlAppender, jsonDocument, isJsonType, jsonPath, passingClause, wrapMode, walker ); + appendJsonQuery( sqlAppender, jsonDocument, isJsonType, jsonPath, passingClause, wrapMode, emptyBehavior, walker ); } - static void appendJsonQuery(SqlAppender sqlAppender, Expression jsonDocument, boolean isJsonType, String jsonPath, @Nullable JsonPathPassingClause passingClause, @Nullable JsonQueryWrapMode wrapMode, SqlAstTranslator walker) { + static void appendJsonQuery( + SqlAppender sqlAppender, + Expression jsonDocument, + boolean isJsonType, + String jsonPath, + @Nullable JsonPathPassingClause passingClause, + @Nullable JsonQueryWrapMode wrapMode, + @Nullable JsonQueryEmptyBehavior emptyBehavior, + SqlAstTranslator walker) { + if ( emptyBehavior == JsonQueryEmptyBehavior.EMPTY_ARRAY || emptyBehavior == JsonQueryEmptyBehavior.EMPTY_OBJECT ) { + sqlAppender.appendSql( "coalesce(" ); + } + if ( wrapMode == JsonQueryWrapMode.WITH_WRAPPER ) { sqlAppender.appendSql( "'['||" ); } - sqlAppender.appendSql( "stringdecode(btrim(nullif(" ); - sqlAppender.appendSql( "cast(" ); + sqlAppender.appendSql( "stringdecode(regexp_replace(nullif(" ); H2JsonValueFunction.renderJsonPath( sqlAppender, jsonDocument, @@ -75,10 +95,15 @@ public class H2JsonQueryFunction extends JsonQueryFunction { jsonPath, passingClause ); - sqlAppender.appendSql( " as varchar)" ); - sqlAppender.appendSql( ",'null'),'\"'))"); + sqlAppender.appendSql( ",JSON'null'),'^\"(.*)\"$','$1'))"); if ( wrapMode == JsonQueryWrapMode.WITH_WRAPPER ) { sqlAppender.appendSql( "||']'" ); } + if ( emptyBehavior == JsonQueryEmptyBehavior.EMPTY_ARRAY ) { + sqlAppender.appendSql( ",'[]')" ); + } + else if ( emptyBehavior == JsonQueryEmptyBehavior.EMPTY_OBJECT ) { + sqlAppender.appendSql( ",'{}')" ); + } } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonTableFunction.java index c7b2ff415b..8eadaff581 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonTableFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonTableFunction.java @@ -63,6 +63,7 @@ import java.util.Collections; import java.util.List; import java.util.Set; + /** * H2 json_table function. *

@@ -662,15 +663,26 @@ public class H2JsonTableFunction extends JsonTableFunction { private String castValueExpression(String baseReadExpression, CastTarget castTarget, @Nullable Literal defaultExpression, SqmToSqlAstConverter converter) { final StringBuilder sb = new StringBuilder( baseReadExpression.length() + 200 ); - sb.append( "cast(stringdecode(btrim(nullif(" ); if ( defaultExpression != null ) { sb.append( "coalesce(" ); } + final boolean hexDecoding = H2JsonValueFunction.needsHexDecoding( castTarget.getJdbcMapping() ); sb.append( "cast(" ); + if ( hexDecoding ) { + // We encode binary data as hex, so we have to decode here + sb.append( "hextoraw(regexp_replace(" ); + } + sb.append( "stringdecode(regexp_replace(nullif(" ); sb.append( baseReadExpression ); - sb.append( " as varchar)" ); + sb.append( ",JSON'null'),'^\"(.*)\"$','$1'))" ); + if ( hexDecoding ) { + sb.append( ",'([0-9a-f][0-9a-f])','00$1'))" ); + } + sb.append( " as " ); + sb.append( determineColumnType( castTarget, converter.getCreationContext().getTypeConfiguration() ) ); + sb.append( ')' ); if ( defaultExpression != null ) { - sb.append( ",cast(" ); + sb.append( ',' ); //noinspection unchecked final String sqlLiteral = defaultExpression.getJdbcMapping().getJdbcLiteralFormatter().toJdbcLiteral( defaultExpression.getLiteralValue(), @@ -678,13 +690,8 @@ public class H2JsonTableFunction extends JsonTableFunction { converter.getCreationContext().getSessionFactory().getWrapperOptions() ); sb.append( sqlLiteral ); - sb.append( " as varchar))" ); + sb.append( ')' ); } - sb.append( ",'null'),'\"'))"); - - sb.append( " as " ); - sb.append( determineColumnType( castTarget, converter.getCreationContext().getTypeConfiguration() ) ); - sb.append( ')' ); return sb.toString(); } @@ -703,18 +710,25 @@ public class H2JsonTableFunction extends JsonTableFunction { private String castQueryExpression(String baseReadExpression, JsonQueryEmptyBehavior emptyBehavior, JsonQueryWrapMode wrapMode, SqmToSqlAstConverter converter) { final StringBuilder sb = new StringBuilder( baseReadExpression.length() + 200 ); + if ( emptyBehavior == JsonQueryEmptyBehavior.EMPTY_ARRAY || emptyBehavior == JsonQueryEmptyBehavior.EMPTY_OBJECT ) { + sb.append( "coalesce(" ); + } if ( wrapMode == JsonQueryWrapMode.WITH_WRAPPER ) { sb.append( "'['||" ); } - sb.append( "stringdecode(btrim(nullif(" ); - sb.append( "cast(" ); + sb.append( "stringdecode(regexp_replace(nullif(" ); sb.append( baseReadExpression ); - sb.append( " as varchar)" ); - sb.append( ",'null'),'\"'))"); + sb.append( ",JSON'null'),'^\"(.*)\"$','$1'))"); if ( wrapMode == JsonQueryWrapMode.WITH_WRAPPER ) { sb.append( "||']'" ); } + if ( emptyBehavior == JsonQueryEmptyBehavior.EMPTY_ARRAY ) { + sb.append( ",'[]')" ); + } + else if ( emptyBehavior == JsonQueryEmptyBehavior.EMPTY_OBJECT ) { + sb.append( ",'{}')" ); + } return sb.toString(); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java index 56a7aaec61..c1add5bbae 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java @@ -8,6 +8,7 @@ import java.util.List; import org.hibernate.QueryException; import org.hibernate.internal.util.QuotingHelper; +import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.query.ReturnableType; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; @@ -20,6 +21,10 @@ import org.hibernate.type.spi.TypeConfiguration; import org.checkerframework.checker.nullness.qual.Nullable; +import static org.hibernate.type.SqlTypes.BINARY; +import static org.hibernate.type.SqlTypes.LONG32VARBINARY; +import static org.hibernate.type.SqlTypes.VARBINARY; + /** * H2 json_value function. */ @@ -45,8 +50,20 @@ public class H2JsonValueFunction extends JsonValueFunction { final Expression defaultExpression = arguments.emptyBehavior() == null ? null : arguments.emptyBehavior().getDefaultExpression(); + if ( defaultExpression != null ) { + sqlAppender.appendSql( "coalesce(" ); + } + final boolean hexDecoding; if ( arguments.returningType() != null ) { + hexDecoding = H2JsonValueFunction.needsHexDecoding( arguments.returningType().getJdbcMapping() ); sqlAppender.appendSql( "cast(" ); + if ( hexDecoding ) { + // We encode binary data as hex, so we have to decode here + sqlAppender.appendSql( "hextoraw(regexp_replace(" ); + } + } + else { + hexDecoding = false; } final String jsonPath; try { @@ -56,11 +73,7 @@ public class H2JsonValueFunction extends JsonValueFunction { throw new QueryException( "H2 json_value only support literal json paths, but got " + arguments.jsonPath() ); } - sqlAppender.appendSql( "stringdecode(btrim(nullif(" ); - if ( defaultExpression != null ) { - sqlAppender.appendSql( "coalesce(" ); - } - sqlAppender.appendSql( "cast(" ); + sqlAppender.appendSql( "stringdecode(regexp_replace(nullif(" ); renderJsonPath( sqlAppender, arguments.jsonDocument(), @@ -69,19 +82,28 @@ public class H2JsonValueFunction extends JsonValueFunction { jsonPath, arguments.passingClause() ); - sqlAppender.appendSql( " as varchar)" ); - if ( defaultExpression != null ) { - sqlAppender.appendSql( ",cast(" ); - defaultExpression.accept( walker ); - sqlAppender.appendSql( " as varchar))" ); - } - sqlAppender.appendSql( ",'null'),'\"'))"); + sqlAppender.appendSql( ",JSON'null'),'^\"(.*)\"$','$1'))"); if ( arguments.returningType() != null ) { + if ( hexDecoding ) { + sqlAppender.appendSql( ",'([0-9a-f][0-9a-f])','00$1'))" ); + } sqlAppender.appendSql( " as " ); arguments.returningType().accept( walker ); sqlAppender.appendSql( ')' ); } + if ( defaultExpression != null ) { + sqlAppender.appendSql( ',' ); + defaultExpression.accept( walker ); + sqlAppender.appendSql( ')' ); + } + } + + public static boolean needsHexDecoding(JdbcMapping jdbcMapping) { + return switch ( jdbcMapping.getJdbcType().getDefaultSqlTypeCode() ) { + case BINARY, VARBINARY, LONG32VARBINARY -> true; + default -> false; + }; } public static void renderJsonPath( diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/EnumJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/EnumJdbcType.java index bf65929587..c7f984d381 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/EnumJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/EnumJdbcType.java @@ -54,6 +54,11 @@ public class EnumJdbcType implements JdbcType { return "ENUM"; } + @Override + public Class getPreferredJavaTypeClass(WrapperOptions options) { + return String.class; + } + @Override public ValueBinder getBinder(JavaType javaType) { return new BasicBinder<>( javaType, this ) { @@ -68,6 +73,11 @@ public class EnumJdbcType implements JdbcType { throws SQLException { st.setString( name, getJavaType().unwrap( value, String.class, options ) ); } + + @Override + public Object getBindValue(X value, WrapperOptions options) throws SQLException { + return getJavaType().unwrap( value, String.class, options ); + } }; }