From 110749fe55ec06edf7e1b96548f443c82d6cb3f7 Mon Sep 17 00:00:00 2001 From: stringintech Date: Sat, 28 Dec 2024 00:21:56 +0330 Subject: [PATCH 1/3] HHH-18981 test reproducing the issue --- .../ArrayToStringWithArrayAggregateTest.java | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayToStringWithArrayAggregateTest.java diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayToStringWithArrayAggregateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayToStringWithArrayAggregateTest.java new file mode 100644 index 000000000000..d24268b15d9d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayToStringWithArrayAggregateTest.java @@ -0,0 +1,143 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.function.array; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Tuple; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.dialect.HSQLDialect; +import org.hibernate.query.criteria.JpaCriteriaQuery; +import org.hibernate.query.criteria.JpaCteCriteria; +import org.hibernate.query.criteria.JpaRoot; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.testing.orm.junit.DialectFeatureChecks.SupportsArrayToString; +import org.hibernate.testing.orm.junit.DialectFeatureChecks.SupportsJsonArrayAgg; +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.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Kowsar Atazadeh + */ +@DomainModel( + annotatedClasses = {ArrayToStringWithArrayAggregateTest.Book.class, ArrayToStringWithArrayAggregateTest.Dummy.class}) +@SessionFactory +@RequiresDialectFeature(feature = SupportsArrayToString.class) +@RequiresDialectFeature( feature = SupportsJsonArrayAgg.class) +@SkipForDialect(dialectClass = H2Dialect.class, reason = "Generated SQL query is not correct") +@SkipForDialect(dialectClass = HSQLDialect.class, reason = "Generated SQL query is not correct") +@JiraKey("HHH-18981") +public class ArrayToStringWithArrayAggregateTest { + + @BeforeEach + public void prepareData(SessionFactoryScope scope) { + scope.inTransaction( em -> { + em.persist( new Book( 1, "title1" ) ); + em.persist( new Book( 2, "title2" ) ); + } ); + } + + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( em -> { + em.createMutationQuery( "delete from Book" ).executeUpdate(); + } ); + } + + @Test + public void test(SessionFactoryScope scope) { + scope.inSession( em -> { + final NodeBuilder cb = (NodeBuilder) em.getCriteriaBuilder(); + final JpaCriteriaQuery q = cb.createTupleQuery(); + JpaRoot root = q.from( Book.class ); + + q.multiselect( + cb.arrayToString( + cb.arrayAgg( cb.asc( root.get( "title" ) ), root.get( "title" ) ), + "," + ).alias( "titles" ) + ); + List list = em.createQuery( q ).getResultList(); + String titles = list.get( 0 ).get( "titles", String.class ); + assertThat( titles ).isEqualTo( "title1,title2" ); + } ); + } + + @Test + public void testWithCte(SessionFactoryScope scope) { + scope.inSession( em -> { + final NodeBuilder cb = (NodeBuilder) em.getCriteriaBuilder(); + + JpaCriteriaQuery cteQuery = cb.createTupleQuery(); + JpaRoot cteRoot = cteQuery.from( Book.class ); + cteQuery.multiselect( + cb.arrayAgg( cb.asc( cteRoot.get( "title" ) ), cteRoot.get( "title" ) ) + .alias( "titles_array" ) + ); + + JpaCriteriaQuery query = cb.createTupleQuery(); + JpaCteCriteria titlesCte = query.with( cteQuery ); + JpaRoot root = query.from( titlesCte ); + query.multiselect( + cb.arrayToString( root.get( "titles_array" ), cb.literal( "," ) ) + .alias( "titles" ) + ); + + List list = em.createQuery( query ).getResultList(); + String titles = list.get( 0 ).get( "titles", String.class ); + assertThat( titles ).isEqualTo( "title1,title2" ); + } ); + } + + @Entity(name = "Book") + public static class Book { + @Id + private Integer id; + private String title; + + public Book() { + } + + public Book(Integer id, String title) { + this.id = id; + this.title = title; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + // Needed for Oracle + @Entity + static class Dummy { + @Id + Long id; + String[] theArray; + } +} From 5ef874c824a669e25e68758fb1551093e6bc79dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=8Cedomir=20Igaly?= Date: Fri, 18 Jul 2025 18:02:43 +0200 Subject: [PATCH 2/3] HHH-18981 Ignore optional parameter(s) in org.hibernate.dialect.function.array.ArrayAndElementArgumentTypeResolver#resolveFunctionArgumentType --- .../function/array/ArrayAndElementArgumentTypeResolver.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayAndElementArgumentTypeResolver.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayAndElementArgumentTypeResolver.java index f338825a93fb..6ffad1fdddc5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayAndElementArgumentTypeResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayAndElementArgumentTypeResolver.java @@ -37,6 +37,9 @@ public ArrayAndElementArgumentTypeResolver(int arrayIndex, int... elementIndexes public @Nullable MappingModelExpressible resolveFunctionArgumentType(List> arguments, int argumentIndex, SqmToSqlAstConverter converter) { if ( argumentIndex == arrayIndex ) { for ( int elementIndex : elementIndexes ) { + if ( elementIndex >= arguments.size() ) { + continue; + } final SqmTypedNode node = arguments.get( elementIndex ); if ( node instanceof SqmExpression sqmExpression ) { final MappingModelExpressible expressible = converter.determineValueMapping( sqmExpression ); From 429004e8a1835e117e4d24d97dd66196be309593 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Tue, 29 Jul 2025 16:30:49 +0200 Subject: [PATCH 3/3] HHH-18981 Handle aggregate function arguments to array_to_string in H2 and HSQL specially --- .../function/array/ArrayToStringFunction.java | 4 +- .../array/H2ArrayToStringFunction.java | 118 +++++++++++++----- .../array/HSQLArrayToStringFunction.java | 113 +++++++++++++---- .../ArrayToStringWithArrayAggregateTest.java | 9 +- 4 files changed, 179 insertions(+), 65 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayToStringFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayToStringFunction.java index 437377aabb6f..94c0995e39b0 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayToStringFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayToStringFunction.java @@ -35,14 +35,14 @@ public ArrayToStringFunction(TypeConfiguration typeConfiguration) { "array_to_string", FunctionKind.NORMAL, StandardArgumentsValidators.composite( - new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 3 ), ANY, STRING, ANY ) + new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 3 ), ANY, STRING, STRING ) ), StandardFunctionReturnTypeResolvers.invariant( typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.STRING ) ), StandardFunctionArgumentTypeResolvers.composite( new ArrayAndElementArgumentTypeResolver( 0, 2 ), - StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, ANY, STRING ) + StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, ANY, STRING, STRING ) ) ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArrayToStringFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArrayToStringFunction.java index 86fcea3abd70..4c382fe3445f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArrayToStringFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArrayToStringFunction.java @@ -7,10 +7,15 @@ import java.util.List; import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.query.sqm.function.SelfRenderingOrderedSetAggregateFunctionSqlAstExpression; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; 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.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; import org.hibernate.type.BasicPluralType; import org.hibernate.type.SqlTypes; import org.hibernate.type.spi.TypeConfiguration; @@ -41,39 +46,94 @@ public void render( final BasicPluralType pluralType = (BasicPluralType) arrayExpression.getExpressionType().getSingleJdbcMapping(); final int ddlTypeCode = pluralType.getElementType().getJdbcType().getDdlTypeCode(); final boolean needsCast = !SqlTypes.isStringType( ddlTypeCode ); - sqlAppender.append( "case when " ); - arrayExpression.accept( walker ); - sqlAppender.append( " is not null then coalesce((select listagg(" ); - if ( defaultExpression != null ) { - sqlAppender.append( "coalesce(" ); - } - if ( needsCast ) { - if ( ddlTypeCode == SqlTypes.BOOLEAN ) { - // By default, H2 uses upper case, so lower it for a consistent experience - sqlAppender.append( "lower(" ); + if ( arrayExpression instanceof SelfRenderingOrderedSetAggregateFunctionSqlAstExpression functionExpression + && ArrayAggFunction.FUNCTION_NAME.equals( functionExpression.getFunctionName() ) ) { + // When the array argument is an aggregate expression, we access its contents directly + final Expression arrayElementExpression = (Expression) functionExpression.getArguments().get( 0 ); + final List withinGroup = functionExpression.getWithinGroup(); + final Predicate filter = functionExpression.getFilter(); + + sqlAppender.append( "listagg(" ); + if ( defaultExpression != null ) { + sqlAppender.append( "coalesce(" ); } - sqlAppender.append( "cast(" ); - } - sqlAppender.append( "array_get(" ); - arrayExpression.accept( walker ); - sqlAppender.append(",i.idx)" ); - if ( needsCast ) { - sqlAppender.append( " as varchar)" ); - if ( ddlTypeCode == SqlTypes.BOOLEAN ) { + if ( needsCast ) { + if ( ddlTypeCode == SqlTypes.BOOLEAN ) { + // By default, H2 uses upper case, so lower it for a consistent experience + sqlAppender.append( "lower(" ); + } + sqlAppender.append( "cast(" ); + } + arrayElementExpression.accept( walker ); + if ( needsCast ) { + sqlAppender.append( " as varchar)" ); + if ( ddlTypeCode == SqlTypes.BOOLEAN ) { + sqlAppender.append( ')' ); + } + } + if ( defaultExpression != null ) { + sqlAppender.append( ',' ); + defaultExpression.accept( walker ); sqlAppender.append( ')' ); } + sqlAppender.append( "," ); + walker.render( separatorExpression, SqlAstNodeRenderingMode.DEFAULT ); + sqlAppender.appendSql( ')' ); + + if ( withinGroup != null && !withinGroup.isEmpty() ) { + walker.getCurrentClauseStack().push( Clause.WITHIN_GROUP ); + sqlAppender.appendSql( " within group (order by " ); + withinGroup.get( 0 ).accept( walker ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( walker ); + } + sqlAppender.appendSql( ')' ); + walker.getCurrentClauseStack().pop(); + } + if ( filter != null ) { + walker.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( " filter (where " ); + filter.accept( walker ); + sqlAppender.appendSql( ')' ); + walker.getCurrentClauseStack().pop(); + } } - if ( defaultExpression != null ) { - sqlAppender.append( ',' ); - defaultExpression.accept( walker ); - sqlAppender.append( ')' ); + else { + sqlAppender.append( "case when " ); + arrayExpression.accept( walker ); + sqlAppender.append( " is not null then coalesce((select listagg(" ); + if ( defaultExpression != null ) { + sqlAppender.append( "coalesce(" ); + } + if ( needsCast ) { + if ( ddlTypeCode == SqlTypes.BOOLEAN ) { + // By default, H2 uses upper case, so lower it for a consistent experience + sqlAppender.append( "lower(" ); + } + sqlAppender.append( "cast(" ); + } + sqlAppender.append( "array_get(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ",i.idx)" ); + if ( needsCast ) { + sqlAppender.append( " as varchar)" ); + if ( ddlTypeCode == SqlTypes.BOOLEAN ) { + sqlAppender.append( ')' ); + } + } + if ( defaultExpression != null ) { + sqlAppender.append( ',' ); + defaultExpression.accept( walker ); + sqlAppender.append( ')' ); + } + sqlAppender.append( "," ); + walker.render( separatorExpression, SqlAstNodeRenderingMode.DEFAULT ); + sqlAppender.append( ") within group (order by i.idx) from system_range(1," ); + sqlAppender.append( Integer.toString( maximumArraySize ) ); + sqlAppender.append( ") i(idx) where i.idx<=coalesce(cardinality(" ); + arrayExpression.accept( walker ); + sqlAppender.append( "),0)),'') end" ); } - sqlAppender.append("," ); - separatorExpression.accept( walker ); - sqlAppender.append( ") within group (order by i.idx) from system_range(1,"); - sqlAppender.append( Integer.toString( maximumArraySize ) ); - sqlAppender.append( ") i(idx) where i.idx<=coalesce(cardinality("); - arrayExpression.accept( walker ); - sqlAppender.append("),0)),'') end" ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayToStringFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayToStringFunction.java index 3cad39f9ec68..412d65030b8c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayToStringFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayToStringFunction.java @@ -7,11 +7,15 @@ import java.util.List; import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.query.sqm.function.SelfRenderingOrderedSetAggregateFunctionSqlAstExpression; +import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; 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.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; import org.hibernate.type.BasicPluralType; import org.hibernate.type.SqlTypes; import org.hibernate.type.spi.TypeConfiguration; @@ -37,36 +41,91 @@ public void render( final BasicPluralType pluralType = (BasicPluralType) arrayExpression.getExpressionType().getSingleJdbcMapping(); final int ddlTypeCode = pluralType.getElementType().getJdbcType().getDdlTypeCode(); final boolean needsCast = !SqlTypes.isStringType( ddlTypeCode ); - sqlAppender.append( "case when " ); - arrayExpression.accept( walker ); - sqlAppender.append( " is not null then coalesce((select group_concat(" ); - if ( defaultExpression != null ) { - sqlAppender.append( "coalesce(" ); - } - if ( needsCast ) { - if ( ddlTypeCode == SqlTypes.BOOLEAN ) { - // By default, HSQLDB uses upper case, so lower it for a consistent experience - sqlAppender.append( "lower(" ); + if ( arrayExpression instanceof SelfRenderingOrderedSetAggregateFunctionSqlAstExpression functionExpression + && ArrayAggFunction.FUNCTION_NAME.equals( functionExpression.getFunctionName() ) ) { + // When the array argument is an aggregate expression, we access its contents directly + final Expression arrayElementExpression = (Expression) functionExpression.getArguments().get( 0 ); + final List withinGroup = functionExpression.getWithinGroup(); + final Predicate filter = functionExpression.getFilter(); + + sqlAppender.append( "group_concat(" ); + if ( defaultExpression != null ) { + sqlAppender.append( "coalesce(" ); } - sqlAppender.append( "cast(" ); - } - sqlAppender.append( "t.val" ); - if ( needsCast ) { - sqlAppender.append( " as longvarchar)" ); - if ( ddlTypeCode == SqlTypes.BOOLEAN ) { - sqlAppender.append( ')' ); + if ( needsCast ) { + if ( ddlTypeCode == SqlTypes.BOOLEAN ) { + // By default, HSQLDB uses upper case, so lower it for a consistent experience + sqlAppender.append( "lower(" ); + } + sqlAppender.append( "cast(" ); + } + arrayElementExpression.accept( walker ); + if ( needsCast ) { + sqlAppender.append( " as longvarchar)" ); + if ( ddlTypeCode == SqlTypes.BOOLEAN ) { + sqlAppender.append( ')' ); + } + } + if ( defaultExpression != null ) { + sqlAppender.append( "," ); + defaultExpression.accept( walker ); + sqlAppender.append( ")" ); + } + + if ( withinGroup != null && !withinGroup.isEmpty() ) { + walker.getCurrentClauseStack().push( Clause.WITHIN_GROUP ); + sqlAppender.appendSql( " order by " ); + withinGroup.get( 0 ).accept( walker ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( walker ); + } + walker.getCurrentClauseStack().pop(); + } + sqlAppender.append( " separator " ); + // HSQLDB doesn't like non-literals as separator + walker.render( separatorExpression, SqlAstNodeRenderingMode.INLINE_PARAMETERS ); + sqlAppender.appendSql( ')' ); + if ( filter != null ) { + walker.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( " filter (where " ); + filter.accept( walker ); + sqlAppender.appendSql( ')' ); + walker.getCurrentClauseStack().pop(); } } - if ( defaultExpression != null ) { - sqlAppender.append( "," ); - defaultExpression.accept( walker ); - sqlAppender.append( ")" ); + else { + sqlAppender.append( "case when " ); + arrayExpression.accept( walker ); + sqlAppender.append( " is not null then coalesce((select group_concat(" ); + if ( defaultExpression != null ) { + sqlAppender.append( "coalesce(" ); + } + if ( needsCast ) { + if ( ddlTypeCode == SqlTypes.BOOLEAN ) { + // By default, HSQLDB uses upper case, so lower it for a consistent experience + sqlAppender.append( "lower(" ); + } + sqlAppender.append( "cast(" ); + } + sqlAppender.append( "t.val" ); + if ( needsCast ) { + sqlAppender.append( " as longvarchar)" ); + if ( ddlTypeCode == SqlTypes.BOOLEAN ) { + sqlAppender.append( ')' ); + } + } + if ( defaultExpression != null ) { + sqlAppender.append( "," ); + defaultExpression.accept( walker ); + sqlAppender.append( ")" ); + } + sqlAppender.append( " order by t.idx separator " ); + // HSQLDB doesn't like non-literals as separator + walker.render( separatorExpression, SqlAstNodeRenderingMode.INLINE_PARAMETERS ); + sqlAppender.append( ") from unnest(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ") with ordinality t(val,idx)),'') end" ); } - sqlAppender.append( " order by t.idx separator " ); - // HSQLDB doesn't like non-literals as separator - walker.render( separatorExpression, SqlAstNodeRenderingMode.INLINE_PARAMETERS ); - sqlAppender.append( ") from unnest("); - arrayExpression.accept( walker ); - sqlAppender.append(") with ordinality t(val,idx)),'') end" ); } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayToStringWithArrayAggregateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayToStringWithArrayAggregateTest.java index d24268b15d9d..e314615efc51 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayToStringWithArrayAggregateTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayToStringWithArrayAggregateTest.java @@ -7,20 +7,17 @@ import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Tuple; -import org.hibernate.dialect.H2Dialect; -import org.hibernate.dialect.HSQLDialect; import org.hibernate.query.criteria.JpaCriteriaQuery; import org.hibernate.query.criteria.JpaCteCriteria; import org.hibernate.query.criteria.JpaRoot; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.testing.orm.junit.DialectFeatureChecks.SupportsArrayToString; -import org.hibernate.testing.orm.junit.DialectFeatureChecks.SupportsJsonArrayAgg; +import org.hibernate.testing.orm.junit.DialectFeatureChecks.SupportsArrayAgg; 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.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -36,9 +33,7 @@ annotatedClasses = {ArrayToStringWithArrayAggregateTest.Book.class, ArrayToStringWithArrayAggregateTest.Dummy.class}) @SessionFactory @RequiresDialectFeature(feature = SupportsArrayToString.class) -@RequiresDialectFeature( feature = SupportsJsonArrayAgg.class) -@SkipForDialect(dialectClass = H2Dialect.class, reason = "Generated SQL query is not correct") -@SkipForDialect(dialectClass = HSQLDialect.class, reason = "Generated SQL query is not correct") +@RequiresDialectFeature(feature = SupportsArrayAgg.class) @JiraKey("HHH-18981") public class ArrayToStringWithArrayAggregateTest {