diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQueryGroup.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQueryGroup.java index f90c532e59b7..9f2fdd738d98 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQueryGroup.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQueryGroup.java @@ -170,7 +170,8 @@ private void validateQueryGroupFetchStructure(List> ty for ( int j = 0; j < firstSelectionSize; j++ ) { final SqmTypedNode firstSqmSelection = typedNodes.get( j ); final JavaType firstJavaType = firstSqmSelection.getNodeJavaType(); - if ( firstJavaType != selections.get( j ).getNodeJavaType() ) { + final JavaType nodeJavaType = selections.get( j ).getNodeJavaType(); + if ( nodeJavaType != null && firstJavaType != null && firstJavaType != nodeJavaType ) { throw new SemanticException( "Select items of the same index must have the same java type across all query parts" ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesMappingProducerProviderStandard.java b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesMappingProducerProviderStandard.java index fa95470f6d4f..151bdb7fc1d8 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesMappingProducerProviderStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesMappingProducerProviderStandard.java @@ -9,9 +9,15 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.results.ResultSetMapping; import org.hibernate.query.results.ResultSetMappingImpl; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.select.QueryGroup; +import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducer; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducerProvider; +import org.hibernate.type.descriptor.jdbc.NullJdbcType; + +import java.util.List; /** * Standard JdbcValuesMappingProducerProvider implementation @@ -28,10 +34,20 @@ public class JdbcValuesMappingProducerProviderStandard implements JdbcValuesMapp public JdbcValuesMappingProducer buildMappingProducer( SelectStatement sqlAst, SessionFactoryImplementor sessionFactory) { - return new JdbcValuesMappingProducerStandard( - sqlAst.getQuerySpec().getSelectClause().getSqlSelections(), - sqlAst.getDomainResultDescriptors() - ); + return new JdbcValuesMappingProducerStandard( getSelections( sqlAst ), sqlAst.getDomainResultDescriptors() ); + } + + private static List getSelections(SelectStatement selectStatement) { + if ( selectStatement.getQueryPart() instanceof QueryGroup ) { + final QueryGroup queryGroup = (QueryGroup) selectStatement.getQueryPart(); + for ( QueryPart queryPart : queryGroup.getQueryParts() ) { + if ( !(queryPart.getFirstQuerySpec().getSelectClause().getSqlSelections() + .get( 0 ).getExpressionType().getSingleJdbcMapping().getJdbcType() instanceof NullJdbcType) ) { + return queryPart.getFirstQuerySpec().getSelectClause().getSqlSelections(); + } + } + } + return selectStatement.getQuerySpec().getSelectClause().getSqlSelections(); } @Override diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/UnionAllSelectNullTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/UnionAllSelectNullTest.java new file mode 100644 index 000000000000..519be3f4c403 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/UnionAllSelectNullTest.java @@ -0,0 +1,140 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.hql; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Tuple; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + UnionAllSelectNullTest.TestEntity.class, + UnionAllSelectNullTest.AnotherTestEntity.class + } +) +@SessionFactory +@JiraKey("HHH-18720") +class UnionAllSelectNullTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist( new TestEntity( 1L, "a" ) ); + session.persist( new AnotherTestEntity( 2L, "b" ) ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createQuery( "delete TestEntity" ).executeUpdate(); + session.createQuery( "delete AnotherTestEntity" ).executeUpdate(); + } + ); + } + + @Test + void testSelect(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + List resultList = session.createQuery( + "SELECT te.id as id from TestEntity te" + + " union all SELECT null as id from AnotherTestEntity ate" + , Tuple.class ) + .getResultList(); + assertThat( resultList.size() ).isEqualTo( 2 ); + assertResultIsCorrect( resultList, 1L, null ); + } + ); + } + + @Test + void testSelect2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + List resultList = session.createQuery( + "SELECT null as id from TestEntity te" + + " union all SELECT ate.id as id from AnotherTestEntity ate" + , Tuple.class ) + .getResultList(); + assertThat( resultList.size() ).isEqualTo( 2 ); + assertResultIsCorrect( resultList, null, 2L ); + } + ); + } + + @Test + void testSelect3(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + List resultList = session.createQuery( + "SELECT te.id as id from TestEntity te" + + " union all SELECT ate.id as id from AnotherTestEntity ate" + , Tuple.class ) + .getResultList(); + assertThat( resultList.size() ).isEqualTo( 2 ); + assertResultIsCorrect( resultList, 1L, 2L ); + } + ); + } + + private static void assertResultIsCorrect(List resultList, Long id1, Long id2) { + Set ids = new HashSet<>( 2 ); + ids.add( (Long) resultList.get( 0 ).get( "id" ) ); + ids.add( (Long) resultList.get( 1 ).get( "id" ) ); + assertThat( ids.contains( id1 ) ).as( "Result does not contain expected value:" + id1 ).isTrue(); + assertThat( ids.contains( id2 ) ).as( "Result does not contain expected value:" + id2 ).isTrue(); + } + + @Entity(name = "TestEntity") + public static class TestEntity { + + @Id + private Long id; + + private String name; + + public TestEntity() { + } + + public TestEntity(Long id, String name) { + this.id = id; + this.name = name; + } + } + + @Entity(name = "AnotherTestEntity") + public static class AnotherTestEntity { + + @Id + private Long id; + + private String name; + + public AnotherTestEntity() { + } + + public AnotherTestEntity(Long id, String name) { + this.id = id; + this.name = name; + } + } +}