-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
HHH-19826 Add array_reverse and array_sort functions #11391
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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>
|
Thanks for your pull request! This pull request appears to follow the contribution rules. › This message was automatically generated. |
beikov
left a comment
There was a problem hiding this 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.
| StandardArgumentsValidators.composite( | ||
| StandardArgumentsValidators.between( 1, 3 ), | ||
| ArrayArgumentValidator.DEFAULT_INSTANCE | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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 | |
| ) |
| StandardArgumentsValidators.composite( | ||
| StandardArgumentsValidators.between( 1, 3 ), | ||
| ArrayArgumentValidator.DEFAULT_INSTANCE | ||
| ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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 | |
| ) |
| StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE, | ||
| StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE |
There was a problem hiding this comment.
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:
| StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE, | |
| StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE | |
| StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, FunctionParameterType.BOOLEAN ), | |
| StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, FunctionParameterType.BOOLEAN ) |
| StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE, | ||
| StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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); |
There was a problem hiding this comment.
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.
| <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 " + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please take inspiration from this article to let the SQL engine do the sorting: https://technology.amis.nl/oracle/sorting-plsql-collections-the-quite-simple-way-part-two-have-the-sql-engine-do-the-heavy-lifting/
|
|
||
| sqlAppender.append( ") from unnest(" ); | ||
| arrayExpression.accept( walker ); | ||
| sqlAppender.append( ") with ordinality t(val,idx))," ); |
There was a problem hiding this comment.
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.
| sqlAppender.append( ") with ordinality t(val,idx))," ); | |
| sqlAppender.append( ") t(val))," ); |
| 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( ")" ); |
There was a problem hiding this comment.
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?
| 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))" ); |
| 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 ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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 |
There was a problem hiding this comment.
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 endThe <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.
Implements
array_reverse()andarray_sort()HQL functions.array_reverse(array)- Returns reversed arrayarray_sort(array [, descending [, nulls_first]])- Returns sorted arrayAll 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