HHH-18765 - additional fix needed for the generic array_to_string function

Signed-off-by: Jan Schatteman <jschatte@redhat.com>
This commit is contained in:
Jan Schatteman 2024-10-30 22:51:29 +01:00 committed by Christian Beikov
parent 7e1a740605
commit b9274f5b75
16 changed files with 124 additions and 47 deletions

View File

@ -16,6 +16,9 @@ import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolv
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.type.BasicPluralType;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.StandardBasicTypes;
import org.hibernate.type.spi.TypeConfiguration;
@ -32,8 +35,7 @@ public class ArrayToStringFunction extends AbstractSqmSelfRenderingFunctionDescr
"array_to_string",
FunctionKind.NORMAL,
StandardArgumentsValidators.composite(
new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 3 ), ANY, STRING, ANY ),
new ArrayAndElementArgumentValidator( 0, 2 )
new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 3 ), ANY, STRING, ANY )
),
StandardFunctionReturnTypeResolvers.invariant(
typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.STRING )
@ -51,15 +53,42 @@ public class ArrayToStringFunction extends AbstractSqmSelfRenderingFunctionDescr
List<? extends SqlAstNode> sqlAstArguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
sqlAppender.appendSql( "array_to_string(" );
sqlAstArguments.get( 0 ).accept( walker );
sqlAppender.appendSql( ',' );
sqlAstArguments.get( 1 ).accept( walker );
if ( sqlAstArguments.size() > 2 ) {
final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 );
final Expression separatorExpression = (Expression) sqlAstArguments.get( 1 );
final Expression defaultExpression = sqlAstArguments.size() > 2 ? (Expression) sqlAstArguments.get( 2 ) : null;
final BasicPluralType<?, ?> pluralType = (BasicPluralType<?, ?>) arrayExpression.getExpressionType().getSingleJdbcMapping();
final int ddlTypeCode = pluralType.getElementType().getJdbcType().getDdlTypeCode();
if ( ddlTypeCode == SqlTypes.BOOLEAN ) {
// For some reason, PostgreSQL turns true/false to t/f in this function, so unnest this manually
sqlAppender.append( "case when " );
arrayExpression.accept( walker );
sqlAppender.append( " is not null then coalesce((select string_agg(" );
if ( defaultExpression != null ) {
sqlAppender.append( "coalesce(" );
}
sqlAppender.append( "cast(t.v as varchar)" );
if ( defaultExpression != null ) {
sqlAppender.append( "," );
defaultExpression.accept( walker );
sqlAppender.append( ")" );
}
sqlAppender.appendSql( ',' );
sqlAstArguments.get( 2 ).accept( walker );
separatorExpression.accept( walker );
sqlAppender.append( " order by t.i) from unnest(");
arrayExpression.accept( walker );
sqlAppender.append(") with ordinality t(v,i)),'') end" );
}
else {
sqlAppender.appendSql( "array_to_string(" );
arrayExpression.accept( walker );
sqlAppender.appendSql( ',' );
separatorExpression.accept( walker );
if ( defaultExpression != null ) {
sqlAppender.appendSql( ',' );
defaultExpression.accept( walker );
}
sqlAppender.appendSql( ')' );
}
sqlAppender.appendSql( ')' );
}
}

View File

@ -11,6 +11,8 @@ 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.type.BasicPluralType;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.spi.TypeConfiguration;
/**
@ -36,19 +38,35 @@ public class H2ArrayToStringFunction extends ArrayToStringFunction {
final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 );
final Expression separatorExpression = (Expression) sqlAstArguments.get( 1 );
final Expression defaultExpression = sqlAstArguments.size() > 2 ? (Expression) sqlAstArguments.get( 2 ) : null;
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(" );
}
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( "," );
sqlAppender.append( ',' );
defaultExpression.accept( walker );
sqlAppender.append( ")" );
sqlAppender.append( ')' );
}
sqlAppender.append("," );
separatorExpression.accept( walker );

View File

@ -12,6 +12,8 @@ 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.type.BasicPluralType;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.spi.TypeConfiguration;
/**
@ -32,13 +34,29 @@ public class HSQLArrayToStringFunction extends ArrayToStringFunction {
final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 );
final Expression separatorExpression = (Expression) sqlAstArguments.get( 1 );
final Expression defaultExpression = sqlAstArguments.size() > 2 ? (Expression) sqlAstArguments.get( 2 ) : null;
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(" );
}
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 );

View File

@ -4,18 +4,12 @@
*/
package org.hibernate.query.results.internal;
import org.hibernate.metamodel.mapping.EntityIdentifierMapping;
import org.hibernate.metamodel.mapping.ModelPart;
import org.hibernate.metamodel.mapping.SelectableMapping;
import org.hibernate.metamodel.mapping.internal.SingleAttributeIdentifierMapping;
import org.hibernate.spi.EntityIdentifierNavigablePath;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.sql.ast.tree.from.TableGroup;
import org.hibernate.sql.ast.tree.from.TableReference;
import org.hibernate.sql.results.graph.DomainResult;
import org.hibernate.sql.results.graph.DomainResultCreationState;
import org.hibernate.sql.results.graph.Fetch;
import org.hibernate.sql.results.graph.basic.BasicFetch;
import org.hibernate.sql.results.jdbc.spi.JdbcValuesMetadata;
import static org.hibernate.sql.ast.spi.SqlExpressionResolver.createColumnReferenceKey;

View File

@ -13,9 +13,7 @@ import org.hibernate.query.results.internal.DomainResultCreationStateImpl;
import org.hibernate.query.results.FetchBuilder;
import org.hibernate.query.results.internal.ResultsHelper;
import org.hibernate.spi.NavigablePath;
import org.hibernate.sql.ast.spi.SqlSelection;
import org.hibernate.sql.ast.tree.from.TableGroup;
import org.hibernate.sql.ast.tree.from.TableReference;
import org.hibernate.sql.results.graph.DomainResultCreationState;
import org.hibernate.sql.results.graph.Fetch;
import org.hibernate.sql.results.graph.FetchParent;

View File

@ -13,9 +13,7 @@ import org.hibernate.query.results.internal.ResultsHelper;
import org.hibernate.spi.NavigablePath;
import org.hibernate.query.results.internal.DomainResultCreationStateImpl;
import org.hibernate.query.results.FetchBuilder;
import org.hibernate.sql.ast.spi.SqlSelection;
import org.hibernate.sql.ast.tree.from.TableGroup;
import org.hibernate.sql.ast.tree.from.TableReference;
import org.hibernate.sql.results.graph.DomainResultCreationState;
import org.hibernate.sql.results.graph.Fetch;
import org.hibernate.sql.results.graph.FetchParent;

View File

@ -10,7 +10,6 @@ import org.hibernate.query.results.internal.DomainResultCreationStateImpl;
import org.hibernate.query.results.internal.ResultsHelper;
import org.hibernate.spi.NavigablePath;
import org.hibernate.sql.ast.spi.SqlSelection;
import org.hibernate.sql.ast.tree.from.TableGroup;
import org.hibernate.sql.ast.tree.from.TableReference;
import org.hibernate.sql.results.graph.DomainResultCreationState;
import org.hibernate.sql.results.graph.basic.BasicResult;

View File

@ -6,7 +6,6 @@ package org.hibernate.query.results.internal.complete;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.metamodel.mapping.BasicValuedMapping;
import org.hibernate.metamodel.mapping.JdbcMapping;
import org.hibernate.query.results.ResultBuilder;
import org.hibernate.query.results.internal.DomainResultCreationStateImpl;
import org.hibernate.query.results.internal.ResultSetMappingSqlSelection;

View File

@ -4,7 +4,6 @@
*/
package org.hibernate.query.results.internal.complete;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.mapping.EntityValuedModelPart;
import org.hibernate.metamodel.mapping.ModelPart;

View File

@ -14,7 +14,6 @@ import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.spi.NavigablePath;
import org.hibernate.sql.ast.spi.SqlAstCreationState;
import org.hibernate.sql.results.graph.AssemblerCreationState;
import org.hibernate.sql.results.graph.DomainResultAssembler;
import org.hibernate.sql.results.graph.DomainResultCreationState;
import org.hibernate.sql.results.graph.Fetch;
import org.hibernate.sql.results.graph.Fetchable;

View File

@ -20,7 +20,6 @@ import org.hibernate.query.results.internal.DomainResultCreationStateImpl;
import org.hibernate.query.results.internal.ResultsHelper;
import org.hibernate.spi.NavigablePath;
import org.hibernate.sql.ast.SqlAstJoinType;
import org.hibernate.sql.ast.spi.SqlAliasBase;
import org.hibernate.sql.ast.spi.SqlAliasBaseConstant;
import org.hibernate.sql.ast.tree.from.TableGroup;
import org.hibernate.sql.ast.tree.from.TableGroupJoin;

View File

@ -9,7 +9,6 @@ import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.metamodel.mapping.BasicValuedMapping;
import org.hibernate.query.results.internal.ResultSetMappingSqlSelection;
import org.hibernate.query.results.internal.ResultsHelper;
import org.hibernate.resource.beans.spi.ManagedBean;
import org.hibernate.resource.beans.spi.ManagedBeanRegistry;
import org.hibernate.resource.beans.spi.ProvidedInstanceManagedBeanImpl;
import org.hibernate.sql.ast.spi.SqlAstCreationState;
@ -21,9 +20,7 @@ import org.hibernate.sql.results.jdbc.spi.JdbcValuesMetadata;
import org.hibernate.type.BasicType;
import org.hibernate.type.descriptor.converter.internal.JpaAttributeConverterImpl;
import org.hibernate.type.descriptor.converter.spi.BasicValueConverter;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry;
import org.hibernate.type.spi.TypeConfiguration;
import java.util.Objects;

View File

@ -12,7 +12,6 @@ import org.hibernate.query.results.internal.DomainResultCreationStateImpl;
import org.hibernate.query.results.internal.ResultsHelper;
import org.hibernate.spi.NavigablePath;
import org.hibernate.sql.ast.spi.SqlSelection;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.sql.ast.tree.from.TableGroup;
import org.hibernate.sql.results.graph.DomainResultCreationState;
import org.hibernate.sql.results.graph.FetchParent;
@ -20,7 +19,6 @@ import org.hibernate.sql.results.graph.basic.BasicFetch;
import org.hibernate.sql.results.jdbc.spi.JdbcValuesMetadata;
import java.util.function.BiConsumer;
import java.util.function.Function;
import static org.hibernate.query.results.internal.ResultsHelper.impl;

View File

@ -7,7 +7,6 @@ package org.hibernate.query.results.internal.implicit;
import org.hibernate.metamodel.mapping.BasicValuedModelPart;
import org.hibernate.query.results.ResultBuilder;
import org.hibernate.query.results.ResultBuilderBasicValued;
import org.hibernate.query.results.internal.DomainResultCreationStateImpl;
import org.hibernate.query.results.internal.ResultsHelper;
import org.hibernate.spi.NavigablePath;
import org.hibernate.sql.ast.tree.from.TableGroup;

View File

@ -6,6 +6,7 @@ package org.hibernate.orm.test.function.array;
import java.util.List;
import org.hibernate.dialect.HSQLDialect;
import org.hibernate.query.criteria.JpaCriteriaQuery;
import org.hibernate.query.criteria.JpaRoot;
import org.hibernate.query.sqm.NodeBuilder;
@ -17,6 +18,7 @@ import org.hibernate.testing.orm.junit.DomainModel;
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;
@ -126,6 +128,8 @@ public class ArrayToStringTest {
}
@Test
@SkipForDialect( dialectClass = HSQLDialect.class, majorVersion = 2, minorVersion = 7, microVersion = 2,
reason = "Needs at least 2.7.3 due to the change in HSQLArrayToStringFunction that introduced a cast")
public void testStr(SessionFactoryScope scope) {
scope.inSession( em -> {
List<String> results = em.createQuery( "select str(e.theArray) from EntityWithArrays e order by e.id", String.class )

View File

@ -14,9 +14,12 @@ 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.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* @author Jan Schatteman
*/
@ -24,29 +27,55 @@ import org.junit.jupiter.api.Test;
annotatedClasses = {BooleanArrayToStringTest.TestEntity.class}
)
@SessionFactory
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsTypedArrays.class)
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayToString.class)
@SkipForDialect( dialectClass = OracleDialect.class, reason = "External driver fix required")
@Jira( value = "https://hibernate.atlassian.net/browse/HHH-18765" )
public class BooleanArrayToStringTest {
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsTypedArrays.class)
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayToString.class)
@SkipForDialect( dialectClass = OracleDialect.class, reason = "External driver fix required")
@Jira( value = "https://hibernate.atlassian.net/browse/HHH-18765" )
public void testBooleanArrayToStringFunction(SessionFactoryScope scope) {
@BeforeEach
public void setup(SessionFactoryScope scope) {
scope.inTransaction(
session -> session.persist( new TestEntity(1L, new Boolean[]{Boolean.FALSE, Boolean.FALSE, null, Boolean.TRUE}) )
);
scope.inTransaction(
session -> {
String s = session.createQuery( "select array_to_string(t.theBoolean, ';', 'null') "
+ "from TestEntity t", String.class ).getSingleResult();
Assertions.assertEquals("false;false;null;true", s);
}
session -> session.persist(
new TestEntity( 1L, new Boolean[] {Boolean.FALSE, Boolean.FALSE, null, Boolean.TRUE} ) )
);
}
@AfterEach
public void tearDown(SessionFactoryScope scope) {
scope.inTransaction(
session -> session.createMutationQuery( "delete from TestEntity" ).executeUpdate()
);
}
@Test
public void testBooleanArrayToStringWithDefault(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
final String actual = session.createQuery(
"select array_to_string(t.theBoolean, ';', 'null') from TestEntity t",
String.class
)
.getSingleResult();
assertEquals("false;false;null;true", actual);
}
);
}
@Test
public void testBooleanArrayToStringWithoutDefault(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
final String actual = session.createQuery(
"select array_to_string(t.theBoolean, ';') from TestEntity t",
String.class
)
.getSingleResult();
assertEquals("false;false;true", actual);
}
);
}
@Entity(name = "TestEntity")
public static class TestEntity {
@Id