Skip to content

Commit c8e8850

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

File tree

4 files changed

+199
-43
lines changed

4 files changed

+199
-43
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
@@ -34,15 +34,15 @@ public ArrayToStringFunction(TypeConfiguration typeConfiguration) {
3434
"array_to_string",
3535
FunctionKind.NORMAL,
3636
StandardArgumentsValidators.composite(
37-
new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 3 ), ANY, STRING, ANY ),
37+
new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 3 ), ANY, STRING, STRING ),
3838
new ArrayAndElementArgumentValidator( 0, 2 )
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: 99 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,18 @@
99
import java.util.List;
1010

1111
import org.hibernate.query.ReturnableType;
12+
import org.hibernate.query.sqm.function.SelfRenderingOrderedSetAggregateFunctionSqlAstExpression;
13+
import org.hibernate.sql.ast.Clause;
14+
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
1215
import org.hibernate.sql.ast.SqlAstTranslator;
1316
import org.hibernate.sql.ast.spi.SqlAppender;
1417
import org.hibernate.sql.ast.tree.SqlAstNode;
1518
import org.hibernate.sql.ast.tree.expression.Expression;
19+
import org.hibernate.sql.ast.tree.expression.FunctionExpression;
20+
import org.hibernate.sql.ast.tree.predicate.Predicate;
21+
import org.hibernate.sql.ast.tree.select.SortSpecification;
22+
import org.hibernate.type.BasicPluralType;
23+
import org.hibernate.type.SqlTypes;
1624
import org.hibernate.type.spi.TypeConfiguration;
1725

1826
/**
@@ -38,26 +46,99 @@ public void render(
3846
final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 );
3947
final Expression separatorExpression = (Expression) sqlAstArguments.get( 1 );
4048
final Expression defaultExpression = sqlAstArguments.size() > 2 ? (Expression) sqlAstArguments.get( 2 ) : null;
41-
sqlAppender.append( "case when " );
42-
arrayExpression.accept( walker );
43-
sqlAppender.append( " is not null then coalesce((select listagg(" );
44-
if ( defaultExpression != null ) {
45-
sqlAppender.append( "coalesce(" );
49+
final BasicPluralType<?, ?> pluralType = (BasicPluralType<?, ?>) arrayExpression.getExpressionType().getSingleJdbcMapping();
50+
final int ddlTypeCode = pluralType.getElementType().getJdbcType().getDdlTypeCode();
51+
final boolean needsCast = !SqlTypes.isStringType( ddlTypeCode );
52+
if ( arrayExpression instanceof SelfRenderingOrderedSetAggregateFunctionSqlAstExpression
53+
&& ArrayAggFunction.FUNCTION_NAME.equals( ( (FunctionExpression) arrayExpression ).getFunctionName() ) ) {
54+
final SelfRenderingOrderedSetAggregateFunctionSqlAstExpression functionExpression
55+
= (SelfRenderingOrderedSetAggregateFunctionSqlAstExpression) arrayExpression;
56+
// When the array argument is an aggregate expression, we access its contents directly
57+
final Expression arrayElementExpression = (Expression) functionExpression.getArguments().get( 0 );
58+
final List<SortSpecification> withinGroup = functionExpression.getWithinGroup();
59+
final Predicate filter = functionExpression.getFilter();
60+
61+
sqlAppender.append( "listagg(" );
62+
if ( defaultExpression != null ) {
63+
sqlAppender.append( "coalesce(" );
64+
}
65+
if ( needsCast ) {
66+
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
67+
// By default, H2 uses upper case, so lower it for a consistent experience
68+
sqlAppender.append( "lower(" );
69+
}
70+
sqlAppender.append( "cast(" );
71+
}
72+
arrayElementExpression.accept( walker );
73+
if ( needsCast ) {
74+
sqlAppender.append( " as varchar)" );
75+
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
76+
sqlAppender.append( ')' );
77+
}
78+
}
79+
if ( defaultExpression != null ) {
80+
sqlAppender.append( ',' );
81+
defaultExpression.accept( walker );
82+
sqlAppender.append( ')' );
83+
}
84+
sqlAppender.append( "," );
85+
walker.render( separatorExpression, SqlAstNodeRenderingMode.DEFAULT );
86+
sqlAppender.appendSql( ')' );
87+
88+
if ( withinGroup != null && !withinGroup.isEmpty() ) {
89+
walker.getCurrentClauseStack().push( Clause.WITHIN_GROUP );
90+
sqlAppender.appendSql( " within group (order by " );
91+
withinGroup.get( 0 ).accept( walker );
92+
for ( int i = 1; i < withinGroup.size(); i++ ) {
93+
sqlAppender.appendSql( ',' );
94+
withinGroup.get( i ).accept( walker );
95+
}
96+
sqlAppender.appendSql( ')' );
97+
walker.getCurrentClauseStack().pop();
98+
}
99+
if ( filter != null ) {
100+
walker.getCurrentClauseStack().push( Clause.WHERE );
101+
sqlAppender.appendSql( " filter (where " );
102+
filter.accept( walker );
103+
sqlAppender.appendSql( ')' );
104+
walker.getCurrentClauseStack().pop();
105+
}
46106
}
47-
sqlAppender.append( "array_get(" );
48-
arrayExpression.accept( walker );
49-
sqlAppender.append(",i.idx)" );
50-
if ( defaultExpression != null ) {
107+
else {
108+
sqlAppender.append( "case when " );
109+
arrayExpression.accept( walker );
110+
sqlAppender.append( " is not null then coalesce((select listagg(" );
111+
if ( defaultExpression != null ) {
112+
sqlAppender.append( "coalesce(" );
113+
}
114+
if ( needsCast ) {
115+
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
116+
// By default, H2 uses upper case, so lower it for a consistent experience
117+
sqlAppender.append( "lower(" );
118+
}
119+
sqlAppender.append( "cast(" );
120+
}
121+
sqlAppender.append( "array_get(" );
122+
arrayExpression.accept( walker );
123+
sqlAppender.append( ",i.idx)" );
124+
if ( needsCast ) {
125+
sqlAppender.append( " as varchar)" );
126+
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
127+
sqlAppender.append( ')' );
128+
}
129+
}
130+
if ( defaultExpression != null ) {
131+
sqlAppender.append( ',' );
132+
defaultExpression.accept( walker );
133+
sqlAppender.append( ')' );
134+
}
51135
sqlAppender.append( "," );
52-
defaultExpression.accept( walker );
53-
sqlAppender.append( ")" );
136+
walker.render( separatorExpression, SqlAstNodeRenderingMode.DEFAULT );
137+
sqlAppender.append( ") within group (order by i.idx) from system_range(1," );
138+
sqlAppender.append( Integer.toString( maximumArraySize ) );
139+
sqlAppender.append( ") i(idx) where i.idx<=coalesce(cardinality(" );
140+
arrayExpression.accept( walker );
141+
sqlAppender.append( "),0)),'') end" );
54142
}
55-
sqlAppender.append("," );
56-
separatorExpression.accept( walker );
57-
sqlAppender.append( ") within group (order by i.idx) from system_range(1,");
58-
sqlAppender.append( Integer.toString( maximumArraySize ) );
59-
sqlAppender.append( ") i(idx) where i.idx<=coalesce(cardinality(");
60-
arrayExpression.accept( walker );
61-
sqlAppender.append("),0)),'') end" );
62143
}
63144
}

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

Lines changed: 96 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,18 @@
99
import java.util.List;
1010

1111
import org.hibernate.query.ReturnableType;
12+
import org.hibernate.query.sqm.function.SelfRenderingOrderedSetAggregateFunctionSqlAstExpression;
13+
import org.hibernate.sql.ast.Clause;
1214
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
1315
import org.hibernate.sql.ast.SqlAstTranslator;
1416
import org.hibernate.sql.ast.spi.SqlAppender;
1517
import org.hibernate.sql.ast.tree.SqlAstNode;
1618
import org.hibernate.sql.ast.tree.expression.Expression;
19+
import org.hibernate.sql.ast.tree.expression.FunctionExpression;
20+
import org.hibernate.sql.ast.tree.predicate.Predicate;
21+
import org.hibernate.sql.ast.tree.select.SortSpecification;
22+
import org.hibernate.type.BasicPluralType;
23+
import org.hibernate.type.SqlTypes;
1724
import org.hibernate.type.spi.TypeConfiguration;
1825

1926
/**
@@ -34,23 +41,96 @@ public void render(
3441
final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 );
3542
final Expression separatorExpression = (Expression) sqlAstArguments.get( 1 );
3643
final Expression defaultExpression = sqlAstArguments.size() > 2 ? (Expression) sqlAstArguments.get( 2 ) : null;
37-
sqlAppender.append( "case when " );
38-
arrayExpression.accept( walker );
39-
sqlAppender.append( " is not null then coalesce((select group_concat(" );
40-
if ( defaultExpression != null ) {
41-
sqlAppender.append( "coalesce(" );
44+
final BasicPluralType<?, ?> pluralType = (BasicPluralType<?, ?>) arrayExpression.getExpressionType().getSingleJdbcMapping();
45+
final int ddlTypeCode = pluralType.getElementType().getJdbcType().getDdlTypeCode();
46+
final boolean needsCast = !SqlTypes.isStringType( ddlTypeCode );
47+
if ( arrayExpression instanceof SelfRenderingOrderedSetAggregateFunctionSqlAstExpression
48+
&& ArrayAggFunction.FUNCTION_NAME.equals( ( (FunctionExpression) arrayExpression ).getFunctionName() ) ) {
49+
final SelfRenderingOrderedSetAggregateFunctionSqlAstExpression functionExpression
50+
= (SelfRenderingOrderedSetAggregateFunctionSqlAstExpression) arrayExpression;
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( "group_concat(" );
57+
if ( defaultExpression != null ) {
58+
sqlAppender.append( "coalesce(" );
59+
}
60+
if ( needsCast ) {
61+
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
62+
// By default, HSQLDB 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 longvarchar)" );
70+
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
71+
sqlAppender.append( ')' );
72+
}
73+
}
74+
if ( defaultExpression != null ) {
75+
sqlAppender.append( "," );
76+
defaultExpression.accept( walker );
77+
sqlAppender.append( ")" );
78+
}
79+
80+
if ( withinGroup != null && !withinGroup.isEmpty() ) {
81+
walker.getCurrentClauseStack().push( Clause.WITHIN_GROUP );
82+
sqlAppender.appendSql( " order by " );
83+
withinGroup.get( 0 ).accept( walker );
84+
for ( int i = 1; i < withinGroup.size(); i++ ) {
85+
sqlAppender.appendSql( ',' );
86+
withinGroup.get( i ).accept( walker );
87+
}
88+
walker.getCurrentClauseStack().pop();
89+
}
90+
sqlAppender.append( " separator " );
91+
// HSQLDB doesn't like non-literals as separator
92+
walker.render( separatorExpression, SqlAstNodeRenderingMode.INLINE_PARAMETERS );
93+
sqlAppender.appendSql( ')' );
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+
}
42101
}
43-
sqlAppender.append( "t.val" );
44-
if ( defaultExpression != null ) {
45-
sqlAppender.append( "," );
46-
defaultExpression.accept( walker );
47-
sqlAppender.append( ")" );
102+
else {
103+
sqlAppender.append( "case when " );
104+
arrayExpression.accept( walker );
105+
sqlAppender.append( " is not null then coalesce((select group_concat(" );
106+
if ( defaultExpression != null ) {
107+
sqlAppender.append( "coalesce(" );
108+
}
109+
if ( needsCast ) {
110+
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
111+
// By default, HSQLDB uses upper case, so lower it for a consistent experience
112+
sqlAppender.append( "lower(" );
113+
}
114+
sqlAppender.append( "cast(" );
115+
}
116+
sqlAppender.append( "t.val" );
117+
if ( needsCast ) {
118+
sqlAppender.append( " as longvarchar)" );
119+
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
120+
sqlAppender.append( ')' );
121+
}
122+
}
123+
if ( defaultExpression != null ) {
124+
sqlAppender.append( "," );
125+
defaultExpression.accept( walker );
126+
sqlAppender.append( ")" );
127+
}
128+
sqlAppender.append( " order by t.idx separator " );
129+
// HSQLDB doesn't like non-literals as separator
130+
walker.render( separatorExpression, SqlAstNodeRenderingMode.INLINE_PARAMETERS );
131+
sqlAppender.append( ") from unnest(" );
132+
arrayExpression.accept( walker );
133+
sqlAppender.append( ") with ordinality t(val,idx)),'') end" );
48134
}
49-
sqlAppender.append( " order by t.idx separator " );
50-
// HSQLDB doesn't like non-literals as separator
51-
walker.render( separatorExpression, SqlAstNodeRenderingMode.INLINE_PARAMETERS );
52-
sqlAppender.append( ") from unnest(");
53-
arrayExpression.accept( walker );
54-
sqlAppender.append(") with ordinality t(val,idx)),'') end" );
55135
}
56136
}

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)