From 3ceb4a3fa497e6c48e999fbd6298e4593f1bea14 Mon Sep 17 00:00:00 2001 From: Yoobin Yoon Date: Sat, 6 Dec 2025 00:30:56 +0900 Subject: [PATCH] HHH-19826 Add array_reverse and array_sort functions Implement array_reverse() and array_sort() with PostgreSQL 18 semantics. Supports PostgreSQL, H2, HSQLDB, Oracle, and CockroachDB with native functions or SQL emulation as appropriate. Signed-off-by: Yoobin Yoon --- .../chapters/query/hql/QueryLanguage.adoc | 42 +++++ .../dialect/CockroachLegacyDialect.java | 3 + .../community/dialect/H2LegacyDialect.java | 3 + .../community/dialect/HSQLLegacyDialect.java | 3 + .../dialect/OracleLegacyDialect.java | 3 + .../dialect/PostgreSQLLegacyDialect.java | 9 + .../hibernate/dialect/CockroachDialect.java | 3 + .../java/org/hibernate/dialect/H2Dialect.java | 3 + .../org/hibernate/dialect/HSQLDialect.java | 3 + .../org/hibernate/dialect/OracleDialect.java | 3 + .../hibernate/dialect/PostgreSQLDialect.java | 9 + .../function/CommonFunctionFactory.java | 92 +++++++++ .../array/AbstractArrayReverseFunction.java | 32 ++++ .../array/AbstractArraySortFunction.java | 35 ++++ .../array/H2ArrayReverseFunction.java | 47 +++++ .../function/array/H2ArraySortFunction.java | 70 +++++++ .../function/array/HSQLArraySortFunction.java | 52 +++++ .../array/OracleArrayReverseFunction.java | 36 ++++ .../array/OracleArraySortFunction.java | 61 ++++++ .../PostgreSQLArrayReverseEmulation.java | 36 ++++ .../array/PostgreSQLArraySortEmulation.java | 56 ++++++ .../type/OracleUserDefinedTypeExporter.java | 63 +++++++ .../criteria/HibernateCriteriaBuilder.java | 97 ++++++++++ .../spi/HibernateCriteriaBuilderDelegate.java | 85 +++++++++ .../org/hibernate/query/sqm/NodeBuilder.java | 37 ++++ .../sqm/internal/SqmCriteriaNodeBuilder.java | 130 +++++++++++++ .../test/function/array/ArrayReverseTest.java | 142 ++++++++++++++ .../test/function/array/ArraySortTest.java | 177 ++++++++++++++++++ .../orm/junit/DialectFeatureChecks.java | 13 ++ 29 files changed, 1345 insertions(+) create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArrayReverseFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArraySortFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArrayReverseFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArraySortFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArraySortFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArrayReverseFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArraySortFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLArrayReverseEmulation.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLArraySortEmulation.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayReverseTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArraySortTest.java 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 ae6daa913311..51bb150442e7 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -1220,6 +1220,8 @@ The following functions deal with SQL array types, which are not supported on ev | <> | Creates a sub-array of the based on lower and upper index | <> | Creates array copy replacing a given element with another | <> | Creates array copy trimming the last _N_ elements +| <> | Returns a copy of the array with elements in reverse order +| <> | Returns a sorted copy of the array | <> | Creates array filled with the same element _N_ times | <> | Like `array_fill`, but returns the result as `List` | <> | String representation of array @@ -1596,6 +1598,46 @@ include::{array-example-dir-hql}/ArrayTrimTest.java[tags=hql-array-trim-example] ---- ==== +[[hql-array-reverse-functions]] +===== `array_reverse()` + +Returns a copy of the array with elements in reverse order. Returns `null` if the argument is `null`. + +[[hql-array-reverse-example]] +==== +[source, java, indent=0] +---- +include::{array-example-dir-hql}/ArrayReverseTest.java[tags=hql-array-reverse-example] +---- +==== + +[[hql-array-sort-functions]] +===== `array_sort()` + +Returns a sorted copy of the array. When called with no optional arguments, elements are sorted in ascending order with `null` elements placed last. +The optional second argument allows specifying descending order, and the optional third argument controls the position of `null` elements. +Returns `null` if the first argument is `null`. + +[[hql-array-sort-example]] +==== +[source, java, indent=0] +---- +include::{array-example-dir-hql}/ArraySortTest.java[tags=hql-array-sort-example] +---- +==== + +The second argument controls sort direction: `false` for ascending (default), `true` for descending. +The third argument controls `null` placement: `false` for nulls last, `true` for nulls first. +When the third argument is omitted, it defaults to the value of the second argument. + +[[hql-array-sort-descending-nulls-last-example]] +==== +[source, java, indent=0] +---- +include::{array-example-dir-hql}/ArraySortTest.java[tags=hql-array-sort-descending-nulls-last-example] +---- +==== + [[hql-array-fill-functions]] ===== `array_fill()` and `array_fill_list()` 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 83b691ec7557..62eafb4a1c95 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 @@ -141,6 +141,7 @@ * A {@linkplain Dialect SQL dialect} for CockroachDB. * * @author Gavin King + * @author Yoobin Yoon */ public class CockroachLegacyDialect extends Dialect { @@ -485,6 +486,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arraySlice_operator(); functionFactory.arrayReplace(); functionFactory.arrayTrim_unnest(); + functionFactory.arrayReverse_unnest(); + functionFactory.arraySort_unnest(); functionFactory.arrayFill_cockroachdb(); functionFactory.arrayToString_postgresql(); 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 8df31e5d4d5d..5858a24a105d 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 @@ -128,6 +128,7 @@ * * @author Thomas Mueller * @author Jürgen Kreitler + * @author Yoobin Yoon */ public class H2LegacyDialect extends Dialect { @@ -409,6 +410,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arraySlice(); functionFactory.arrayReplace_h2( getMaximumArraySize() ); functionFactory.arrayTrim_trim_array(); + functionFactory.arrayReverse_h2( getMaximumArraySize() ); + functionFactory.arraySort_h2( getMaximumArraySize() ); functionFactory.arrayFill_h2(); functionFactory.arrayToString_h2( getMaximumArraySize() ); 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 6b53fe514060..c55d6f81a2d0 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 @@ -95,6 +95,7 @@ * @author Christoph Sturm * @author Phillip Baird * @author Fred Toussi + * @author Yoobin Yoon */ public class HSQLLegacyDialect extends Dialect { @@ -269,6 +270,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arraySlice_unnest(); functionFactory.arrayReplace_unnest(); functionFactory.arrayTrim_trim_array(); + functionFactory.arrayReverse_unnest(); + functionFactory.arraySort_hsql(); functionFactory.arrayFill_hsql(); functionFactory.arrayToString_hsql(); 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 c7a0a2d09167..becb07ed5cd9 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 @@ -170,6 +170,7 @@ * @author Steve Ebersole * @author Gavin King * @author Loïc Lefèvre + * @author Yoobin Yoon */ public class OracleLegacyDialect extends Dialect { @@ -384,6 +385,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arraySlice_oracle(); functionFactory.arrayReplace_oracle(); functionFactory.arrayTrim_oracle(); + functionFactory.arrayReverse_oracle(); + functionFactory.arraySort_oracle(); functionFactory.arrayFill_oracle(); functionFactory.arrayToString_oracle(); 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 9d29ba01e9c3..ac94ad655ca7 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 @@ -171,6 +171,7 @@ * A {@linkplain Dialect SQL dialect} for PostgreSQL 8 and above. * * @author Gavin King + * @author Yoobin Yoon */ public class PostgreSQLLegacyDialect extends Dialect { @@ -668,6 +669,14 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio else { functionFactory.arrayTrim_unnest(); } + if ( getVersion().isSameOrAfter( 18 ) ) { + functionFactory.arrayReverse(); + functionFactory.arraySort(); + } + else { + functionFactory.arrayReverse_unnest(); + functionFactory.arraySort_unnest(); + } functionFactory.arrayFill_postgresql(); functionFactory.arrayToString_postgresql(); 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 c40a5cdad6d7..b1ca35b2a220 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -136,6 +136,7 @@ * A {@linkplain Dialect SQL dialect} for CockroachDB 23.1 and above. * * @author Gavin King + * @author Yoobin Yoon */ public class CockroachDialect extends Dialect { @@ -477,6 +478,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arraySlice_operator(); functionFactory.arrayReplace(); functionFactory.arrayTrim_unnest(); + functionFactory.arrayReverse_unnest(); + functionFactory.arraySort_unnest(); functionFactory.arrayFill_cockroachdb(); functionFactory.arrayToString_postgresql(); 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 dffce552b7aa..84977844b6ab 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -126,6 +126,7 @@ * * @author Thomas Mueller * @author Jürgen Kreitler + * @author Yoobin Yoon */ public class H2Dialect extends Dialect { private static final DatabaseVersion MINIMUM_VERSION = DatabaseVersion.make( 2, 1, 214 ); @@ -342,6 +343,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arraySlice(); functionFactory.arrayReplace_h2( getMaximumArraySize() ); functionFactory.arrayTrim_trim_array(); + functionFactory.arrayReverse_h2( getMaximumArraySize() ); + functionFactory.arraySort_h2( getMaximumArraySize() ); functionFactory.arrayFill_h2(); functionFactory.arrayToString_h2( getMaximumArraySize() ); 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 9a636cb29992..08246efdeb17 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java @@ -77,6 +77,7 @@ * @author Christoph Sturm * @author Phillip Baird * @author Fred Toussi + * @author Yoobin Yoon */ public class HSQLDialect extends Dialect { @@ -214,6 +215,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arraySlice_unnest(); functionFactory.arrayReplace_unnest(); functionFactory.arrayTrim_trim_array(); + functionFactory.arrayReverse_unnest(); + functionFactory.arraySort_hsql(); functionFactory.arrayFill_hsql(); functionFactory.arrayToString_hsql(); 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 59ca66444fc5..e95d198e3620 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -179,6 +179,7 @@ * @author Steve Ebersole * @author Gavin King * @author Loïc Lefèvre + * @author Yoobin Yoon */ public class OracleDialect extends Dialect { @@ -397,6 +398,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arraySlice_oracle(); functionFactory.arrayReplace_oracle(); functionFactory.arrayTrim_oracle(); + functionFactory.arrayReverse_oracle(); + functionFactory.arraySort_oracle(); functionFactory.arrayFill_oracle(); functionFactory.arrayToString_oracle(); 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 6621a58cd495..ccfc8a54c5f0 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -167,6 +167,7 @@ * PostgreSQL documentation. * * @author Gavin King + * @author Yoobin Yoon */ public class PostgreSQLDialect extends Dialect { protected final static DatabaseVersion MINIMUM_VERSION = DatabaseVersion.make( 13 ); @@ -628,6 +629,14 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio else { functionFactory.arrayTrim_unnest(); } + if ( getVersion().isSameOrAfter( 18 ) ) { + functionFactory.arrayReverse(); + functionFactory.arraySort(); + } + else { + functionFactory.arrayReverse_unnest(); + functionFactory.arraySort_unnest(); + } functionFactory.arrayFill_postgresql(); functionFactory.arrayToString_postgresql(); 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 d2f944187055..d09033804dea 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 @@ -62,6 +62,7 @@ * * @author Steve Ebersole * @author Gavin King + * @author Yoobin Yoon */ public class CommonFunctionFactory { @@ -3321,6 +3322,97 @@ public void arrayTrim_oracle() { functionRegistry.register( "array_trim", new OracleArrayTrimFunction() ); } + /** + * CockroachDB and PostgreSQL array_reverse() function + */ + public void arrayReverse() { + functionRegistry.namedDescriptorBuilder( "array_reverse" ) + .setArgumentsValidator( + StandardArgumentsValidators.composite( + StandardArgumentsValidators.exactly( 1 ), + ArrayArgumentValidator.DEFAULT_INSTANCE + ) ) + .setReturnTypeResolver( ArrayViaArgumentReturnTypeResolver.DEFAULT_INSTANCE ) + .setArgumentTypeResolver( + StandardFunctionArgumentTypeResolvers.composite( + StandardFunctionArgumentTypeResolvers.invariant( ANY ) + ) ) + .setArgumentListSignature( "(ARRAY array)" ) + .register(); + } + + /** + * array_reverse() emulation for PostgreSQL versions before 18 and HSQLDB + */ + public void arrayReverse_unnest() { + functionRegistry.register( "array_reverse", new PostgreSQLArrayReverseEmulation() ); + } + + /** + * Oracle array_reverse() function + */ + public void arrayReverse_oracle() { + functionRegistry.register( "array_reverse", new OracleArrayReverseFunction() ); + } + + /** + * H2 array_reverse() function + */ + public void arrayReverse_h2(int maximumArraySize) { + functionRegistry.register( "array_reverse", new H2ArrayReverseFunction( maximumArraySize ) ); + } + + /** + * CockroachDB and PostgreSQL array_sort() function + */ + public void arraySort() { + functionRegistry.namedDescriptorBuilder( "array_sort" ) + .setArgumentsValidator( + StandardArgumentsValidators.composite( + StandardArgumentsValidators.between( 1, 3 ), + ArrayArgumentValidator.DEFAULT_INSTANCE + ) + ) + .setReturnTypeResolver( ArrayViaArgumentReturnTypeResolver.DEFAULT_INSTANCE ) + .setArgumentTypeResolver( + StandardFunctionArgumentTypeResolvers.composite( + StandardFunctionArgumentTypeResolvers.invariant( ANY ), + StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE, + StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE + ) + ) + .setArgumentListSignature( "(ARRAY array[, boolean descending[, boolean nulls_first]])" ) + .register(); + } + + /** + * PostgreSQL array_sort() emulation for versions before 18 + */ + public void arraySort_unnest() { + functionRegistry.register( "array_sort", new PostgreSQLArraySortEmulation() ); + } + + /** + * Oracle array_sort() function + */ + public void arraySort_oracle() { + functionRegistry.register( "array_sort", new OracleArraySortFunction() ); + } + + /** + * H2 array_sort() function + */ + public void arraySort_h2(int maximumArraySize) { + functionRegistry.register( "array_sort", new H2ArraySortFunction( maximumArraySize ) ); + } + + /** + * HSQL array_sort() function + */ + public void arraySort_hsql() { + functionRegistry.register( "array_sort", new HSQLArraySortFunction() ); + } + /** * H2 array_fill() function */ diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArrayReverseFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArrayReverseFunction.java new file mode 100644 index 000000000000..7e759ed59fb8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArrayReverseFunction.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ANY; + +/** + * Encapsulates the validator, return type and argument type resolvers for the array_reverse functions. + * Subclasses only have to implement the rendering. + */ +public abstract class AbstractArrayReverseFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public AbstractArrayReverseFunction() { + super( + "array_reverse", + StandardArgumentsValidators.composite( + StandardArgumentsValidators.exactly( 1 ), + ArrayArgumentValidator.DEFAULT_INSTANCE + ), + ArrayViaArgumentReturnTypeResolver.DEFAULT_INSTANCE, + StandardFunctionArgumentTypeResolvers.composite( + StandardFunctionArgumentTypeResolvers.invariant( ANY ) + ) + ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArraySortFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArraySortFunction.java new file mode 100644 index 000000000000..791a9ee8acc7 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArraySortFunction.java @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ANY; + +/** + * Encapsulates the validator, return type and argument type resolvers for the array_sort functions. + * Subclasses only have to implement the rendering. + */ +public abstract class AbstractArraySortFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public AbstractArraySortFunction() { + super( + "array_sort", + StandardArgumentsValidators.composite( + StandardArgumentsValidators.between( 1, 3 ), + ArrayArgumentValidator.DEFAULT_INSTANCE + ), + ArrayViaArgumentReturnTypeResolver.DEFAULT_INSTANCE, + StandardFunctionArgumentTypeResolvers.composite( + StandardFunctionArgumentTypeResolvers.invariant( ANY ), + StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE, + StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE + ) + ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArrayReverseFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArrayReverseFunction.java new file mode 100644 index 000000000000..0572556a0f3a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArrayReverseFunction.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +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; + +/** + * H2 requires a very special emulation, because {@code unnest} is pretty much useless, + * due to https://github.com/h2database/h2database/issues/1815. + * This emulation uses {@code array_get}, {@code cardinality} and {@code system_range} + * functions to achieve array reversal. + */ +public class H2ArrayReverseFunction extends AbstractArrayReverseFunction { + + private final int maximumArraySize; + + public H2ArrayReverseFunction(int maximumArraySize) { + this.maximumArraySize = maximumArraySize; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + + sqlAppender.append( "case when " ); + arrayExpression.accept( walker ); + sqlAppender.append( " is not null then coalesce((select array_agg(array_get(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ",i.idx) order by i.idx desc) from system_range(1," ); + sqlAppender.append( Integer.toString( maximumArraySize ) ); + sqlAppender.append( ") i(idx) where i.idx<=coalesce(cardinality(" ); + arrayExpression.accept( walker ); + sqlAppender.append( "),0)),array[]) end" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArraySortFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArraySortFunction.java new file mode 100644 index 000000000000..6036653a38a2 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArraySortFunction.java @@ -0,0 +1,70 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +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; +import org.hibernate.sql.ast.tree.expression.Literal; + +/** + * H2 requires a very special emulation, because {@code unnest} is pretty much useless, + * due to https://github.com/h2database/h2database/issues/1815. + * This emulation uses {@code array_get}, {@code cardinality} and {@code system_range} + * functions to achieve array sorting. + */ +public class H2ArraySortFunction extends AbstractArraySortFunction { + + private final int maximumArraySize; + + public H2ArraySortFunction(int maximumArraySize) { + this.maximumArraySize = maximumArraySize; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + + final boolean descending = sqlAstArguments.size() > 1 + && sqlAstArguments.get( 1 ) instanceof Literal literal + && literal.getLiteralValue() instanceof Boolean boolValue + ? boolValue + : false; + + final Boolean nullsFirst = sqlAstArguments.size() > 2 + && sqlAstArguments.get( 2 ) instanceof Literal literal + && literal.getLiteralValue() instanceof Boolean boolValue + ? boolValue + : null; + + final boolean actualNullsFirst = nullsFirst != null ? nullsFirst : descending; + + sqlAppender.append( "case when " ); + arrayExpression.accept( walker ); + sqlAppender.append( " is not null then coalesce((select array_agg(array_get(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ",i.idx) order by array_get(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ",i.idx)" ); + + sqlAppender.append( descending + ? ( actualNullsFirst ? " desc nulls first" : " desc nulls last" ) + : ( actualNullsFirst ? " asc nulls first" : " asc nulls last" ) ); + + sqlAppender.append( ") from system_range(1," ); + sqlAppender.append( Integer.toString( maximumArraySize ) ); + sqlAppender.append( ") i(idx) where i.idx<=coalesce(cardinality(" ); + arrayExpression.accept( walker ); + sqlAppender.append( "),0)),array[]) end" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArraySortFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArraySortFunction.java new file mode 100644 index 000000000000..a285ded833c1 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArraySortFunction.java @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +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; +import org.hibernate.sql.ast.tree.expression.Literal; + +/** + * HSQLDB sort_array function. + */ +public class HSQLArraySortFunction extends AbstractArraySortFunction { + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + + sqlAppender.append( "sort_array(" ); + arrayExpression.accept( walker ); + + final boolean descending = sqlAstArguments.size() > 1 + && sqlAstArguments.get( 1 ) instanceof Literal literal + && literal.getLiteralValue() instanceof Boolean boolValue + ? boolValue + : false; + + final Boolean nullsFirst = sqlAstArguments.size() > 2 + && sqlAstArguments.get( 2 ) instanceof Literal literal + && literal.getLiteralValue() instanceof Boolean boolValue + ? boolValue + : null; + + final boolean actualNullsFirst = nullsFirst != null ? nullsFirst : descending; + + sqlAppender.append( descending + ? ( actualNullsFirst ? " desc nulls first" : " desc nulls last" ) + : ( actualNullsFirst ? " asc nulls first" : " asc nulls last" ) ); + sqlAppender.append( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArrayReverseFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArrayReverseFunction.java new file mode 100644 index 000000000000..c00fa75aea00 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArrayReverseFunction.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +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 array_reverse function. + */ +public class OracleArrayReverseFunction extends AbstractArrayReverseFunction { + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + final String arrayTypeName = DdlTypeHelper.getTypeName( + arrayExpression.getExpressionType(), + walker.getSessionFactory().getTypeConfiguration() + ); + sqlAppender.append( arrayTypeName ); + sqlAppender.append( "_reverse(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArraySortFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArraySortFunction.java new file mode 100644 index 000000000000..fc39c0d6c524 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArraySortFunction.java @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +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; +import org.hibernate.sql.ast.tree.expression.Literal; + +/** + * Oracle array_sort function. + */ +public class OracleArraySortFunction extends AbstractArraySortFunction { + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + final String arrayTypeName = DdlTypeHelper.getTypeName( + arrayExpression.getExpressionType(), + walker.getSessionFactory().getTypeConfiguration() + ); + + sqlAppender.append( arrayTypeName ); + sqlAppender.append( "_sort(" ); + arrayExpression.accept( walker ); + + if ( sqlAstArguments.size() > 1 ) { + sqlAppender.append( ',' ); + final SqlAstNode descNode = sqlAstArguments.get( 1 ); + if ( descNode instanceof Literal literal && literal.getLiteralValue() instanceof Boolean boolValue ) { + sqlAppender.append( boolValue ? '1' : '0' ); + } + else { + descNode.accept( walker ); + } + + if ( sqlAstArguments.size() > 2 ) { + sqlAppender.append( ',' ); + final SqlAstNode nullsNode = sqlAstArguments.get( 2 ); + if ( nullsNode instanceof Literal literal && literal.getLiteralValue() instanceof Boolean boolValue ) { + sqlAppender.append( boolValue ? '1' : '0' ); + } + else { + nullsNode.accept( walker ); + } + } + } + + sqlAppender.append( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLArrayReverseEmulation.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLArrayReverseEmulation.java new file mode 100644 index 000000000000..ade49b6dd449 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLArrayReverseEmulation.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; + +/** + * PostgreSQL array_reverse emulation for versions before 18. + * HSQLDB uses the same approach. + */ +public class PostgreSQLArrayReverseEmulation extends AbstractArrayReverseFunction { + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final SqlAstNode arrayExpression = sqlAstArguments.get( 0 ); + + sqlAppender.append( "coalesce((select array_agg(t.val order by t.idx desc) from unnest(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ") with ordinality t(val,idx))," ); + + arrayExpression.accept( walker ); + sqlAppender.append( ")" ); + + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLArraySortEmulation.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLArraySortEmulation.java new file mode 100644 index 000000000000..8878f7666bac --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLArraySortEmulation.java @@ -0,0 +1,56 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +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.Literal; + +/** + * PostgreSQL array_sort emulation for versions before 18. + */ +public class PostgreSQLArraySortEmulation extends AbstractArraySortFunction { + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + + final SqlAstNode arrayExpression = sqlAstArguments.get( 0 ); + + final boolean descending = sqlAstArguments.size() > 1 + && sqlAstArguments.get( 1 ) instanceof Literal literal + && literal.getLiteralValue() instanceof Boolean boolValue + ? boolValue + : false; + + final Boolean nullsFirst = sqlAstArguments.size() > 2 + && sqlAstArguments.get( 2 ) instanceof Literal literal + && literal.getLiteralValue() instanceof Boolean boolValue + ? boolValue + : null; + + final boolean actualNullsFirst = nullsFirst != null ? nullsFirst : descending; + + sqlAppender.append( "coalesce((select array_agg(t.val order by t.val" ); + + sqlAppender.append( descending + ? ( actualNullsFirst ? " desc nulls first" : " desc nulls last" ) + : ( actualNullsFirst ? " asc nulls first" : " asc nulls last" ) ); + + sqlAppender.append( ") from unnest(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ") with ordinality t(val,idx))," ); + + arrayExpression.accept( walker ); + sqlAppender.append( ")" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/type/OracleUserDefinedTypeExporter.java b/hibernate-core/src/main/java/org/hibernate/dialect/type/OracleUserDefinedTypeExporter.java index 4808017f80ce..35f529f18802 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/type/OracleUserDefinedTypeExporter.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/type/OracleUserDefinedTypeExporter.java @@ -30,6 +30,7 @@ /** * @author Christian Beikov + * @author Yoobin Yoon */ public class OracleUserDefinedTypeExporter extends StandardUserDefinedTypeExporter { @@ -220,6 +221,66 @@ public String[] getSqlCreateStrings( "end loop; " + "return res; " + "end;", + "create or replace function " + arrayTypeName + "_reverse(arr in " + arrayTypeName + + ") return " + arrayTypeName + " deterministic is " + + "res " + arrayTypeName + ":=" + arrayTypeName + "(); begin " + + "if arr is null then return null; end if; " + + "for i in reverse 1 .. arr.count loop " + + "res.extend; " + + "res(res.count) := arr(i); " + + "end loop; " + + "return res; " + + "end;", + "create or replace function " + arrayTypeName + "_sort(" + + "arr in " + arrayTypeName + "," + + "p_descending in number default 0," + + "p_nulls_first in number default -1" + + ") return " + arrayTypeName + " deterministic is " + + "res " + arrayTypeName + ":=" + arrayTypeName + "(); " + + "v_count number; " + + "tmp " + elementType + "; " + + "v_nulls_first number; " + + "i_null number; " + + "j_null number; " + + "swap number; " + + "begin " + + "if arr is null then return null; end if; " + + "v_count := arr.count; " + + "if v_count <= 1 then return arr; end if; " + + "for i in 1 .. v_count loop " + + "res.extend; " + + "res(i) := arr(i); " + + "end loop; " + + "if p_nulls_first = -1 then " + + "v_nulls_first := p_descending; " + + "else " + + "v_nulls_first := p_nulls_first; " + + "end if; " + + "for i in 1 .. v_count - 1 loop " + + "for j in i + 1 .. v_count loop " + + "swap := 0; " + + "i_null := case when res(i) is null then 1 else 0 end; " + + "j_null := case when res(j) is null then 1 else 0 end; " + + "if i_null = 1 and j_null = 1 then " + + "swap := 0; " + + "elsif i_null = 1 then " + + "swap := case when v_nulls_first = 0 then 1 else 0 end; " + + "elsif j_null = 1 then " + + "swap := case when v_nulls_first = 1 then 1 else 0 end; " + + "elsif p_descending = 0 then " + + "swap := case when res(j) < res(i) then 1 else 0 end; " + + "else " + + "swap := case when res(j) > res(i) then 1 else 0 end; " + + "end if; " + + "if swap = 1 then " + + "tmp := res(i); " + + "res(i) := res(j); " + + "res(j) := tmp; " + + "end if; " + + "end loop; " + + "end loop; " + + "return res; " + + "end;", "create or replace function " + arrayTypeName + "_fill(elem in " + getRawTypeName( elementType ) + ", elems number) return " + arrayTypeName + " deterministic is " + "res " + arrayTypeName + ":=" + arrayTypeName + "(); begin " + @@ -303,6 +364,8 @@ public String[] getSqlDropStrings(UserDefinedArrayType userDefinedType, Metadata buildDropFunctionSqlString(arrayTypeName + "_slice"), buildDropFunctionSqlString(arrayTypeName + "_replace"), buildDropFunctionSqlString(arrayTypeName + "_trim"), + buildDropFunctionSqlString(arrayTypeName + "_reverse"), + buildDropFunctionSqlString(arrayTypeName + "_sort"), buildDropFunctionSqlString(arrayTypeName + "_fill"), buildDropFunctionSqlString(arrayTypeName + "_positions"), buildDropFunctionSqlString(arrayTypeName + "_to_string"), diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java index d21452932703..7d0a8937a315 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java @@ -90,6 +90,7 @@ * @since 6.0 * * @author Steve Ebersole + * @author Yoobin Yoon */ @Incubating public interface HibernateCriteriaBuilder extends CriteriaBuilder { @@ -2744,6 +2745,54 @@ JpaExpression arrayAgg( @Incubating JpaExpression arrayTrim(Expression arrayExpression, Integer elementCount); + /** + * Reverses the order of elements in an array. + * + * @since 7.2 + */ + @Incubating + JpaExpression arrayReverse(Expression arrayExpression); + + /** + * Sorts the elements of an array in ascending order. + * + * @since 7.2 + */ + @Incubating + JpaExpression arraySort(Expression arrayExpression); + + /** + * Sorts the elements of an array in the specified order. + * + * @since 7.2 + */ + @Incubating + JpaExpression arraySort(Expression arrayExpression, boolean descending); + + /** + * Sorts the elements of an array in the specified order. + * + * @since 7.2 + */ + @Incubating + JpaExpression arraySort(Expression arrayExpression, Expression descendingExpression); + + /** + * Create an expression that sorts the given array with explicit null ordering. + * + * @since 7.2 + */ + @Incubating + JpaExpression arraySort(Expression arrayExpression, boolean descending, boolean nullsFirst); + + /** + * Create an expression that sorts the given array with explicit null ordering. + * + * @since 7.2 + */ + @Incubating + JpaExpression arraySort(Expression arrayExpression, Expression descendingExpression, Expression nullsFirstExpression); + /** * Creates array with the same element N times, as specified by the arguments. * @@ -3403,6 +3452,54 @@ default JpaPredicate arrayOverlapsNullable(T[] array1, Expression array @Incubating > JpaExpression collectionTrim(Expression arrayExpression, Integer elementCount); + /** + * Reverses the order of elements in a basic collection. + * + * @since 7.2 + */ + @Incubating + JpaExpression> collectionReverse(Expression> collectionExpression); + + /** + * Sorts the elements of a basic collection in ascending order. + * + * @since 7.2 + */ + @Incubating + JpaExpression> collectionSort(Expression> collectionExpression); + + /** + * Create an expression that sorts the given collection in specified order. + * + * @since 7.2 + */ + @Incubating + JpaExpression> collectionSort(Expression> collectionExpression, boolean descending); + + /** + * Create an expression that sorts the given collection in specified order. + * + * @since 7.2 + */ + @Incubating + JpaExpression> collectionSort(Expression> collectionExpression, Expression descendingExpression); + + /** + * Create an expression that sorts the given collection with explicit null ordering. + * + * @since 7.2 + */ + @Incubating + JpaExpression> collectionSort(Expression> collectionExpression, boolean descending, boolean nullsFirst); + + /** + * Create an expression that sorts the given collection with explicit null ordering. + * + * @since 7.2 + */ + @Incubating + JpaExpression> collectionSort(Expression> collectionExpression, Expression descendingExpression, Expression nullsFirstExpression); + /** * Creates basic collection with the same element N times, as specified by the arguments. * diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java index 2a67e5ab3625..ea19eef762c4 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java @@ -2557,6 +2557,45 @@ public JpaExpression arrayTrim(Expression arrayExpression, Integer return criteriaBuilder.arrayTrim( arrayExpression, elementCount ); } + @Override + @Incubating + public JpaExpression arrayReverse(Expression arrayExpression) { + return criteriaBuilder.arrayReverse( arrayExpression ); + } + + @Override + @Incubating + public JpaExpression arraySort(Expression arrayExpression) { + return criteriaBuilder.arraySort( arrayExpression ); + } + + @Override + @Incubating + public JpaExpression arraySort(Expression arrayExpression, boolean descending) { + return criteriaBuilder.arraySort( arrayExpression, descending ); + } + + @Override + @Incubating + public JpaExpression arraySort(Expression arrayExpression, Expression descendingExpression) { + return criteriaBuilder.arraySort( arrayExpression, descendingExpression ); + } + + @Override + @Incubating + public JpaExpression arraySort(Expression arrayExpression, boolean descending, boolean nullsFirst) { + return criteriaBuilder.arraySort( arrayExpression, descending, nullsFirst ); + } + + @Override + @Incubating + public JpaExpression arraySort( + Expression arrayExpression, + Expression descendingExpression, + Expression nullsFirstExpression) { + return criteriaBuilder.arraySort( arrayExpression, descendingExpression, nullsFirstExpression ); + } + @Override @Incubating public JpaExpression arrayFill( @@ -3103,6 +3142,52 @@ public > JpaExpression collectionTrim( return criteriaBuilder.collectionTrim( arrayExpression, elementCount ); } + @Override + @Incubating + public JpaExpression> collectionReverse(Expression> collectionExpression) { + return criteriaBuilder.collectionReverse( collectionExpression ); + } + + @Override + @Incubating + public JpaExpression> collectionSort(Expression> collectionExpression) { + return criteriaBuilder.collectionSort( collectionExpression ); + } + + @Override + @Incubating + public JpaExpression> collectionSort( + Expression> collectionExpression, + boolean descending) { + return criteriaBuilder.collectionSort( collectionExpression, descending ); + } + + @Override + @Incubating + public JpaExpression> collectionSort( + Expression> collectionExpression, + Expression descendingExpression) { + return criteriaBuilder.collectionSort( collectionExpression, descendingExpression ); + } + + @Override + @Incubating + public JpaExpression> collectionSort( + Expression> collectionExpression, + boolean descending, + boolean nullsFirst) { + return criteriaBuilder.collectionSort( collectionExpression, descending, nullsFirst ); + } + + @Override + @Incubating + public JpaExpression> collectionSort( + Expression> collectionExpression, + Expression descendingExpression, + Expression nullsFirstExpression) { + return criteriaBuilder.collectionSort( collectionExpression, descendingExpression, nullsFirstExpression ); + } + @Override @Incubating public JpaExpression> collectionFill( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java index bcf7134ccb6d..50ebce3a96aa 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java @@ -84,6 +84,7 @@ * Adapts the JPA CriteriaBuilder to generate SQM nodes. * * @author Steve Ebersole + * @author Yoobin Yoon */ public interface NodeBuilder extends HibernateCriteriaBuilder, SqmCreationContext { default JpaMetamodel getDomainModel() { @@ -246,6 +247,24 @@ SqmExpression arrayAgg( @Override SqmExpression arrayTrim(Expression arrayExpression, Integer elementCount); + @Override + SqmExpression arrayReverse(Expression arrayExpression); + + @Override + SqmExpression arraySort(Expression arrayExpression); + + @Override + SqmExpression arraySort(Expression arrayExpression, boolean descending); + + @Override + SqmExpression arraySort(Expression arrayExpression, Expression descendingExpression); + + @Override + SqmExpression arraySort(Expression arrayExpression, boolean descending, boolean nullsFirst); + + @Override + SqmExpression arraySort(Expression arrayExpression, Expression descendingExpression, Expression nullsFirstExpression); + @Override SqmExpression arrayFill(Expression elementExpression, Expression elementCountExpression); @@ -498,6 +517,24 @@ default SqmPredicate arrayOverlapsNullable(T[] array1, Expression array @Override > SqmExpression collectionTrim(Expression arrayExpression, Integer elementCount); + @Override + SqmExpression> collectionReverse(Expression> collectionExpression); + + @Override + SqmExpression> collectionSort(Expression> collectionExpression); + + @Override + SqmExpression> collectionSort(Expression> collectionExpression, boolean descending); + + @Override + SqmExpression> collectionSort(Expression> collectionExpression, Expression descendingExpression); + + @Override + SqmExpression> collectionSort(Expression> collectionExpression, boolean descending, boolean nullsFirst); + + @Override + SqmExpression> collectionSort(Expression> collectionExpression, Expression descendingExpression, Expression nullsFirstExpression); + @Override SqmExpression> collectionFill(Expression elementExpression, Expression elementCountExpression); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index c9678488ca50..0d56be79686c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java @@ -181,6 +181,7 @@ * using SQM nodes as the JPA Criteria nodes * * @author Steve Ebersole + * @author Yoobin Yoon */ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable { @@ -4692,6 +4693,67 @@ public SqmExpression arrayTrim( ); } + @Override + public SqmExpression arrayReverse(Expression arrayExpression) { + return getFunctionDescriptor( "array_reverse" ).generateSqmExpression( + Collections.singletonList( (SqmExpression) arrayExpression ), + null, + queryEngine + ); + } + + @Override + public SqmExpression arraySort(Expression arrayExpression) { + return getFunctionDescriptor( "array_sort" ).generateSqmExpression( + Collections.singletonList( (SqmExpression) arrayExpression ), + null, + queryEngine + ); + } + + @Override + public SqmExpression arraySort(Expression arrayExpression, boolean descending) { + return getFunctionDescriptor( "array_sort" ).generateSqmExpression( + asList( (SqmExpression) arrayExpression, value( descending ) ), + null, + queryEngine + ); + } + + @Override + public SqmExpression arraySort(Expression arrayExpression, Expression descendingExpression) { + return getFunctionDescriptor( "array_sort" ).generateSqmExpression( + asList( (SqmExpression) arrayExpression, (SqmExpression) descendingExpression ), + null, + queryEngine + ); + } + + @Override + public SqmExpression arraySort(Expression arrayExpression, boolean descending, boolean nullsFirst) { + return getFunctionDescriptor( "array_sort" ).generateSqmExpression( + asList( (SqmExpression) arrayExpression, value( descending ), value( nullsFirst ) ), + null, + queryEngine + ); + } + + @Override + public SqmExpression arraySort( + Expression arrayExpression, + Expression descendingExpression, + Expression nullsFirstExpression) { + return getFunctionDescriptor( "array_sort" ).generateSqmExpression( + asList( + (SqmExpression) arrayExpression, + (SqmExpression) descendingExpression, + (SqmExpression) nullsFirstExpression + ), + null, + queryEngine + ); + } + @Override public SqmExpression arrayTrim(Expression arrayExpression, Integer elementCount) { return getFunctionDescriptor( "array_trim" ).generateSqmExpression( @@ -5378,6 +5440,74 @@ public > SqmExpression collectionTrim( ); } + @Override + public SqmExpression> collectionReverse(Expression> collectionExpression) { + return getFunctionDescriptor( "array_reverse" ).generateSqmExpression( + Collections.singletonList( (SqmExpression) collectionExpression ), + null, + queryEngine + ); + } + + @Override + public SqmExpression> collectionSort(Expression> collectionExpression) { + return getFunctionDescriptor( "array_sort" ).generateSqmExpression( + Collections.singletonList( (SqmExpression) collectionExpression ), + null, + queryEngine + ); + } + + @Override + public SqmExpression> collectionSort( + Expression> collectionExpression, + boolean descending) { + return getFunctionDescriptor( "array_sort" ).generateSqmExpression( + asList( (SqmExpression) collectionExpression, value( descending ) ), + null, + queryEngine + ); + } + + @Override + public SqmExpression> collectionSort( + Expression> collectionExpression, + Expression descendingExpression) { + return getFunctionDescriptor( "array_sort" ).generateSqmExpression( + asList( (SqmExpression) collectionExpression, (SqmExpression) descendingExpression ), + null, + queryEngine + ); + } + + @Override + public SqmExpression> collectionSort( + Expression> collectionExpression, + boolean descending, + boolean nullsFirst) { + return getFunctionDescriptor( "array_sort" ).generateSqmExpression( + asList( (SqmExpression) collectionExpression, value( descending ), value( nullsFirst ) ), + null, + queryEngine + ); + } + + @Override + public SqmExpression> collectionSort( + Expression> collectionExpression, + Expression descendingExpression, + Expression nullsFirstExpression) { + return getFunctionDescriptor( "array_sort" ).generateSqmExpression( + asList( + (SqmExpression) collectionExpression, + (SqmExpression) descendingExpression, + (SqmExpression) nullsFirstExpression + ), + null, + queryEngine + ); + } + @Override public SqmExpression> collectionFill( Expression elementExpression, diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayReverseTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayReverseTest.java new file mode 100644 index 000000000000..2d37fa8fafd4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayReverseTest.java @@ -0,0 +1,142 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.function.array; + +import java.util.Collection; +import java.util.List; + +import org.hibernate.query.criteria.JpaCriteriaQuery; +import org.hibernate.query.criteria.JpaRoot; +import org.hibernate.query.sqm.NodeBuilder; + +import org.hibernate.testing.jdbc.SharedDriverManagerTypeCacheClearingIntegrator; +import org.hibernate.testing.orm.junit.BootstrapServiceRegistry; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +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 Yoobin Yoon + */ +@JiraKey("HHH-19826") +@DomainModel(annotatedClasses = EntityWithArrays.class) +@SessionFactory +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayReverse.class) +@BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) +public class ArrayReverseTest { + + @BeforeEach + public void prepareData(SessionFactoryScope scope) { + scope.inTransaction( em -> { + em.persist( new EntityWithArrays( 1L, new String[] {} ) ); + em.persist( new EntityWithArrays( 2L, new String[] { "abc", "def", null } ) ); + em.persist( new EntityWithArrays( 3L, null ) ); + } ); + } + + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncate(); + } + + @Test + public void testReverse(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-array-reverse-example[] + List results = em.createQuery( + "select e.id, array_reverse(e.theArray) from EntityWithArrays e order by e.id", + Tuple.class + ) + .getResultList(); + //end::hql-array-reverse-example[] + assertEquals( 3, results.size() ); + assertEquals( 1L, results.get( 0 ).get( 0 ) ); + assertArrayEquals( new String[] {}, results.get( 0 ).get( 1, String[].class ) ); + assertEquals( 2L, results.get( 1 ).get( 0 ) ); + assertArrayEquals( new String[] { null, "def", "abc" }, 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 testReverseSingleElement(SessionFactoryScope scope) { + scope.inTransaction( em -> { + em.persist( new EntityWithArrays( 4L, new String[] { "single" } ) ); + } ); + scope.inSession( em -> { + List results = em.createQuery( + "select e.id, array_reverse(e.theArray) from EntityWithArrays e where e.id = 4", + Tuple.class + ) + .getResultList(); + assertEquals( 1, results.size() ); + assertEquals( 4L, results.get( 0 ).get( 0 ) ); + assertArrayEquals( new String[] { "single" }, results.get( 0 ).get( 1, String[].class ) ); + } ); + } + + @Test + public void testReverseDuplicates(SessionFactoryScope scope) { + scope.inTransaction( em -> { + em.persist( new EntityWithArrays( 5L, new String[] { "a", "b", "a", "c", "a" } ) ); + } ); + scope.inSession( em -> { + List results = em.createQuery( + "select e.id, array_reverse(e.theArray) from EntityWithArrays e where e.id = 5", + Tuple.class + ) + .getResultList(); + assertEquals( 1, results.size() ); + assertEquals( 5L, results.get( 0 ).get( 0 ) ); + assertArrayEquals( new String[] { "a", "c", "a", "b", "a" }, results.get( 0 ).get( 1, String[].class ) ); + } ); + } + + @Test + public void testNodeBuilderArray(SessionFactoryScope scope) { + scope.inSession( em -> { + final NodeBuilder cb = (NodeBuilder) em.getCriteriaBuilder(); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaRoot root = cq.from( EntityWithArrays.class ); + cq.multiselect( + root.get( "id" ), + cb.arrayReverse( root.get( "theArray" ) ), + cb.arrayReverse( root.get( "theArray" ) ) + ); + em.createQuery( cq ).getResultList(); + + } ); + } + + @Test + public void testNodeBuilderCollection(SessionFactoryScope scope) { + scope.inSession( em -> { + final NodeBuilder cb = (NodeBuilder) em.getCriteriaBuilder(); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaRoot root = cq.from( EntityWithArrays.class ); + cq.multiselect( + root.get( "id" ), + cb.collectionReverse( root.>get( "theCollection" ) ), + cb.collectionReverse( root.get( "theCollection" ) ) + ); + em.createQuery( cq ).getResultList(); + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArraySortTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArraySortTest.java new file mode 100644 index 000000000000..65af32f85c3d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArraySortTest.java @@ -0,0 +1,177 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.function.array; + +import java.util.Collection; +import java.util.List; + +import org.hibernate.query.criteria.JpaCriteriaQuery; +import org.hibernate.query.criteria.JpaRoot; +import org.hibernate.query.sqm.NodeBuilder; + +import org.hibernate.testing.jdbc.SharedDriverManagerTypeCacheClearingIntegrator; +import org.hibernate.testing.orm.junit.BootstrapServiceRegistry; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +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.hibernate.testing.orm.junit.DialectFeatureChecks.SupportsStructuralArrays; +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 Yoobin Yoon + */ +@JiraKey("HHH-19826") +@DomainModel(annotatedClasses = EntityWithArrays.class) +@SessionFactory +@RequiresDialectFeature(feature = SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArraySort.class) +@BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) +public class ArraySortTest { + + @BeforeEach + public void prepareData(SessionFactoryScope scope) { + scope.inTransaction( em -> { + em.persist( new EntityWithArrays( 1L, new String[] { "banana", "apple", "cherry" } ) ); + em.persist( new EntityWithArrays( 2L, null ) ); + em.persist( new EntityWithArrays( 3L, new String[] {} ) ); + em.persist( new EntityWithArrays( 4L, new String[] { "banana", null, "apple" } ) ); + } ); + } + + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncate(); + } + + @Test + public void testArraySortAscending(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-array-sort-example[] + List results = em.createQuery( + "select array_sort(e.theArray) from EntityWithArrays e order by e.id", + String[].class + ).getResultList(); + //end::hql-array-sort-example[] + assertEquals( 4, results.size() ); + assertArrayEquals( new String[] { "apple", "banana", "cherry" }, results.get( 0 ) ); + assertNull( results.get( 1 ) ); + assertArrayEquals( new String[] {}, results.get( 2 ) ); + assertArrayEquals( new String[] { "apple", "banana", null }, results.get( 3 ) ); + } ); + } + + @Test + public void testArraySortAscendingExplicit(SessionFactoryScope scope) { + scope.inSession( em -> { + List results = em.createQuery( + "select array_sort(e.theArray, false) from EntityWithArrays e order by e.id", + String[].class + ).getResultList(); + assertEquals( 4, results.size() ); + assertArrayEquals( new String[] { "apple", "banana", "cherry" }, results.get( 0 ) ); + assertNull( results.get( 1 ) ); + assertArrayEquals( new String[] {}, results.get( 2 ) ); + assertArrayEquals( new String[] { "apple", "banana", null }, results.get( 3 ) ); + } ); + } + + @Test + public void testArraySortDescending(SessionFactoryScope scope) { + scope.inSession( em -> { + List results = em.createQuery( + "select array_sort(e.theArray, true) from EntityWithArrays e order by e.id", + String[].class + ).getResultList(); + assertEquals( 4, results.size() ); + assertArrayEquals( new String[] { "cherry", "banana", "apple" }, results.get( 0 ) ); + assertNull( results.get( 1 ) ); + assertArrayEquals( new String[] {}, results.get( 2 ) ); + assertArrayEquals( new String[] { null, "banana", "apple" }, results.get( 3 ) ); + } ); + } + + @Test + public void testArraySortDescendingNullsLast(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-array-sort-descending-nulls-last-example[] + List results = em.createQuery( + "select array_sort(e.theArray, true, false) from EntityWithArrays e order by e.id", + String[].class + ).getResultList(); + //end::hql-array-sort-descending-nulls-last-example[] + assertEquals( 4, results.size() ); + assertArrayEquals( new String[] { "cherry", "banana", "apple" }, results.get( 0 ) ); + assertNull( results.get( 1 ) ); + assertArrayEquals( new String[] {}, results.get( 2 ) ); + assertArrayEquals( new String[] { "banana", "apple", null }, results.get( 3 ) ); + } ); + } + + @Test + public void testArraySortAscendingNullsFirst(SessionFactoryScope scope) { + scope.inSession( em -> { + List results = em.createQuery( + "select array_sort(e.theArray, false, true) from EntityWithArrays e order by e.id", + String[].class + ).getResultList(); + assertEquals( 4, results.size() ); + assertArrayEquals( new String[] { "apple", "banana", "cherry" }, results.get( 0 ) ); + assertNull( results.get( 1 ) ); + assertArrayEquals( new String[] {}, results.get( 2 ) ); + assertArrayEquals( new String[] { null, "apple", "banana" }, results.get( 3 ) ); + } ); + } + + @Test + public void testNodeBuilderArray(SessionFactoryScope scope) { + scope.inSession( em -> { + final NodeBuilder cb = (NodeBuilder) em.getCriteriaBuilder(); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaRoot root = cq.from( EntityWithArrays.class ); + cq.multiselect( + root.get( "id" ), + cb.arraySort( root.get( "theArray" ) ), + cb.arraySort( root.get( "theArray" ) ), + cb.arraySort( root.get( "theArray" ), true ), + cb.arraySort( root.get( "theArray" ), cb.literal( false ) ), + cb.arraySort( root.get( "theArray" ), true, false ), + cb.arraySort( root.get( "theArray" ), cb.literal( true ), cb.literal( false ) ) + ); + em.createQuery( cq ).getResultList(); + + } ); + } + + @Test + public void testNodeBuilderCollection(SessionFactoryScope scope) { + scope.inSession( em -> { + final NodeBuilder cb = (NodeBuilder) em.getCriteriaBuilder(); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaRoot root = cq.from( EntityWithArrays.class ); + cq.multiselect( + root.get( "id" ), + cb.collectionSort( root.>get( "theCollection" ) ), + cb.collectionSort( root.get( "theCollection" ) ), + cb.collectionSort( root.>get( "theCollection" ), true ), + cb.collectionSort( root.get( "theCollection" ), cb.literal( false ) ), + cb.collectionSort( root.>get( "theCollection" ), true, false ), + cb.collectionSort( root.get( "theCollection" ), cb.literal( true ), cb.literal( false ) ) + ); + em.createQuery( cq ).getResultList(); + + } ); + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index b6e415441cbd..163d944b3a56 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -121,6 +121,7 @@ * * @author Hardy Ferentschik * @author Steve Ebersole + * @author Yoobin Yoon */ abstract public class DialectFeatureChecks { public static class SupportsSequences implements DialectFeatureCheck { @@ -1107,6 +1108,18 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsArrayReverse implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "array_reverse" ); + } + } + + public static class SupportsArraySort implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "array_sort" ); + } + } + public static class SupportsRegexpLike implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return definesFunction( dialect, "regexp_like" );