Skip to content

Commit ca59579

Browse files
committed
HHH-18981 Handle aggregate function arguments to array_to_string in H2 and HSQL specially
1 parent a2782a6 commit ca59579

File tree

4 files changed

+179
-65
lines changed

4 files changed

+179
-65
lines changed

hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayToStringFunction.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,14 @@ public ArrayToStringFunction(TypeConfiguration typeConfiguration) {
3535
"array_to_string",
3636
FunctionKind.NORMAL,
3737
StandardArgumentsValidators.composite(
38-
new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 3 ), ANY, STRING, ANY )
38+
new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 3 ), ANY, STRING, STRING )
3939
),
4040
StandardFunctionReturnTypeResolvers.invariant(
4141
typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.STRING )
4242
),
4343
StandardFunctionArgumentTypeResolvers.composite(
4444
new ArrayAndElementArgumentTypeResolver( 0, 2 ),
45-
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, ANY, STRING )
45+
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, ANY, STRING, STRING )
4646
)
4747
);
4848
}

hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArrayToStringFunction.java

Lines changed: 89 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@
77
import java.util.List;
88

99
import org.hibernate.metamodel.model.domain.ReturnableType;
10+
import org.hibernate.query.sqm.function.SelfRenderingOrderedSetAggregateFunctionSqlAstExpression;
11+
import org.hibernate.sql.ast.Clause;
12+
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
1013
import org.hibernate.sql.ast.SqlAstTranslator;
1114
import org.hibernate.sql.ast.spi.SqlAppender;
1215
import org.hibernate.sql.ast.tree.SqlAstNode;
1316
import org.hibernate.sql.ast.tree.expression.Expression;
17+
import org.hibernate.sql.ast.tree.predicate.Predicate;
18+
import org.hibernate.sql.ast.tree.select.SortSpecification;
1419
import org.hibernate.type.BasicPluralType;
1520
import org.hibernate.type.SqlTypes;
1621
import org.hibernate.type.spi.TypeConfiguration;
@@ -41,39 +46,94 @@ public void render(
4146
final BasicPluralType<?, ?> pluralType = (BasicPluralType<?, ?>) arrayExpression.getExpressionType().getSingleJdbcMapping();
4247
final int ddlTypeCode = pluralType.getElementType().getJdbcType().getDdlTypeCode();
4348
final boolean needsCast = !SqlTypes.isStringType( ddlTypeCode );
44-
sqlAppender.append( "case when " );
45-
arrayExpression.accept( walker );
46-
sqlAppender.append( " is not null then coalesce((select listagg(" );
47-
if ( defaultExpression != null ) {
48-
sqlAppender.append( "coalesce(" );
49-
}
50-
if ( needsCast ) {
51-
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
52-
// By default, H2 uses upper case, so lower it for a consistent experience
53-
sqlAppender.append( "lower(" );
49+
if ( arrayExpression instanceof SelfRenderingOrderedSetAggregateFunctionSqlAstExpression<?> functionExpression
50+
&& ArrayAggFunction.FUNCTION_NAME.equals( functionExpression.getFunctionName() ) ) {
51+
// When the array argument is an aggregate expression, we access its contents directly
52+
final Expression arrayElementExpression = (Expression) functionExpression.getArguments().get( 0 );
53+
final List<SortSpecification> withinGroup = functionExpression.getWithinGroup();
54+
final Predicate filter = functionExpression.getFilter();
55+
56+
sqlAppender.append( "listagg(" );
57+
if ( defaultExpression != null ) {
58+
sqlAppender.append( "coalesce(" );
5459
}
55-
sqlAppender.append( "cast(" );
56-
}
57-
sqlAppender.append( "array_get(" );
58-
arrayExpression.accept( walker );
59-
sqlAppender.append(",i.idx)" );
60-
if ( needsCast ) {
61-
sqlAppender.append( " as varchar)" );
62-
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
60+
if ( needsCast ) {
61+
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
62+
// By default, H2 uses upper case, so lower it for a consistent experience
63+
sqlAppender.append( "lower(" );
64+
}
65+
sqlAppender.append( "cast(" );
66+
}
67+
arrayElementExpression.accept( walker );
68+
if ( needsCast ) {
69+
sqlAppender.append( " as varchar)" );
70+
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
71+
sqlAppender.append( ')' );
72+
}
73+
}
74+
if ( defaultExpression != null ) {
75+
sqlAppender.append( ',' );
76+
defaultExpression.accept( walker );
6377
sqlAppender.append( ')' );
6478
}
79+
sqlAppender.append( "," );
80+
walker.render( separatorExpression, SqlAstNodeRenderingMode.DEFAULT );
81+
sqlAppender.appendSql( ')' );
82+
83+
if ( withinGroup != null && !withinGroup.isEmpty() ) {
84+
walker.getCurrentClauseStack().push( Clause.WITHIN_GROUP );
85+
sqlAppender.appendSql( " within group (order by " );
86+
withinGroup.get( 0 ).accept( walker );
87+
for ( int i = 1; i < withinGroup.size(); i++ ) {
88+
sqlAppender.appendSql( ',' );
89+
withinGroup.get( i ).accept( walker );
90+
}
91+
sqlAppender.appendSql( ')' );
92+
walker.getCurrentClauseStack().pop();
93+
}
94+
if ( filter != null ) {
95+
walker.getCurrentClauseStack().push( Clause.WHERE );
96+
sqlAppender.appendSql( " filter (where " );
97+
filter.accept( walker );
98+
sqlAppender.appendSql( ')' );
99+
walker.getCurrentClauseStack().pop();
100+
}
65101
}
66-
if ( defaultExpression != null ) {
67-
sqlAppender.append( ',' );
68-
defaultExpression.accept( walker );
69-
sqlAppender.append( ')' );
102+
else {
103+
sqlAppender.append( "case when " );
104+
arrayExpression.accept( walker );
105+
sqlAppender.append( " is not null then coalesce((select listagg(" );
106+
if ( defaultExpression != null ) {
107+
sqlAppender.append( "coalesce(" );
108+
}
109+
if ( needsCast ) {
110+
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
111+
// By default, H2 uses upper case, so lower it for a consistent experience
112+
sqlAppender.append( "lower(" );
113+
}
114+
sqlAppender.append( "cast(" );
115+
}
116+
sqlAppender.append( "array_get(" );
117+
arrayExpression.accept( walker );
118+
sqlAppender.append( ",i.idx)" );
119+
if ( needsCast ) {
120+
sqlAppender.append( " as varchar)" );
121+
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
122+
sqlAppender.append( ')' );
123+
}
124+
}
125+
if ( defaultExpression != null ) {
126+
sqlAppender.append( ',' );
127+
defaultExpression.accept( walker );
128+
sqlAppender.append( ')' );
129+
}
130+
sqlAppender.append( "," );
131+
walker.render( separatorExpression, SqlAstNodeRenderingMode.DEFAULT );
132+
sqlAppender.append( ") within group (order by i.idx) from system_range(1," );
133+
sqlAppender.append( Integer.toString( maximumArraySize ) );
134+
sqlAppender.append( ") i(idx) where i.idx<=coalesce(cardinality(" );
135+
arrayExpression.accept( walker );
136+
sqlAppender.append( "),0)),'') end" );
70137
}
71-
sqlAppender.append("," );
72-
separatorExpression.accept( walker );
73-
sqlAppender.append( ") within group (order by i.idx) from system_range(1,");
74-
sqlAppender.append( Integer.toString( maximumArraySize ) );
75-
sqlAppender.append( ") i(idx) where i.idx<=coalesce(cardinality(");
76-
arrayExpression.accept( walker );
77-
sqlAppender.append("),0)),'') end" );
78138
}
79139
}

hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayToStringFunction.java

Lines changed: 86 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@
77
import java.util.List;
88

99
import org.hibernate.metamodel.model.domain.ReturnableType;
10+
import org.hibernate.query.sqm.function.SelfRenderingOrderedSetAggregateFunctionSqlAstExpression;
11+
import org.hibernate.sql.ast.Clause;
1012
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
1113
import org.hibernate.sql.ast.SqlAstTranslator;
1214
import org.hibernate.sql.ast.spi.SqlAppender;
1315
import org.hibernate.sql.ast.tree.SqlAstNode;
1416
import org.hibernate.sql.ast.tree.expression.Expression;
17+
import org.hibernate.sql.ast.tree.predicate.Predicate;
18+
import org.hibernate.sql.ast.tree.select.SortSpecification;
1519
import org.hibernate.type.BasicPluralType;
1620
import org.hibernate.type.SqlTypes;
1721
import org.hibernate.type.spi.TypeConfiguration;
@@ -37,36 +41,91 @@ public void render(
3741
final BasicPluralType<?, ?> pluralType = (BasicPluralType<?, ?>) arrayExpression.getExpressionType().getSingleJdbcMapping();
3842
final int ddlTypeCode = pluralType.getElementType().getJdbcType().getDdlTypeCode();
3943
final boolean needsCast = !SqlTypes.isStringType( ddlTypeCode );
40-
sqlAppender.append( "case when " );
41-
arrayExpression.accept( walker );
42-
sqlAppender.append( " is not null then coalesce((select group_concat(" );
43-
if ( defaultExpression != null ) {
44-
sqlAppender.append( "coalesce(" );
45-
}
46-
if ( needsCast ) {
47-
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
48-
// By default, HSQLDB uses upper case, so lower it for a consistent experience
49-
sqlAppender.append( "lower(" );
44+
if ( arrayExpression instanceof SelfRenderingOrderedSetAggregateFunctionSqlAstExpression<?> functionExpression
45+
&& ArrayAggFunction.FUNCTION_NAME.equals( functionExpression.getFunctionName() ) ) {
46+
// When the array argument is an aggregate expression, we access its contents directly
47+
final Expression arrayElementExpression = (Expression) functionExpression.getArguments().get( 0 );
48+
final List<SortSpecification> withinGroup = functionExpression.getWithinGroup();
49+
final Predicate filter = functionExpression.getFilter();
50+
51+
sqlAppender.append( "group_concat(" );
52+
if ( defaultExpression != null ) {
53+
sqlAppender.append( "coalesce(" );
5054
}
51-
sqlAppender.append( "cast(" );
52-
}
53-
sqlAppender.append( "t.val" );
54-
if ( needsCast ) {
55-
sqlAppender.append( " as longvarchar)" );
56-
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
57-
sqlAppender.append( ')' );
55+
if ( needsCast ) {
56+
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
57+
// By default, HSQLDB uses upper case, so lower it for a consistent experience
58+
sqlAppender.append( "lower(" );
59+
}
60+
sqlAppender.append( "cast(" );
61+
}
62+
arrayElementExpression.accept( walker );
63+
if ( needsCast ) {
64+
sqlAppender.append( " as longvarchar)" );
65+
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
66+
sqlAppender.append( ')' );
67+
}
68+
}
69+
if ( defaultExpression != null ) {
70+
sqlAppender.append( "," );
71+
defaultExpression.accept( walker );
72+
sqlAppender.append( ")" );
73+
}
74+
75+
if ( withinGroup != null && !withinGroup.isEmpty() ) {
76+
walker.getCurrentClauseStack().push( Clause.WITHIN_GROUP );
77+
sqlAppender.appendSql( " order by " );
78+
withinGroup.get( 0 ).accept( walker );
79+
for ( int i = 1; i < withinGroup.size(); i++ ) {
80+
sqlAppender.appendSql( ',' );
81+
withinGroup.get( i ).accept( walker );
82+
}
83+
walker.getCurrentClauseStack().pop();
84+
}
85+
sqlAppender.append( " separator " );
86+
// HSQLDB doesn't like non-literals as separator
87+
walker.render( separatorExpression, SqlAstNodeRenderingMode.INLINE_PARAMETERS );
88+
sqlAppender.appendSql( ')' );
89+
if ( filter != null ) {
90+
walker.getCurrentClauseStack().push( Clause.WHERE );
91+
sqlAppender.appendSql( " filter (where " );
92+
filter.accept( walker );
93+
sqlAppender.appendSql( ')' );
94+
walker.getCurrentClauseStack().pop();
5895
}
5996
}
60-
if ( defaultExpression != null ) {
61-
sqlAppender.append( "," );
62-
defaultExpression.accept( walker );
63-
sqlAppender.append( ")" );
97+
else {
98+
sqlAppender.append( "case when " );
99+
arrayExpression.accept( walker );
100+
sqlAppender.append( " is not null then coalesce((select group_concat(" );
101+
if ( defaultExpression != null ) {
102+
sqlAppender.append( "coalesce(" );
103+
}
104+
if ( needsCast ) {
105+
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
106+
// By default, HSQLDB uses upper case, so lower it for a consistent experience
107+
sqlAppender.append( "lower(" );
108+
}
109+
sqlAppender.append( "cast(" );
110+
}
111+
sqlAppender.append( "t.val" );
112+
if ( needsCast ) {
113+
sqlAppender.append( " as longvarchar)" );
114+
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
115+
sqlAppender.append( ')' );
116+
}
117+
}
118+
if ( defaultExpression != null ) {
119+
sqlAppender.append( "," );
120+
defaultExpression.accept( walker );
121+
sqlAppender.append( ")" );
122+
}
123+
sqlAppender.append( " order by t.idx separator " );
124+
// HSQLDB doesn't like non-literals as separator
125+
walker.render( separatorExpression, SqlAstNodeRenderingMode.INLINE_PARAMETERS );
126+
sqlAppender.append( ") from unnest(" );
127+
arrayExpression.accept( walker );
128+
sqlAppender.append( ") with ordinality t(val,idx)),'') end" );
64129
}
65-
sqlAppender.append( " order by t.idx separator " );
66-
// HSQLDB doesn't like non-literals as separator
67-
walker.render( separatorExpression, SqlAstNodeRenderingMode.INLINE_PARAMETERS );
68-
sqlAppender.append( ") from unnest(");
69-
arrayExpression.accept( walker );
70-
sqlAppender.append(") with ordinality t(val,idx)),'') end" );
71130
}
72131
}

hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayToStringWithArrayAggregateTest.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,17 @@
77
import jakarta.persistence.Entity;
88
import jakarta.persistence.Id;
99
import jakarta.persistence.Tuple;
10-
import org.hibernate.dialect.H2Dialect;
11-
import org.hibernate.dialect.HSQLDialect;
1210
import org.hibernate.query.criteria.JpaCriteriaQuery;
1311
import org.hibernate.query.criteria.JpaCteCriteria;
1412
import org.hibernate.query.criteria.JpaRoot;
1513
import org.hibernate.query.sqm.NodeBuilder;
1614
import org.hibernate.testing.orm.junit.DialectFeatureChecks.SupportsArrayToString;
17-
import org.hibernate.testing.orm.junit.DialectFeatureChecks.SupportsJsonArrayAgg;
15+
import org.hibernate.testing.orm.junit.DialectFeatureChecks.SupportsArrayAgg;
1816
import org.hibernate.testing.orm.junit.DomainModel;
1917
import org.hibernate.testing.orm.junit.JiraKey;
2018
import org.hibernate.testing.orm.junit.RequiresDialectFeature;
2119
import org.hibernate.testing.orm.junit.SessionFactory;
2220
import org.hibernate.testing.orm.junit.SessionFactoryScope;
23-
import org.hibernate.testing.orm.junit.SkipForDialect;
2421
import org.junit.jupiter.api.AfterEach;
2522
import org.junit.jupiter.api.BeforeEach;
2623
import org.junit.jupiter.api.Test;
@@ -36,9 +33,7 @@
3633
annotatedClasses = {ArrayToStringWithArrayAggregateTest.Book.class, ArrayToStringWithArrayAggregateTest.Dummy.class})
3734
@SessionFactory
3835
@RequiresDialectFeature(feature = SupportsArrayToString.class)
39-
@RequiresDialectFeature( feature = SupportsJsonArrayAgg.class)
40-
@SkipForDialect(dialectClass = H2Dialect.class, reason = "Generated SQL query is not correct")
41-
@SkipForDialect(dialectClass = HSQLDialect.class, reason = "Generated SQL query is not correct")
36+
@RequiresDialectFeature(feature = SupportsArrayAgg.class)
4237
@JiraKey("HHH-18981")
4338
public class ArrayToStringWithArrayAggregateTest {
4439

0 commit comments

Comments
 (0)