Skip to content

Conversation

@yybmion
Copy link

@yybmion yybmion commented Dec 5, 2025

Implements array_reverse() and array_sort() HQL functions.

  • array_reverse(array) - Returns reversed array
  • array_sort(array [, descending [, nulls_first]]) - Returns sorted array
    • Default: ASC, NULLs last
    • Follows PostgreSQL 18 semantics

All database tests pass.


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license
and can be relicensed under the terms of the LGPL v2.1 license in the future at the maintainers' discretion.
For more information on licensing, please check here.


https://hibernate.atlassian.net/browse/HHH-19826

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 <yunyubin54@gmail.com>
@hibernate-github-bot
Copy link

hibernate-github-bot bot commented Dec 5, 2025

Thanks for your pull request!

This pull request appears to follow the contribution rules.

› This message was automatically generated.

Copy link
Member

@beikov beikov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks quite good already. I left a couple of comments.

Comment on lines +3371 to +3374
StandardArgumentsValidators.composite(
StandardArgumentsValidators.between( 1, 3 ),
ArrayArgumentValidator.DEFAULT_INSTANCE
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
StandardArgumentsValidators.composite(
StandardArgumentsValidators.between( 1, 3 ),
ArrayArgumentValidator.DEFAULT_INSTANCE
)
new ArgumentTypesValidator(
StandardArgumentsValidators.composite(
StandardArgumentsValidators.between( 1, 3 ),
ArrayArgumentValidator.DEFAULT_INSTANCE
),
FunctionParameterType.ANY,
FunctionParameterType.BOOLEAN,
FunctionParameterType.BOOLEAN
)

Comment on lines +22 to +25
StandardArgumentsValidators.composite(
StandardArgumentsValidators.between( 1, 3 ),
ArrayArgumentValidator.DEFAULT_INSTANCE
),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
StandardArgumentsValidators.composite(
StandardArgumentsValidators.between( 1, 3 ),
ArrayArgumentValidator.DEFAULT_INSTANCE
),
new ArgumentTypesValidator(
StandardArgumentsValidators.composite(
StandardArgumentsValidators.between( 1, 3 ),
ArrayArgumentValidator.DEFAULT_INSTANCE
),
FunctionParameterType.ANY,
FunctionParameterType.BOOLEAN,
FunctionParameterType.BOOLEAN
)

Comment on lines +29 to +30
StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE,
StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also need to pass the TypeConfiguration in the constructor:

Suggested change
StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE,
StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, FunctionParameterType.BOOLEAN ),
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, FunctionParameterType.BOOLEAN )

Comment on lines +3380 to +3381
StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE,
StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE,
StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, FunctionParameterType.BOOLEAN ),
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, FunctionParameterType.BOOLEAN )

* @since 7.2
*/
@Incubating
<T> JpaExpression<List<T>> collectionReverse(Expression<? extends Collection<T>> collectionExpression);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're going to return something of the same type as passed as argument, this signature seems more correct to me. I'd love to use SequencedCollection as type bound, but that is unfortunately only part of Java 21 and Hibernate ORM 7 still supports Java 17, so we will have to live with Collection, though I don't think there are expectations around sorting/reversing when passing something that isn't ordered. We could do a dynamic check of the argument Java type in the argument type validator though if you like.

Suggested change
<T> JpaExpression<List<T>> collectionReverse(Expression<? extends Collection<T>> collectionExpression);
<C extends Collection<?>> JpaExpression<C> collectionReverse(Expression<C> collectionExpression);

"else " +
"v_nulls_first := p_nulls_first; " +
"end if; " +
"for i in 1 .. v_count - 1 loop " +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


sqlAppender.append( ") from unnest(" );
arrayExpression.accept( walker );
sqlAppender.append( ") with ordinality t(val,idx))," );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this implementation doesn't need ordinality.

Suggested change
sqlAppender.append( ") with ordinality t(val,idx))," );
sqlAppender.append( ") t(val))," );

Comment on lines +28 to +33
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( ")" );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need a coalesce wrapper? I see that you pass an empty array in the other emulations, so I wonder if PostgreSQL fails to infer the type of the array[] expression here? Did you also test with PostgreSQL 17?

Suggested change
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( ")" );
sqlAppender.append( "(select array_agg(t.val order by t.idx desc) from unnest(" );
arrayExpression.accept( walker );
sqlAppender.append( ") with ordinality t(val,idx))" );

Comment on lines +39 to +56
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 );
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 );
}
}
final Expression descNode = (Expression) sqlAstArguments.get( 1 );
sqlAppender.append( "case when " );
descNode.accept( walker );
sqlAppender.append( '=' );
var sessionFactory = walker.getSessionFactory();
castNonNull( descNode.getExpressionType() ).getSingleJdbcMapping().getJdbcLiteralFormatter()
.appendJdbcLiteral( sqlAppender, true, sessionFactory.getJdbcServices().getDialect(), sessionFactory.getWrapperOptions() );
sqlAppender.append( " then 1 else 0 end" );
if ( sqlAstArguments.size() > 2 ) {
sqlAppender.append( ",case when " );
final Expression nullsNode = (Expression) sqlAstArguments.get( 2 );
nullsNode.accept( walker );
sqlAppender.append( '=' );
castNonNull( nullsNode.getExpressionType() ).getSingleJdbcMapping().getJdbcLiteralFormatter()
.appendJdbcLiteral( sqlAppender, true, sessionFactory.getJdbcServices().getDialect(), sessionFactory.getWrapperOptions() );
sqlAppender.append( " then 1 else 0 end" );
}


final SqlAstNode arrayExpression = sqlAstArguments.get( 0 );

final boolean descending = sqlAstArguments.size() > 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, this will not work if the argument is passed as e.g. parameter. The same comment applies to the emulations for H2 and HSQLDB.

I know it's not great, but you could have this specialized branch for when the arguments are literals, and have a general implementation that produces something like this:

order by
  case when <nulls-first> then (case when t.val is null then 0 else 1 end) else (case when t.val is null then 1 else 0 end) end,
  case when <descending> then t.val end desc,
  case when not <descending> then t.val end

The <nulls-first> and <descending> placeholders are to be filled with sqlAstArguments.get( 1 ) and sqlAstArguments.get( 2 ) respectively.

Maybe this can be simplified further, but I didn't give this a lot of thought.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants