diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index 95fa170e1e..9ab942ac07 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -1122,6 +1122,7 @@ The following functions deal with SQL array types, which are not supported on ev | `array_contains_null()` | Whether an array contains a null | `array_position()` | Determines the position of an element in an array | `array_length()` | Determines the length of an array +| `array_concat()` | Concatenates array with each other in order |=== ===== `array()` @@ -1181,6 +1182,19 @@ include::{array-example-dir-hql}/ArrayLengthTest.java[tags=hql-array-length-exam ---- ==== +[[hql-array-concat-functions]] +===== `array_concat()` + +Concatenates arrays with each other in order. Returns `null` if one of the arguments is `null`. + +[[hql-array-concat-example]] +==== +[source, JAVA, indent=0] +---- +include::{array-example-dir-hql}/ArrayConcatTest.java[tags=hql-array-concat-example] +---- +==== + [[hql-user-defined-functions]] ==== Native and user-defined functions diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java index b4e87c7eeb..89c72fda94 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java @@ -467,6 +467,7 @@ public class CockroachLegacyDialect extends Dialect { functionFactory.arrayContainsNull_array_position(); functionFactory.arrayPosition_postgresql(); functionFactory.arrayLength_cardinality(); + functionFactory.arrayConcat_postgresql(); functionContributions.getFunctionRegistry().register( "trunc", diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java index e219d804a0..3e04ca857d 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java @@ -375,6 +375,7 @@ public class H2LegacyDialect extends Dialect { functionFactory.arrayContains(); functionFactory.arrayContainsNull(); functionFactory.arrayLength_cardinality(); + functionFactory.arrayConcat_operator(); } else { // Use group_concat until 2.x as listagg was buggy diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java index f99bb6705b..c0229feff2 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java @@ -253,6 +253,7 @@ public class HSQLLegacyDialect extends Dialect { functionFactory.arrayContainsNull_hsql(); functionFactory.arrayPosition_hsql(); functionFactory.arrayLength_cardinality(); + functionFactory.arrayConcat_operator(); } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java index e9aed4aa57..12cfe8f085 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java @@ -289,6 +289,7 @@ public class OracleLegacyDialect extends Dialect { functionFactory.arrayContainsNull_oracle(); functionFactory.arrayPosition_oracle(); functionFactory.arrayLength_oracle(); + functionFactory.arrayConcat_oracle(); } @Override 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 2d3dbef4e0..04b4b01ec2 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 @@ -587,6 +587,7 @@ public class PostgreSQLLegacyDialect extends Dialect { functionFactory.arrayContainsNull_array_position(); functionFactory.arrayPosition_postgresql(); functionFactory.arrayLength_cardinality(); + functionFactory.arrayConcat_postgresql(); if ( getVersion().isSameOrAfter( 9, 4 ) ) { functionFactory.makeDateTimeTimestamp(); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java index 74f42b21db..24ac33c208 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -454,6 +454,7 @@ public class CockroachDialect extends Dialect { functionFactory.arrayContainsNull_array_position(); functionFactory.arrayPosition_postgresql(); functionFactory.arrayLength_cardinality(); + functionFactory.arrayConcat_postgresql(); functionContributions.getFunctionRegistry().register( "trunc", 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 08d5754b47..ce5025d846 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -315,6 +315,7 @@ public class H2Dialect extends Dialect { functionFactory.arrayContains(); functionFactory.arrayContainsNull(); functionFactory.arrayLength_cardinality(); + functionFactory.arrayConcat_operator(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java index 0f969be80a..a6cac7e499 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java @@ -193,6 +193,7 @@ public class HSQLDialect extends Dialect { functionFactory.arrayContainsNull_hsql(); functionFactory.arrayPosition_hsql(); functionFactory.arrayLength_cardinality(); + functionFactory.arrayConcat_operator(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleArrayJdbcType.java index 9d5bccd167..018bc7c75f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleArrayJdbcType.java @@ -272,6 +272,48 @@ public class OracleArrayJdbcType extends ArrayJdbcType { false ) ); + database.addAuxiliaryDatabaseObject( + new NamedAuxiliaryDatabaseObject( + arrayTypeName + "_concat", + database.getDefaultNamespace(), + new String[]{ createOrReplaceConcatFunction( arrayTypeName ) }, + new String[] { "drop function " + arrayTypeName + "_concat" }, + emptySet(), + false + ) + ); + } + + protected String createOrReplaceConcatFunction(String arrayTypeName) { + // Since Oracle has no builtin concat function for varrays and doesn't support varargs, + // we have to create a function with a fixed amount of arguments with default that fits "most" cases. + // Let's just use 5 for the time being until someone requests more. + return createOrReplaceConcatFunction( arrayTypeName, 5 ); + } + + protected String createOrReplaceConcatFunction(String arrayTypeName, int maxConcatParams) { + final StringBuilder sb = new StringBuilder(); + sb.append( "create or replace function " ).append( arrayTypeName ).append( "_concat(" ); + sb.append( "arr0 in " ).append( arrayTypeName ).append( ",arr1 in " ).append( arrayTypeName ); + for ( int i = 2; i < maxConcatParams; i++ ) { + sb.append( ",arr" ).append( i ).append( " in " ).append( arrayTypeName ) + .append( " default " ).append( arrayTypeName ).append( "()" ); + } + sb.append( ") return " ).append( arrayTypeName ).append( " deterministic is res " ).append( arrayTypeName ) + .append( "; begin if " ); + String separator = ""; + for ( int i = 0; i < maxConcatParams; i++ ) { + sb.append( separator ).append( "arr" ).append( i ).append( " is null" ); + separator = " or "; + } + sb.append( " then return null; end if; " ); + sb.append( "select * bulk collect into res from (" ); + separator = ""; + for ( int i = 0; i < maxConcatParams; i++ ) { + sb.append( separator ).append( "select * from table(arr" ).append( i ).append( ')' ); + separator = " union all "; + } + return sb.append( "); return res; end;" ).toString(); } private static String getRawTypeName(String typeName) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index 1d8fc0469d..f5baa69373 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -318,6 +318,7 @@ public class OracleDialect extends Dialect { functionFactory.arrayContainsNull_oracle(); functionFactory.arrayPosition_oracle(); functionFactory.arrayLength_oracle(); + functionFactory.arrayConcat_oracle(); } @Override 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 68b321794e..a25f210c42 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -635,6 +635,7 @@ public class PostgreSQLDialect extends Dialect { functionFactory.arrayContainsNull_array_position(); functionFactory.arrayPosition_postgresql(); functionFactory.arrayLength_cardinality(); + functionFactory.arrayConcat_postgresql(); functionFactory.makeDateTimeTimestamp(); // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java index 706925ac60..e0b4382663 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java @@ -16,11 +16,15 @@ import org.hibernate.dialect.function.array.ArrayAggFunction; import org.hibernate.dialect.function.array.ArrayAndElementArgumentTypeResolver; import org.hibernate.dialect.function.array.ArrayAndElementArgumentValidator; import org.hibernate.dialect.function.array.ArrayArgumentValidator; +import org.hibernate.dialect.function.array.ArrayConcatArgumentValidator; +import org.hibernate.dialect.function.array.ArrayConcatFunction; import org.hibernate.dialect.function.array.ArrayConstructorFunction; import org.hibernate.dialect.function.array.ArrayContainsOperatorFunction; import org.hibernate.dialect.function.array.HSQLArrayPositionFunction; +import org.hibernate.dialect.function.array.OracleArrayConcatFunction; import org.hibernate.dialect.function.array.OracleArrayLengthFunction; import org.hibernate.dialect.function.array.OracleArrayPositionFunction; +import org.hibernate.dialect.function.array.PostgreSQLArrayConcatFunction; import org.hibernate.dialect.function.array.PostgreSQLArrayPositionFunction; import org.hibernate.dialect.function.array.CastingArrayConstructorFunction; import org.hibernate.dialect.function.array.OracleArrayAggEmulation; @@ -2739,4 +2743,25 @@ public class CommonFunctionFactory { public void arrayLength_oracle() { functionRegistry.register( "array_length", new OracleArrayLengthFunction( typeConfiguration ) ); } + + /** + * H2 and HSQLDB array_concat() function + */ + public void arrayConcat_operator() { + functionRegistry.register( "array_concat", new ArrayConcatFunction( "", "||", "" ) ); + } + + /** + * CockroachDB and PostgreSQL array_concat() function + */ + public void arrayConcat_postgresql() { + functionRegistry.register( "array_concat", new PostgreSQLArrayConcatFunction() ); + } + + /** + * Oracle array_concat() function + */ + public void arrayConcat_oracle() { + functionRegistry.register( "array_concat", new OracleArrayConcatFunction() ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayArgumentValidator.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayArgumentValidator.java index 3e99d65926..c4c5144d9f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayArgumentValidator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayArgumentValidator.java @@ -41,6 +41,14 @@ public class ArrayArgumentValidator implements ArgumentsValidator { List> arguments, String functionName, TypeConfiguration typeConfiguration) { + return getElementType( arrayIndex, arguments, functionName, typeConfiguration ); + } + + protected BasicType getElementType( + int arrayIndex, + List> arguments, + String functionName, + TypeConfiguration typeConfiguration) { final SqmTypedNode arrayArgument = arguments.get( arrayIndex ); final SqmExpressible arrayType = arrayArgument.getExpressible().getSqmType(); if ( arrayType == null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayConcatArgumentValidator.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayConcatArgumentValidator.java new file mode 100644 index 0000000000..74161f507d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayConcatArgumentValidator.java @@ -0,0 +1,66 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.DomainType; +import org.hibernate.query.sqm.produce.function.ArgumentsValidator; +import org.hibernate.query.sqm.produce.function.FunctionArgumentException; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.type.BasicPluralType; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * A {@link ArgumentsValidator} that validates all arguments are of the same array type. + */ +public class ArrayConcatArgumentValidator implements ArgumentsValidator { + + public static final ArgumentsValidator INSTANCE = new ArrayConcatArgumentValidator(); + + @Override + public void validate( + List> arguments, + String functionName, + TypeConfiguration typeConfiguration) { + BasicPluralType arrayType = null; + for ( int i = 0; i < arguments.size(); i++ ) { + final DomainType sqmType = arguments.get( i ).getExpressible().getSqmType(); + if ( sqmType != null ) { + if ( arrayType == null ) { + if ( !( sqmType instanceof BasicPluralType ) ) { + throw new FunctionArgumentException( + String.format( + "Parameter %d of function '%s()' requires an array type, but argument is of type '%s'", + i, + functionName, + sqmType.getTypeName() + ) + ); + } + arrayType = (BasicPluralType) sqmType; + } + else if ( !arrayType.equals( sqmType ) ) { + throw new FunctionArgumentException( + String.format( + "Parameter %d of function '%s()' requires an array type %s, but argument is of type '%s'", + i, + functionName, + arrayType.getTypeName(), + sqmType.getTypeName() + ) + ); + } + } + } + } + + @Override + public String getSignature() { + return "(ARRAY array0, ARRAY array1[, ARRAY array2, ...])"; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayConcatFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayConcatFunction.java new file mode 100644 index 0000000000..392a9f2f28 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayConcatFunction.java @@ -0,0 +1,56 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; + +/** + * Concatenation function for arrays. + */ +public class ArrayConcatFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + private final String prefix; + private final String separator; + private final String suffix; + + public ArrayConcatFunction(String prefix, String separator, String suffix) { + super( + "array_concat", + StandardArgumentsValidators.composite( + StandardArgumentsValidators.min( 2 ), + ArrayConcatArgumentValidator.INSTANCE + ), + StandardFunctionReturnTypeResolvers.useFirstNonNull(), + StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE + ); + this.prefix = prefix; + this.separator = separator; + this.suffix = suffix; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + SqlAstTranslator walker) { + sqlAppender.append( prefix ); + sqlAstArguments.get( 0 ).accept( walker ); + for ( int i = 1; i < sqlAstArguments.size(); i++ ) { + sqlAppender.append( separator ); + sqlAstArguments.get( i ).accept( walker ); + } + sqlAppender.append( suffix ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArrayConcatFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArrayConcatFunction.java new file mode 100644 index 0000000000..e1ac2d7796 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArrayConcatFunction.java @@ -0,0 +1,44 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +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.Expression; + +/** + * Oracle concatenation function for arrays. + */ +public class OracleArrayConcatFunction extends ArrayConcatFunction { + + public OracleArrayConcatFunction() { + super( "(", ",", ")" ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + SqlAstTranslator walker) { + JdbcMappingContainer expressionType = null; + for ( SqlAstNode sqlAstArgument : sqlAstArguments ) { + expressionType = ( (Expression) sqlAstArgument ).getExpressionType(); + if ( expressionType != null ) { + break; + } + } + + final String arrayTypeName = ArrayTypeHelper.getArrayTypeName( expressionType, walker ); + sqlAppender.append( arrayTypeName ); + sqlAppender.append( "_concat" ); + super.render( sqlAppender, sqlAstArguments, walker ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLArrayConcatFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLArrayConcatFunction.java new file mode 100644 index 0000000000..10e095ad53 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLArrayConcatFunction.java @@ -0,0 +1,42 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; + +/** + * PostgreSQL variant of the function to properly return {@code null} when one of the arguments is null. + */ +public class PostgreSQLArrayConcatFunction extends ArrayConcatFunction { + + public PostgreSQLArrayConcatFunction() { + super( "", "||", "" ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + SqlAstTranslator walker) { + sqlAppender.append( "case when " ); + String separator = ""; + for ( SqlAstNode node : sqlAstArguments ) { + sqlAppender.append( separator ); + node.accept( walker ); + sqlAppender.append( " is not null" ); + separator = " and "; + } + + sqlAppender.append( " then " ); + super.render( sqlAppender, sqlAstArguments, walker ); + sqlAppender.append( " end" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/BasicArrayType.java b/hibernate-core/src/main/java/org/hibernate/type/BasicArrayType.java index a5ac95550d..a2bf4c36a2 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/BasicArrayType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/BasicArrayType.java @@ -6,6 +6,8 @@ */ package org.hibernate.type; +import java.util.Objects; + import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; @@ -51,4 +53,15 @@ public class BasicArrayType //noinspection unchecked return (BasicType) this; } + + @Override + public boolean equals(Object o) { + return o == this || o.getClass() == BasicArrayType.class + && Objects.equals( baseDescriptor, ( (BasicArrayType) o ).baseDescriptor ); + } + + @Override + public int hashCode() { + return baseDescriptor.hashCode(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/BasicCollectionType.java b/hibernate-core/src/main/java/org/hibernate/type/BasicCollectionType.java index f2abed8720..bd40f9e17f 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/BasicCollectionType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/BasicCollectionType.java @@ -7,6 +7,7 @@ package org.hibernate.type; import java.util.Collection; +import java.util.Objects; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.spi.BasicCollectionJavaType; @@ -73,4 +74,15 @@ public class BasicCollectionType, E> //noinspection unchecked return (BasicType) this; } + + @Override + public boolean equals(Object o) { + return o == this || o.getClass() == BasicCollectionType.class + && Objects.equals( baseDescriptor, ( (BasicCollectionType) o ).baseDescriptor ); + } + + @Override + public int hashCode() { + return baseDescriptor.hashCode(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/ConvertedBasicArrayType.java b/hibernate-core/src/main/java/org/hibernate/type/ConvertedBasicArrayType.java index c8b7e307f4..d592694b37 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/ConvertedBasicArrayType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/ConvertedBasicArrayType.java @@ -6,6 +6,8 @@ */ package org.hibernate.type; +import java.util.Objects; + import org.hibernate.type.descriptor.ValueBinder; import org.hibernate.type.descriptor.ValueExtractor; import org.hibernate.type.descriptor.converter.spi.BasicValueConverter; @@ -98,4 +100,18 @@ public class ConvertedBasicArrayType public JdbcLiteralFormatter getJdbcLiteralFormatter() { return jdbcLiteralFormatter; } + + @Override + public boolean equals(Object o) { + return o == this || super.equals( o ) + && o instanceof ConvertedBasicArrayType + && Objects.equals( converter, ( (ConvertedBasicArrayType) o ).converter ); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + converter.hashCode(); + return result; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/ConvertedBasicCollectionType.java b/hibernate-core/src/main/java/org/hibernate/type/ConvertedBasicCollectionType.java index c02e9a04aa..69b862ea85 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/ConvertedBasicCollectionType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/ConvertedBasicCollectionType.java @@ -7,6 +7,7 @@ package org.hibernate.type; import java.util.Collection; +import java.util.Objects; import org.hibernate.type.descriptor.ValueBinder; import org.hibernate.type.descriptor.ValueExtractor; @@ -65,4 +66,18 @@ public class ConvertedBasicCollectionType, E> extends Ba public JdbcLiteralFormatter getJdbcLiteralFormatter() { return jdbcLiteralFormatter; } + + @Override + public boolean equals(Object o) { + return o == this || super.equals( o ) + && o instanceof ConvertedBasicCollectionType + && Objects.equals( converter, ( (ConvertedBasicCollectionType) o ).converter ); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + converter.hashCode(); + return result; + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConcatTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConcatTest.java new file mode 100644 index 0000000000..207a311c33 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConcatTest.java @@ -0,0 +1,90 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.function.array; + +import java.util.List; + +import org.hibernate.cfg.AvailableSettings; + +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.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * @author Christian Beikov + */ +@DomainModel(annotatedClasses = EntityWithArrays.class) +@SessionFactory +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStructuralArrays.class) +// Make sure this stuff runs on a dedicated connection pool, +// otherwise we might run into ORA-21700: object does not exist or is marked for delete +// because the JDBC connection or database session caches something that should have been invalidated +@ServiceRegistry(settings = @Setting(name = AvailableSettings.CONNECTION_PROVIDER, value = "")) +public class ArrayConcatTest { + + @BeforeEach + public void prepareData(SessionFactoryScope scope) { + scope.inTransaction( em -> { + em.persist( new EntityWithArrays( 1L, new String[]{} ) ); + em.persist( new EntityWithArrays( 2L, new String[]{ "abc", null, "def" } ) ); + em.persist( new EntityWithArrays( 3L, null ) ); + } ); + } + + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( em -> { + em.createMutationQuery( "delete from EntityWithArrays" ).executeUpdate(); + } ); + } + + @Test + public void testConcatAppend(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-array-concat-example[] + List results = em.createQuery( "select e.id, array_concat(e.theArray, array('xyz')) from EntityWithArrays e order by e.id", Tuple.class ) + .getResultList(); + //end::hql-array-concat-example[] + assertEquals( 3, results.size() ); + assertEquals( 1L, results.get( 0 ).get( 0 ) ); + assertArrayEquals( new String[]{ "xyz" }, results.get( 0 ).get( 1, String[].class ) ); + assertEquals( 2L, results.get( 1 ).get( 0 ) ); + assertArrayEquals( new String[]{ "abc", null, "def", "xyz" }, results.get( 1 ).get( 1, String[].class ) ); + assertEquals( 3L, results.get( 2 ).get( 0 ) ); + assertNull( results.get( 2 ).get( 1, String[].class ) ); + } ); + } + + @Test + public void testConcatPrepend(SessionFactoryScope scope) { + scope.inSession( em -> { + List results = em.createQuery( "select e.id, array_concat(array('xyz'), e.theArray) from EntityWithArrays e order by e.id", Tuple.class ) + .getResultList(); + assertEquals( 3, results.size() ); + assertEquals( 1L, results.get( 0 ).get( 0 ) ); + assertArrayEquals( new String[]{ "xyz" }, results.get( 0 ).get( 1, String[].class ) ); + assertEquals( 2L, results.get( 1 ).get( 0 ) ); + assertArrayEquals( new String[]{ "xyz", "abc", null, "def" }, results.get( 1 ).get( 1, String[].class ) ); + assertEquals( 3L, results.get( 2 ).get( 0 ) ); + assertNull( results.get( 2 ).get( 1, String[].class ) ); + } ); + } + +}