From 112dbbc8bbb09cfdcd6f258527286a1d4ef168a8 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Thu, 23 Oct 2025 10:27:22 +0200 Subject: [PATCH 1/2] HHH-19868 Add test for issue --- .../query/SelectWithWrongResultTypeTest.java | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectWithWrongResultTypeTest.java diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectWithWrongResultTypeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectWithWrongResultTypeTest.java new file mode 100644 index 000000000000..24221cc48405 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectWithWrongResultTypeTest.java @@ -0,0 +1,205 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; +import jakarta.persistence.Version; +import org.hibernate.InstantiationException; +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.ArrayList; +import java.util.List; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@DomainModel( + annotatedClasses = { + SelectWithWrongResultTypeTest.Element.class, + SelectWithWrongResultTypeTest.Node.class, + } +) +@SessionFactory +@JiraKey("HHH-19868") +public class SelectWithWrongResultTypeTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Node basik = new Node( "Child" ); + basik.parent = new Node( "Parent" ); + basik.elements.add( new Element( basik ) ); + basik.elements.add( new Element( basik ) ); + basik.elements.add( new Element( basik ) ); + + session.persist( basik ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Test + void testSelectWithWrongResultType(SessionFactoryScope scope) { + assertThrows( InstantiationException.class, () -> scope.inTransaction( session -> { + List resultList = session + .createSelectionQuery( "select distinct n, e from Node n join n.elements e", Node.class ) + .getResultList(); + assertThat( resultList ).hasSize( 3 ); + } ) ); + + assertThrows( InstantiationException.class, () -> scope.inTransaction( session -> { + List resultList = session + .createSelectionQuery( "select distinct n.id, e.id from Node n join n.elements e", Node.class ) + .getResultList(); + assertThat( resultList ).hasSize( 3 ); + } ) ); + + assertThrows( InstantiationException.class, () -> scope.inTransaction( session -> { + List resultList = session + .createSelectionQuery( + "select max(e.id), min(e.id), sum(e.id) from Node n join n.elements e group by n.id order by n.id", + Node.class ) + .getResultList(); + assertThat( resultList ).hasSize( 1 ); + } ) ); + } + + @Test + void testSelect(SessionFactoryScope scope) { + + scope.inTransaction( session -> { + List resultList = session + .createSelectionQuery( "select distinct n from Node n left join fetch n.elements", Node.class ) + .getResultList(); + assertThat( resultList ).hasSize( 2 ); + } ); + scope.inTransaction( session -> { + List resultList = session + .createSelectionQuery( "select distinct n, e from Node n join n.elements e", Tuple.class ) + .getResultList(); + assertThat( resultList ).hasSize( 3 ); + } ); + scope.inTransaction( session -> { + List resultList = session + .createSelectionQuery( "select distinct n.id, e.id from Node n join n.elements e", Tuple.class ) + .getResultList(); + assertThat( resultList ).hasSize( 3 ); + } ); + scope.inTransaction( session -> { + List resultList = session + .createSelectionQuery( + "select max(e.id), min(e.id), sum(e.id) from Node n join n.elements e group by n.id order by n.id", + Tuple.class ) + .getResultList(); + assertThat( resultList ).hasSize( 1 ); + } ); + } + + @Entity(name = "Element") + @Table(name = "Element") + public static class Element { + @Id + @GeneratedValue + Integer id; + + @ManyToOne + Node node; + + public Element(Node node) { + this.node = node; + } + + public Element() { + } + } + + @Entity(name = "Node") + @Table(name = "Node") + public static class Node { + + @Id + @GeneratedValue + Integer id; + + @Version + Integer version; + + String string; + + @ManyToOne(fetch = FetchType.LAZY, + cascade = CascadeType.PERSIST) + Node parent; + + @OneToMany(fetch = FetchType.EAGER, + cascade = CascadeType.PERSIST, + mappedBy = "node") + List elements = new ArrayList<>(); + + public Node(String string) { + this.string = string; + } + + public Node() { + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getString() { + return string; + } + + public void setString(String string) { + this.string = string; + } + + @Override + public String toString() { + return id + ": " + string; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Node node = (Node) o; + return Objects.equals( string, node.string ); + } + + @Override + public int hashCode() { + return Objects.hash( string ); + } + } +} From 7ab3c7271f18f2a7ead634f07e13326f2e9b9138 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Thu, 23 Oct 2025 10:28:24 +0200 Subject: [PATCH 2/2] HHH-19868 RowTransformerConstructorImpl throws NullPointerException when TupleMetadata is null --- .../query/sqm/internal/AbstractSqmSelectionQuery.java | 5 +++-- .../results/internal/RowTransformerConstructorImpl.java | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java index 3b3b0dabe489..520cbf830274 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java @@ -316,7 +316,7 @@ protected TupleMetadata buildTupleMetadata(SqmStatement statement, Class r if ( statement instanceof SqmSelectStatement select ) { final var selections = select.getQueryPart().getFirstQuerySpec().getSelectClause().getSelections(); - return isTupleMetadataRequired( resultType, selections.get(0) ) + return isTupleMetadataRequired( resultType, selections ) ? getTupleMetadata( selections ) : null; } @@ -325,7 +325,8 @@ protected TupleMetadata buildTupleMetadata(SqmStatement statement, Class r } } - private static boolean isTupleMetadataRequired(Class resultType, SqmSelection selection) { + private static boolean isTupleMetadataRequired(Class resultType, List> selections) { + final var selection = selections.size() == 1 ? selections.get( 0 ) : null; return isHqlTuple( selection ) || !isInstantiableWithoutMetadata( resultType ) && !isSelectionAssignableToResultType( selection, resultType ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerConstructorImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerConstructorImpl.java index ecbe16fd9b33..2ba34b88003e 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerConstructorImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerConstructorImpl.java @@ -6,15 +6,14 @@ import jakarta.persistence.TupleElement; import org.hibernate.InstantiationException; +import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.query.sqm.tree.SqmExpressibleAccessor; import org.hibernate.sql.results.spi.RowTransformer; +import org.hibernate.type.spi.TypeConfiguration; import java.lang.reflect.Constructor; import java.util.List; -import org.hibernate.query.sqm.SqmExpressible; -import org.hibernate.query.sqm.tree.SqmExpressibleAccessor; -import org.hibernate.type.spi.TypeConfiguration; - import static java.util.stream.Collectors.toList; import static org.hibernate.sql.results.graph.instantiation.internal.InstantiationHelper.findMatchingConstructor; @@ -32,6 +31,7 @@ public RowTransformerConstructorImpl( TupleMetadata tupleMetadata, TypeConfiguration typeConfiguration) { this.type = type; + assert tupleMetadata != null : "TupleMetadata must not be null"; final List> elements = tupleMetadata.getList(); final List> argumentTypes = elements.stream() .map( RowTransformerConstructorImpl::resolveElementJavaType )