From 9caac5cfd2cd1dab5ac76b98fac886acef5f2a18 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Sat, 20 Apr 2024 16:27:22 +0200 Subject: [PATCH 1/2] HHH-17956 Add test for issue --- .../criteria/MultiSelectResultTypeTest.java | 114 +++++++++++++ .../sql/exec/EntityWithEmbeddedIdTest.java | 152 ++++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/MultiSelectResultTypeTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/EntityWithEmbeddedIdTest.java diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/MultiSelectResultTypeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/MultiSelectResultTypeTest.java new file mode 100644 index 000000000000..d204ef745dab --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/MultiSelectResultTypeTest.java @@ -0,0 +1,114 @@ +package org.hibernate.orm.test.jpa.criteria; + +import java.util.List; + +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@Jpa( + annotatedClasses = { + MultiSelectResultTypeTest.TestEntity.class + } +) +@JiraKey("HHH-17956") +public class MultiSelectResultTypeTest { + + @BeforeAll + public static void setUp(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + TestEntity testEntity = new TestEntity( 1, "a" ); + entityManager.persist( testEntity ); + } + ); + } + + @Test + public void testResultOfMultiSelect(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery q = cb.createQuery( Integer[].class ); + Root r = q.from( TestEntity.class ); + q.multiselect( List.of( r.get( "id" ), r.get( "id" ) ) ); + List idPairs = entityManager.createQuery( q ).getResultList(); + assertThat( idPairs.size() ).isEqualTo( 1 ); + Integer[] ids = idPairs.get( 0 ); + } + ); + } + + @Test + public void testResultOfMultiSelectPrimitive(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery q = cb.createQuery( int[].class ); + Root r = q.from( TestEntity.class ); + q.multiselect( List.of( r.get( "id" ), r.get( "id" ) ) ); + List idPairs = entityManager.createQuery( q ).getResultList(); + assertThat( idPairs.size() ).isEqualTo( 1 ); + int[] ids = idPairs.get( 0 ); + } + ); + } + + @Test + public void testResultOfMultiSelect2(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery q = cb.createQuery( Object[].class ); + Root r = q.from( TestEntity.class ); + q.multiselect( List.of( r.get( "id" ), r.get( "name" ) ) ); + List values = entityManager.createQuery( q ).getResultList(); + assertThat( values.size() ).isEqualTo( 1 ); + Object[] value = values.get( 0 ); + Integer id = (Integer) value[0]; + String name = (String) value[1]; + } + ); + } + + @Test + public void testResultOfSelect(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery q = cb.createQuery( Integer.class ); + Root r = q.from( TestEntity.class ); + q.select( r.get( "id" ) ); + List idPairs = entityManager.createQuery( q ).getResultList(); + assertThat( idPairs.size() ).isEqualTo( 1 ); + Integer id = idPairs.get( 0 ); + } + ); + } + + @Entity(name = "TestEntity") + public static class TestEntity { + @Id + private Integer id; + + private String name; + + public TestEntity() { + } + + public TestEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/EntityWithEmbeddedIdTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/EntityWithEmbeddedIdTest.java new file mode 100644 index 000000000000..675c968cf06c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/EntityWithEmbeddedIdTest.java @@ -0,0 +1,152 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.sql.exec; + +import java.io.Serializable; + +import org.hibernate.stat.spi.StatisticsImplementor; + +import org.hibernate.testing.orm.junit.DomainModel; +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 jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@DomainModel( + annotatedClasses = { + EntityWithEmbeddedIdTest.TestEntity.class + } +) +@SessionFactory(generateStatistics = true) +public class EntityWithEmbeddedIdTest { + + private PK entityId; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + final TestEntity entity = new TestEntity(); + entityId = new PK( 25, "Acme" ); + scope.inTransaction( + session -> { + entity.setId( entityId ); + entity.setData( "test" ); + session.save( entity ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + sesison -> + sesison.createQuery( "delete from TestEntity" ).executeUpdate() + ); + } + + @Test + public void testHqlSelectOnlyTheEmbeddedId(SessionFactoryScope scope) { + StatisticsImplementor statistics = scope.getSessionFactory().getStatistics(); + statistics.clear(); + scope.inTransaction( + session -> { + final PK value = session.createQuery( + "select e.id FROM TestEntity e", + PK.class + ).uniqueResult(); + assertThat( value, equalTo( entityId ) ); + } + ); + assertThat( statistics.getPrepareStatementCount(), is( 1L ) ); + } + + @Entity(name = "TestEntity") + public static class TestEntity { + + @EmbeddedId + PK id; + + private String data; + + public PK getId() { + return id; + } + + public void setId(PK id) { + this.id = id; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + } + + public static class PK implements Serializable { + private Integer value1; + private String value2; + + public PK() { + } + + public PK(Integer value1, String value2) { + this.value1 = value1; + this.value2 = value2; + } + + public Integer getValue1() { + return value1; + } + + public void setValue1(Integer value1) { + this.value1 = value1; + } + + public String getValue2() { + return value2; + } + + public void setValue2(String value2) { + this.value2 = value2; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + + final PK pk = (PK) o; + + if ( value1 != null ? !value1.equals( pk.getValue1() ) : pk.getValue1() != null ) { + return false; + } + return value2 != null ? value2.equals( pk.getValue2() ) : pk.getValue2() == null; + } + + @Override + public int hashCode() { + int result = value1 != null ? value1.hashCode() : 0; + result = 31 * result + ( value2 != null ? value2.hashCode() : 0 ); + return result; + } + } + +} From d7a1a77d997e0b4f24d68ebbf3fee03795a5f3e6 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Mon, 22 Apr 2024 10:58:17 +0200 Subject: [PATCH 2/2] HHH-17956 Criteria multiselect ignores type of the criteria query and always returns list of Object[] --- .../asciidoc/querylanguage/Expressions.adoc | 6 +- .../function/SumReturnTypeResolver.java | 2 +- .../AbstractSharedSessionContract.java | 10 +- .../query/spi/AbstractSelectionQuery.java | 136 ++++++++----- .../spi/DomainQueryExecutionContext.java | 4 + .../internal/ConcreteSqmSelectQueryPlan.java | 2 + .../query/sqm/internal/QuerySqmImpl.java | 40 ++-- .../sqm/internal/SqmCriteriaNodeBuilder.java | 22 ++- .../domain/SqmElementAggregateFunction.java | 48 ++++- .../domain/SqmIndexAggregateFunction.java | 48 +++++ .../results/internal/StandardRowReader.java | 187 ++++++++++++++++-- .../sql/results/spi/ListResultsConsumer.java | 23 ++- .../hibernate/sql/results/spi/RowReader.java | 11 +- .../function/array/ArrayAggregateTest.java | 2 +- ...tanceMappedSuperclassAndGenericsTest.java} | 20 +- .../criteria/MultiSelectResultTypeTest.java | 49 ++++- .../TemporalParameterPlusDurationTest.java | 24 +-- .../orm/test/query/hql/FunctionTests.java | 2 +- 18 files changed, 497 insertions(+), 139 deletions(-) rename hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/embeddable/{EmbeddableInheritanceMappedSuperclassAdnGenericsTest.java => EmbeddableInheritanceMappedSuperclassAndGenericsTest.java} (91%) diff --git a/documentation/src/main/asciidoc/querylanguage/Expressions.adoc b/documentation/src/main/asciidoc/querylanguage/Expressions.adoc index 9805d82280db..3778f5940a18 100644 --- a/documentation/src/main/asciidoc/querylanguage/Expressions.adoc +++ b/documentation/src/main/asciidoc/querylanguage/Expressions.adoc @@ -740,9 +740,9 @@ The following functions are abbreviations for `extract()`: | `year(x)` | `extract(year from x)` | ✖ | `month(x)` | `extract(month from x)` | ✖ | `day(x)` | `extract(day from x)` | ✖ -| `hour(x)` | `extract(year from x)` | ✖ -| `minute(x)` | `extract(year from x)` | ✖ -| `second(x)` | `extract(year from x)` | ✖ +| `hour(x)` | `extract(hour from x)` | ✖ +| `minute(x)` | `extract(minute from x)` | ✖ +| `second(x)` | `extract(second from x)` | ✖ |=== TIP: These abbreviations aren't part of the JPQL standard, but on the other hand they're a lot less verbose. diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/SumReturnTypeResolver.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/SumReturnTypeResolver.java index 5994e8a8187e..908188d97b43 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/SumReturnTypeResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/SumReturnTypeResolver.java @@ -41,7 +41,7 @@ * * @author Christian Beikov */ -class SumReturnTypeResolver implements FunctionReturnTypeResolver { +public class SumReturnTypeResolver implements FunctionReturnTypeResolver { private final BasicType longType; private final BasicType doubleType; diff --git a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java index 22815de52f64..08a033f3f1dc 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -115,6 +115,8 @@ import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.CriteriaUpdate; import org.hibernate.stat.spi.StatisticsImplementor; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.java.spi.UnknownBasicJavaType; import static java.lang.Boolean.TRUE; import static org.hibernate.internal.util.ReflectHelper.isClass; @@ -962,8 +964,7 @@ else if ( getFactory().getMappingMetamodel().isEntityClass( resultClass ) ) { query.addEntity( resultClass, LockMode.READ ); } else if ( resultClass != Object.class && resultClass != Object[].class ) { - if ( isClass( resultClass ) - && getTypeConfiguration().getJavaTypeRegistry().findDescriptor( resultClass ) == null ) { + if ( isClass( resultClass ) && !hasJavaTypeDescriptor( resultClass ) ) { // not a basic type query.setTupleTransformer( new NativeQueryConstructorTransformer<>( resultClass ) ); } @@ -973,6 +974,11 @@ && getTypeConfiguration().getJavaTypeRegistry().findDescriptor( resultClass ) == } } + private boolean hasJavaTypeDescriptor(Class resultClass) { + final JavaType descriptor = getTypeConfiguration().getJavaTypeRegistry().findDescriptor( resultClass ); + return descriptor != null && descriptor.getClass() != UnknownBasicJavaType.class; + } + @Override public NativeQueryImplementor createNativeQuery(String sqlString, Class resultClass, String tableAlias) { @SuppressWarnings("unchecked") diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java index 6ebb9322fc50..620f837c532e 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java @@ -30,6 +30,7 @@ import jakarta.persistence.Tuple; import jakarta.persistence.TupleElement; import jakarta.persistence.criteria.CompoundSelection; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.CacheMode; import org.hibernate.FlushMode; @@ -260,24 +261,24 @@ protected void visitQueryReturnType( SqmQueryPart queryPart, Class expectedResultType, SessionFactoryImplementor factory) { - assert getQueryString().equals( CRITERIA_HQL_STRING ); - if ( queryPart instanceof SqmQuerySpec ) { final SqmQuerySpec sqmQuerySpec = (SqmQuerySpec) queryPart; final List> sqmSelections = sqmQuerySpec.getSelectClause().getSelections(); - if ( sqmSelections == null || sqmSelections.isEmpty() ) { - // make sure there is at least one root - final List> sqmRoots = sqmQuerySpec.getFromClause().getRoots(); - if ( sqmRoots == null || sqmRoots.isEmpty() ) { - throw new IllegalArgumentException( "Criteria did not define any query roots" ); - } - // if there is a single root, use that as the selection - if ( sqmRoots.size() == 1 ) { - sqmQuerySpec.getSelectClause().add( sqmRoots.get( 0 ), null ); - } - else { - throw new IllegalArgumentException( "Criteria has multiple query roots" ); + if ( getQueryString() == CRITERIA_HQL_STRING ) { + if ( sqmSelections == null || sqmSelections.isEmpty() ) { + // make sure there is at least one root + final List> sqmRoots = sqmQuerySpec.getFromClause().getRoots(); + if ( sqmRoots == null || sqmRoots.isEmpty() ) { + throw new IllegalArgumentException( "Criteria did not define any query roots" ); + } + // if there is a single root, use that as the selection + if ( sqmRoots.size() == 1 ) { + sqmQuerySpec.getSelectClause().add( sqmRoots.get( 0 ), null ); + } + else { + throw new IllegalArgumentException( "Criteria has multiple query roots" ); + } } } @@ -302,28 +303,71 @@ protected static void checkQueryReturnType( if ( selections.size() == 1 ) { // we have one item in the select list, // the type has to match (no instantiation) - final SqmSelection sqmSelection = selections.get(0); - - // special case for parameters in the select list - final SqmSelectableNode selection = sqmSelection.getSelectableNode(); - if ( selection instanceof SqmParameter ) { - final SqmParameter sqmParameter = (SqmParameter) selection; - final SqmExpressible nodeType = sqmParameter.getNodeType(); - // we may not yet know a selection type - if ( nodeType == null || nodeType.getExpressibleJavaType() == null ) { - // we can't verify the result type up front - return; + final SqmSelection sqmSelection = selections.get( 0 ); + final SqmSelectableNode selectableNode = sqmSelection.getSelectableNode(); + if ( selectableNode.isCompoundSelection() ) { + final Class expectedSelectItemType = expectedResultClass.isArray() + ? expectedResultClass.getComponentType() + : expectedResultClass; + for ( JpaSelection selection : selectableNode.getSelectionItems() ) { + verifySelectionType( expectedSelectItemType, sessionFactory, (SqmSelectableNode) selection ); } } - - if ( !sessionFactory.getSessionFactoryOptions().getJpaCompliance().isJpaQueryComplianceEnabled() ) { - verifyResultType( expectedResultClass, sqmSelection.getExpressible() ); + else { + verifySelectionType( expectedResultClass, sessionFactory, sqmSelection.getSelectableNode() ); + } + } + else if ( expectedResultClass.isArray() ) { + final Class componentType = expectedResultClass.getComponentType(); + for ( SqmSelection selection : selections ) { + verifySelectionType( componentType, sessionFactory, selection.getSelectableNode() ); } } // else, let's assume we can instantiate it! } } + private static void verifySelectionType( + Class expectedResultClass, + SessionFactoryImplementor sessionFactory, + SqmSelection sqmSelection) { + // special case for parameters in the select list + final SqmSelectableNode selection = sqmSelection.getSelectableNode(); + if ( selection instanceof SqmParameter ) { + final SqmParameter sqmParameter = (SqmParameter) selection; + final SqmExpressible nodeType = sqmParameter.getNodeType(); + // we may not yet know a selection type + if ( nodeType == null || nodeType.getExpressibleJavaType() == null ) { + // we can't verify the result type up front + return; + } + } + + if ( !sessionFactory.getSessionFactoryOptions().getJpaCompliance().isJpaQueryComplianceEnabled() ) { + verifyResultType( expectedResultClass, selection.getExpressible() ); + } + } + + private static void verifySelectionType( + Class expectedResultClass, + SessionFactoryImplementor sessionFactory, + SqmSelectableNode selection) { + // special case for parameters in the select list + if ( selection instanceof SqmParameter ) { + final SqmParameter sqmParameter = (SqmParameter) selection; + final SqmExpressible nodeType = sqmParameter.getExpressible(); + // we may not yet know a selection type + if ( nodeType == null || nodeType.getExpressibleJavaType() == null ) { + // we can't verify the result type up front + return; + } + } + + if ( !sessionFactory.getSessionFactoryOptions().getJpaCompliance().isJpaQueryComplianceEnabled() ) { + verifyResultType( expectedResultClass, selection.getExpressible() ); + } + } + private static boolean isInstantiableWithoutMetadata(Class resultType) { return resultType == null || resultType.isArray() @@ -334,27 +378,29 @@ private static boolean isInstantiableWithoutMetadata(Class resultType) { private static boolean isResultTypeAlwaysAllowed(Class expectedResultClass) { return expectedResultClass == null || expectedResultClass == Object.class + || expectedResultClass == Object[].class || expectedResultClass == List.class - || expectedResultClass == Tuple.class - || expectedResultClass.isArray(); - } - - protected static void verifyResultType(Class resultClass, SqmExpressible sqmExpressible) { - assert sqmExpressible != null; - final JavaType expressibleJavaType = sqmExpressible.getExpressibleJavaType(); - assert expressibleJavaType != null; - final Class javaTypeClass = expressibleJavaType.getJavaTypeClass(); - if ( !resultClass.isAssignableFrom( javaTypeClass ) ) { - if ( expressibleJavaType instanceof PrimitiveJavaType ) { - final PrimitiveJavaType javaType = (PrimitiveJavaType) expressibleJavaType; - if ( javaType.getPrimitiveClass() != resultClass ) { + || expectedResultClass == Map.class + || expectedResultClass == Tuple.class; + } + + protected static void verifyResultType(Class resultClass, @Nullable SqmExpressible sqmExpressible) { + if ( sqmExpressible != null ) { + final JavaType expressibleJavaType = sqmExpressible.getExpressibleJavaType(); + assert expressibleJavaType != null; + final Class javaTypeClass = expressibleJavaType.getJavaTypeClass(); + if ( javaTypeClass != Object.class && !resultClass.isAssignableFrom( javaTypeClass ) ) { + if ( expressibleJavaType instanceof PrimitiveJavaType ) { + final PrimitiveJavaType javaType = (PrimitiveJavaType) expressibleJavaType; + if ( javaType.getPrimitiveClass() != resultClass ) { + throwQueryTypeMismatchException( resultClass, sqmExpressible ); + } + } + else if ( !isMatchingDateType( javaTypeClass, resultClass, sqmExpressible ) ) { throwQueryTypeMismatchException( resultClass, sqmExpressible ); } + // else special case, we are good } - else if ( !isMatchingDateType( javaTypeClass, resultClass, sqmExpressible ) ) { - throwQueryTypeMismatchException( resultClass, sqmExpressible ); - } - // else special case, we are good } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/DomainQueryExecutionContext.java b/hibernate-core/src/main/java/org/hibernate/query/spi/DomainQueryExecutionContext.java index aaafafaff185..9b2bd5a0310e 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/DomainQueryExecutionContext.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/DomainQueryExecutionContext.java @@ -38,4 +38,8 @@ default boolean hasCallbackActions() { * The underlying session */ SharedSessionContractImplementor getSession(); + + default Class getResultType() { + return null; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java index 5d92c9aa7c23..d9cf9cc5f5f3 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java @@ -137,11 +137,13 @@ public ConcreteSqmSelectQueryPlan( jdbcParameterBindings ); session.autoFlushIfRequired( jdbcSelect.getAffectedTableNames(), true ); + //noinspection unchecked return session.getFactory().getJdbcServices().getJdbcSelectExecutor().list( jdbcSelect, jdbcParameterBindings, listInterpreterExecutionContext( hql, executionContext, jdbcSelect, subSelectFetchKeyHandler ), rowTransformer, + (Class) executionContext.getResultType(), uniqueSemantic ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java index 1ec6ed49a815..c56c70b6520d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java @@ -251,34 +251,7 @@ public QuerySqmImpl( bindCriteriaParameter((SqmJpaCriteriaParameterWrapper) sqmParameter); } } - - if ( sqm instanceof SqmSelectStatement ) { - SqmUtil.verifyIsSelectStatement( sqm, null ); - final SqmQueryPart queryPart = ( (SqmSelectStatement) sqm ).getQueryPart(); - // For criteria queries, we have to validate the fetch structure here - queryPart.validateQueryStructureAndFetchOwners(); - visitQueryReturnType( - queryPart, - expectedResultType, - producer.getFactory() - ); - } - else { - if ( expectedResultType != null ) { - throw new IllegalQueryOperationException( "Result type given for a non-SELECT Query", hql, null ); - } - if ( sqm instanceof SqmUpdateStatement ) { - final SqmUpdateStatement updateStatement = (SqmUpdateStatement) sqm; - verifyImmutableEntityUpdate( CRITERIA_HQL_STRING, updateStatement, producer.getFactory() ); - if ( updateStatement.getSetClause() == null - || updateStatement.getSetClause().getAssignments().isEmpty() ) { - throw new IllegalArgumentException( "No assignments specified as part of UPDATE criteria" ); - } - } - else if ( sqm instanceof SqmInsertStatement ) { - verifyInsertTypesMatch( CRITERIA_HQL_STRING, (SqmInsertStatement) sqm ); - } - } + validateStatement( sqm, expectedResultType ); resultType = expectedResultType; tupleMetadata = buildTupleMetadata( criteria, expectedResultType ); @@ -298,7 +271,12 @@ private void bindCriteriaParameter(SqmJpaCriteriaParameterWrapper sqmPara private void validateStatement(SqmStatement sqmStatement, Class resultType) { if ( sqmStatement instanceof SqmSelectStatement ) { - SqmUtil.verifyIsSelectStatement( sqmStatement, hql ); + final SqmQueryPart queryPart = ( (SqmSelectStatement) sqm ).getQueryPart(); + if ( hql == CRITERIA_HQL_STRING ) { + // For criteria queries, we have to validate the fetch structure here + queryPart.validateQueryStructureAndFetchOwners(); + } + visitQueryReturnType( queryPart, resultType, getSessionFactory() ); } else { if ( resultType != null ) { @@ -307,6 +285,10 @@ private void validateStatement(SqmStatement sqmStatement, Class resultType if ( sqmStatement instanceof SqmUpdateStatement ) { final SqmUpdateStatement updateStatement = (SqmUpdateStatement) sqmStatement; verifyImmutableEntityUpdate( hql, updateStatement, getSessionFactory() ); + if ( updateStatement.getSetClause() == null + || updateStatement.getSetClause().getAssignments().isEmpty() ) { + throw new IllegalArgumentException( "No assignments specified as part of UPDATE criteria" ); + } verifyUpdateTypesMatch( hql, updateStatement ); } else if ( sqmStatement instanceof SqmInsertStatement ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index 3efce6953d39..65d8569f8fb4 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java @@ -34,6 +34,8 @@ import java.util.function.Supplier; import org.hibernate.SessionFactory; +import org.hibernate.dialect.function.AvgFunction; +import org.hibernate.dialect.function.SumReturnTypeResolver; import org.hibernate.dialect.function.array.DdlTypeHelper; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.CoreLogging; @@ -81,6 +83,7 @@ import org.hibernate.query.sqm.function.NamedSqmFunctionDescriptor; import org.hibernate.query.sqm.function.SqmFunctionDescriptor; import org.hibernate.query.sqm.produce.function.FunctionArgumentException; +import org.hibernate.query.sqm.produce.function.FunctionReturnTypeResolver; import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; import org.hibernate.query.sqm.spi.SqmCreationContext; import org.hibernate.query.sqm.tree.SqmQuery; @@ -201,6 +204,8 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext, private transient BasicType integerType; private transient BasicType longType; private transient BasicType characterType; + private transient FunctionReturnTypeResolver sumReturnTypeResolver; + private transient FunctionReturnTypeResolver avgReturnTypeResolver; private final transient Map, HibernateCriteriaBuilder> extensions; public SqmCriteriaNodeBuilder( @@ -248,7 +253,6 @@ public SessionFactoryImplementor getSessionFactory() { } - @Override public BasicType getBooleanType() { final BasicType booleanType = this.booleanType; @@ -293,6 +297,22 @@ public BasicType getCharacterType() { return characterType; } + public FunctionReturnTypeResolver getSumReturnTypeResolver() { + final FunctionReturnTypeResolver resolver = sumReturnTypeResolver; + if ( resolver == null ) { + return this.sumReturnTypeResolver = new SumReturnTypeResolver( getTypeConfiguration() ); + } + return resolver; + } + + public FunctionReturnTypeResolver getAvgReturnTypeResolver() { + final FunctionReturnTypeResolver resolver = avgReturnTypeResolver; + if ( resolver == null ) { + return this.avgReturnTypeResolver = new AvgFunction.ReturnTypeResolver( getTypeConfiguration() ); + } + return resolver; + } + @Override public QueryEngine getQueryEngine() { return queryEngine; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmElementAggregateFunction.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmElementAggregateFunction.java index 29b41ba0d5b4..7fb909d8fbf6 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmElementAggregateFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmElementAggregateFunction.java @@ -6,17 +6,23 @@ */ package org.hibernate.query.sqm.tree.domain; +import java.util.List; + import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; +import org.hibernate.query.ReturnableType; import org.hibernate.query.hql.spi.SqmCreationState; import org.hibernate.query.sqm.SemanticQueryWalker; -import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.type.descriptor.java.JavaType; /** * @author Steve Ebersole */ public class SqmElementAggregateFunction extends AbstractSqmSpecificPluralPartPath { private final String functionName; + private final ReturnableType returnableType; public SqmElementAggregateFunction(SqmPath pluralDomainPath, String functionName) { //noinspection unchecked @@ -27,6 +33,46 @@ public SqmElementAggregateFunction(SqmPath pluralDomainPath, String functionN ( (PluralPersistentAttribute) pluralDomainPath.getReferencedPathSource() ).getElementPathSource() ); this.functionName = functionName; + switch ( functionName ) { + case "sum": + //noinspection unchecked + this.returnableType = (ReturnableType) nodeBuilder().getSumReturnTypeResolver() + .resolveFunctionReturnType( + null, + (SqmToSqlAstConverter) null, + List.of( pluralDomainPath ), + nodeBuilder().getTypeConfiguration() + ); + break; + case "avg": + //noinspection unchecked + this.returnableType = (ReturnableType) nodeBuilder().getAvgReturnTypeResolver() + .resolveFunctionReturnType( + null, + (SqmToSqlAstConverter) null, + List.of( pluralDomainPath ), + nodeBuilder().getTypeConfiguration() + ); + break; + default: + this.returnableType = null; + break; + } + } + + @Override + public SqmExpressible getExpressible() { + return returnableType == null ? super.getExpressible() : returnableType; + } + + @Override + public JavaType getJavaTypeDescriptor() { + return returnableType == null ? super.getJavaTypeDescriptor() : returnableType.getExpressibleJavaType(); + } + + @Override + public JavaType getNodeJavaType() { + return returnableType == null ? super.getNodeJavaType() : returnableType.getExpressibleJavaType(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmIndexAggregateFunction.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmIndexAggregateFunction.java index e8966c7361f8..e33f9844e7f6 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmIndexAggregateFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmIndexAggregateFunction.java @@ -6,17 +6,25 @@ */ package org.hibernate.query.sqm.tree.domain; +import java.util.List; + +import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; +import org.hibernate.query.ReturnableType; import org.hibernate.query.hql.spi.SqmCreationState; import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmExpressible; import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.type.descriptor.java.JavaType; /** * @author Steve Ebersole */ public class SqmIndexAggregateFunction extends AbstractSqmSpecificPluralPartPath { private final String functionName; + private final ReturnableType returnableType; public SqmIndexAggregateFunction(SqmPath pluralDomainPath, String functionName) { //noinspection unchecked @@ -27,6 +35,46 @@ public SqmIndexAggregateFunction(SqmPath pluralDomainPath, String functionNam (SqmPathSource) ( (PluralPersistentAttribute) pluralDomainPath.getReferencedPathSource() ).getIndexPathSource() ); this.functionName = functionName; + switch ( functionName ) { + case "sum": + //noinspection unchecked + this.returnableType = (ReturnableType) nodeBuilder().getSumReturnTypeResolver() + .resolveFunctionReturnType( + null, + (SqmToSqlAstConverter) null, + List.of( pluralDomainPath.get( CollectionPart.Nature.INDEX.getName() ) ), + nodeBuilder().getTypeConfiguration() + ); + break; + case "avg": + //noinspection unchecked + this.returnableType = (ReturnableType) nodeBuilder().getAvgReturnTypeResolver() + .resolveFunctionReturnType( + null, + (SqmToSqlAstConverter) null, + List.of( pluralDomainPath.get( CollectionPart.Nature.INDEX.getName() ) ), + nodeBuilder().getTypeConfiguration() + ); + break; + default: + this.returnableType = null; + break; + } + } + + @Override + public SqmExpressible getExpressible() { + return returnableType == null ? super.getExpressible() : returnableType; + } + + @Override + public JavaType getJavaTypeDescriptor() { + return returnableType == null ? super.getJavaTypeDescriptor() : returnableType.getExpressibleJavaType(); + } + + @Override + public JavaType getNodeJavaType() { + return returnableType == null ? super.getNodeJavaType() : returnableType.getExpressibleJavaType(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/StandardRowReader.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/StandardRowReader.java index d1f2115abb69..eec60541df6b 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/StandardRowReader.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/StandardRowReader.java @@ -6,6 +6,7 @@ */ package org.hibernate.sql.results.internal; +import java.lang.reflect.Array; import java.util.ArrayList; import java.util.List; @@ -41,6 +42,9 @@ public class StandardRowReader implements RowReader { private final RowTransformer rowTransformer; private final Class domainResultJavaType; + private final ComponentType componentType; + private final Class resultElementClass; + private static final Logger LOGGER = LoadingLogger.LOGGER; public StandardRowReader( @@ -76,6 +80,19 @@ public StandardRowReader( this.hasCollectionInitializers = hasCollectionInitializers; this.rowTransformer = rowTransformer; this.domainResultJavaType = domainResultJavaType; + if ( domainResultJavaType == null + || domainResultJavaType == Object[].class + || domainResultJavaType == Object.class + || !domainResultJavaType.isArray() + || resultAssemblers.length == 1 + && domainResultJavaType == resultAssemblers[0].getAssembledJavaType().getJavaTypeClass() ) { + this.resultElementClass = Object.class; + this.componentType = ComponentType.OBJECT; + } + else { + this.resultElementClass = domainResultJavaType.getComponentType(); + this.componentType = ComponentType.determineComponentType( domainResultJavaType ); + } } @Override @@ -84,16 +101,7 @@ public Class getDomainResultResultJavaType() { } @Override - public Class getResultJavaType() { - if ( resultAssemblers.length == 1 ) { - return resultAssemblers[0].getAssembledJavaType().getJavaTypeClass(); - } - - return Object[].class; - } - - @Override - public List> getResultJavaTypes() { + public List<@Nullable JavaType> getResultJavaTypes() { List> javaTypes = new ArrayList<>( resultAssemblers.length ); for ( DomainResultAssembler resultAssembler : resultAssemblers ) { javaTypes.add( resultAssembler.getAssembledJavaType() ); @@ -128,16 +136,111 @@ public boolean hasCollectionInitializers() { public T readRow(RowProcessingState rowProcessingState) { coordinateInitializers( rowProcessingState ); - final Object[] resultRow = new Object[ resultAssemblers.length ]; + // The following is ugly, but unfortunately necessary to not hurt performance. + // This implementation was micro-benchmarked and discussed with Francesco Nigro, + // who hinted that using this style instead of the reflective Array.getLength(), Array.set() + // is easier for the JVM to optimize + switch ( componentType ) { + case BOOLEAN: + final boolean[] resultBooleanRow = new boolean[resultAssemblers.length]; - for ( int i = 0; i < resultAssemblers.length; i++ ) { - final DomainResultAssembler assembler = resultAssemblers[i]; - resultRow[i] = assembler.assemble( rowProcessingState ); - } + for ( int i = 0; i < resultAssemblers.length; i++ ) { + final DomainResultAssembler assembler = resultAssemblers[i]; + resultBooleanRow[i] = (boolean) assembler.assemble( rowProcessingState ); + } + + afterRow( rowProcessingState ); + + return (T) resultBooleanRow; + case BYTE: + final byte[] resultByteRow = new byte[resultAssemblers.length]; + + for ( int i = 0; i < resultAssemblers.length; i++ ) { + final DomainResultAssembler assembler = resultAssemblers[i]; + resultByteRow[i] = (byte) assembler.assemble( rowProcessingState ); + } + + afterRow( rowProcessingState ); + + return (T) resultByteRow; + case CHAR: + final char[] resultCharRow = new char[resultAssemblers.length]; + + for ( int i = 0; i < resultAssemblers.length; i++ ) { + final DomainResultAssembler assembler = resultAssemblers[i]; + resultCharRow[i] = (char) assembler.assemble( rowProcessingState ); + } + + afterRow( rowProcessingState ); + + return (T) resultCharRow; + case SHORT: + final short[] resultShortRow = new short[resultAssemblers.length]; + + for ( int i = 0; i < resultAssemblers.length; i++ ) { + final DomainResultAssembler assembler = resultAssemblers[i]; + resultShortRow[i] = (short) assembler.assemble( rowProcessingState ); + } + + afterRow( rowProcessingState ); + + return (T) resultShortRow; + case INT: + final int[] resultIntRow = new int[resultAssemblers.length]; + + for ( int i = 0; i < resultAssemblers.length; i++ ) { + final DomainResultAssembler assembler = resultAssemblers[i]; + resultIntRow[i] = (int) assembler.assemble( rowProcessingState ); + } + + afterRow( rowProcessingState ); + + return (T) resultIntRow; + case LONG: + final long[] resultLongRow = new long[resultAssemblers.length]; + + for ( int i = 0; i < resultAssemblers.length; i++ ) { + final DomainResultAssembler assembler = resultAssemblers[i]; + resultLongRow[i] = (long) assembler.assemble( rowProcessingState ); + } + + afterRow( rowProcessingState ); + + return (T) resultLongRow; + case FLOAT: + final float[] resultFloatRow = new float[resultAssemblers.length]; - afterRow( rowProcessingState ); + for ( int i = 0; i < resultAssemblers.length; i++ ) { + final DomainResultAssembler assembler = resultAssemblers[i]; + resultFloatRow[i] = (float) assembler.assemble( rowProcessingState ); + } - return rowTransformer.transformRow( resultRow ); + afterRow( rowProcessingState ); + + return (T) resultFloatRow; + case DOUBLE: + final double[] resultDoubleRow = new double[resultAssemblers.length]; + + for ( int i = 0; i < resultAssemblers.length; i++ ) { + final DomainResultAssembler assembler = resultAssemblers[i]; + resultDoubleRow[i] = (double) assembler.assemble( rowProcessingState ); + } + + afterRow( rowProcessingState ); + + return (T) resultDoubleRow; + default: + final Object[] resultRow = (Object[]) Array.newInstance( resultElementClass, resultAssemblers.length ); + + for ( int i = 0; i < resultAssemblers.length; i++ ) { + final DomainResultAssembler assembler = resultAssemblers[i]; + resultRow[i] = assembler.assemble( rowProcessingState ); + } + + afterRow( rowProcessingState ); + + return rowTransformer.transformRow( resultRow ); + } } private void afterRow(RowProcessingState rowProcessingState) { @@ -188,4 +291,54 @@ public void finishUp(RowProcessingState rowProcessingState) { } } + enum ComponentType { + BOOLEAN(boolean.class), + BYTE(byte.class), + SHORT(short.class), + CHAR(char.class), + INT(int.class), + LONG(long.class), + FLOAT(float.class), + DOUBLE(double.class), + OBJECT(Object.class); + + private final Class componentType; + + ComponentType(Class componentType) { + this.componentType = componentType; + } + + public static ComponentType determineComponentType(Class resultType) { + if ( resultType == boolean[].class) { + return BOOLEAN; + } + else if ( resultType == byte[].class) { + return BYTE; + } + else if ( resultType == short[].class) { + return SHORT; + } + else if ( resultType == char[].class) { + return CHAR; + } + else if ( resultType == int[].class) { + return INT; + } + else if ( resultType == long[].class) { + return LONG; + } + else if ( resultType == float[].class) { + return FLOAT; + } + else if ( resultType == double[].class) { + return DOUBLE; + } + return OBJECT; + } + + public Class getComponentType() { + return componentType; + } + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/spi/ListResultsConsumer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/spi/ListResultsConsumer.java index 17b12c108af0..a3b5ebfd7f8f 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/spi/ListResultsConsumer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/spi/ListResultsConsumer.java @@ -25,6 +25,8 @@ import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry; import org.hibernate.type.spi.TypeConfiguration; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * ResultsConsumer for creating a List of results * @@ -247,22 +249,37 @@ else if ( uniqueSemantic == UniqueSemantic.ASSERT ) { private JavaType resolveDomainResultJavaType( Class domainResultResultJavaType, - List> resultJavaTypes, + List<@Nullable JavaType> resultJavaTypes, TypeConfiguration typeConfiguration) { final JavaTypeRegistry javaTypeRegistry = typeConfiguration.getJavaTypeRegistry(); if ( domainResultResultJavaType != null ) { - return javaTypeRegistry.resolveDescriptor( domainResultResultJavaType ); + final JavaType resultJavaType = javaTypeRegistry.resolveDescriptor( domainResultResultJavaType ); + // Could be that the user requested a more general type than the actual type, + // so resolve the most concrete type since this type is used to determine equality of objects + if ( resultJavaTypes.size() == 1 && isMoreConcrete( resultJavaType, resultJavaTypes.get( 0 ) ) ) { + //noinspection unchecked + return (JavaType) resultJavaTypes.get( 0 ); + } + return resultJavaType; } if ( resultJavaTypes.size() == 1 ) { + final JavaType firstJavaType = resultJavaTypes.get( 0 ); + if ( firstJavaType == null ) { + return javaTypeRegistry.resolveDescriptor( Object.class ); + } //noinspection unchecked - return (JavaType) resultJavaTypes.get( 0 ); + return (JavaType) firstJavaType; } return javaTypeRegistry.resolveDescriptor( Object[].class ); } + private static boolean isMoreConcrete(JavaType resultJavaType, @Nullable JavaType javaType) { + return javaType != null && resultJavaType.getJavaTypeClass().isAssignableFrom( javaType.getJavaTypeClass() ); + } + @Override public boolean canResultsBeCached() { return true; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/spi/RowReader.java b/hibernate-core/src/main/java/org/hibernate/sql/results/spi/RowReader.java index 2b44ca1ee322..6bed911a4ace 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/spi/RowReader.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/spi/RowReader.java @@ -30,19 +30,10 @@ public interface RowReader { */ Class getDomainResultResultJavaType(); - /** - * The row result Java type, before any transformations. - * - * @apiNote along with {@link #getResultJavaTypes()}, describes the "raw" - * values as determined from the {@link org.hibernate.sql.results.graph.DomainResult} - * references associated with the JdbcValues being processed - */ - Class getResultJavaType(); - /** * The individual JavaType for each DomainResult */ - List> getResultJavaTypes(); + List<@Nullable JavaType> getResultJavaTypes(); int getInitializerCount(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayAggregateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayAggregateTest.java index a403fffad660..136338a4d44f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayAggregateTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayAggregateTest.java @@ -141,7 +141,7 @@ public void testWithNull(SessionFactoryScope scope) { @Test public void testCompareAgainstArray(SessionFactoryScope scope) { scope.inSession( em -> { - List results = em.createQuery( "select 1 where array('abc','def',null) is not distinct from (select array_agg(e.theString) within group (order by e.theString asc nulls last) from EntityOfBasics e)", String[].class ) + List results = em.createQuery( "select 1 where array('abc','def',null) is not distinct from (select array_agg(e.theString) within group (order by e.theString asc nulls last) from EntityOfBasics e)", Integer.class ) .getResultList(); assertEquals( 1, results.size() ); } ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/embeddable/EmbeddableInheritanceMappedSuperclassAdnGenericsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/embeddable/EmbeddableInheritanceMappedSuperclassAndGenericsTest.java similarity index 91% rename from hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/embeddable/EmbeddableInheritanceMappedSuperclassAdnGenericsTest.java rename to hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/embeddable/EmbeddableInheritanceMappedSuperclassAndGenericsTest.java index e4af377c360e..49700080f6ee 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/embeddable/EmbeddableInheritanceMappedSuperclassAdnGenericsTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/embeddable/EmbeddableInheritanceMappedSuperclassAndGenericsTest.java @@ -28,15 +28,15 @@ * @author Marco Belladelli */ @DomainModel( annotatedClasses = { - EmbeddableInheritanceMappedSuperclassAdnGenericsTest.AbstractSuperclass.class, - EmbeddableInheritanceMappedSuperclassAdnGenericsTest.Range.class, - EmbeddableInheritanceMappedSuperclassAdnGenericsTest.IntegerRange.class, - EmbeddableInheritanceMappedSuperclassAdnGenericsTest.ToleranceRange.class, - EmbeddableInheritanceMappedSuperclassAdnGenericsTest.TestEntity.class, + EmbeddableInheritanceMappedSuperclassAndGenericsTest.AbstractSuperclass.class, + EmbeddableInheritanceMappedSuperclassAndGenericsTest.Range.class, + EmbeddableInheritanceMappedSuperclassAndGenericsTest.IntegerRange.class, + EmbeddableInheritanceMappedSuperclassAndGenericsTest.ToleranceRange.class, + EmbeddableInheritanceMappedSuperclassAndGenericsTest.TestEntity.class, } ) @SessionFactory @Jira( "https://hibernate.atlassian.net/browse/HHH-18172" ) -public class EmbeddableInheritanceMappedSuperclassAdnGenericsTest { +public class EmbeddableInheritanceMappedSuperclassAndGenericsTest { @Test public void testFind(SessionFactoryScope scope) { scope.inTransaction( session -> { @@ -62,9 +62,9 @@ public void testQueryEntity(SessionFactoryScope scope) { @Test public void testQueryEmbeddable(SessionFactoryScope scope) { scope.inTransaction( session -> { - final IntegerRange result = session.createQuery( + final Range result = session.createQuery( "select range from TestEntity where id = 1", - IntegerRange.class + Range.class ).getSingleResult(); assertThat( result.getName() ).isEqualTo( "tolerance_range" ); assertThat( result ).isExactlyInstanceOf( ToleranceRange.class ); @@ -74,9 +74,9 @@ public void testQueryEmbeddable(SessionFactoryScope scope) { @Test public void testQueryJoinedEmbeddable(SessionFactoryScope scope) { scope.inTransaction( session -> { - final IntegerRange result = session.createQuery( + final Range result = session.createQuery( "select r from TestEntity t join t.range r where id = 2", - IntegerRange.class + Range.class ).getSingleResult(); assertThat( result.getName() ).isEqualTo( "integer_range" ); assertThat( result ).isExactlyInstanceOf( IntegerRange.class ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/MultiSelectResultTypeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/MultiSelectResultTypeTest.java index d204ef745dab..110bab83af0f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/MultiSelectResultTypeTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/MultiSelectResultTypeTest.java @@ -2,6 +2,8 @@ import java.util.List; +import org.hibernate.query.QueryTypeMismatchException; + import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.Jpa; @@ -15,6 +17,7 @@ import jakarta.persistence.criteria.Root; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.fail; @Jpa( annotatedClasses = { @@ -45,6 +48,7 @@ public void testResultOfMultiSelect(EntityManagerFactoryScope scope) { List idPairs = entityManager.createQuery( q ).getResultList(); assertThat( idPairs.size() ).isEqualTo( 1 ); Integer[] ids = idPairs.get( 0 ); + assertThat( ids[0] ).isEqualTo( 1 ); } ); } @@ -60,6 +64,7 @@ public void testResultOfMultiSelectPrimitive(EntityManagerFactoryScope scope) { List idPairs = entityManager.createQuery( q ).getResultList(); assertThat( idPairs.size() ).isEqualTo( 1 ); int[] ids = idPairs.get( 0 ); + assertThat( ids[0] ).isEqualTo( 1 ); } ); } @@ -75,8 +80,8 @@ public void testResultOfMultiSelect2(EntityManagerFactoryScope scope) { List values = entityManager.createQuery( q ).getResultList(); assertThat( values.size() ).isEqualTo( 1 ); Object[] value = values.get( 0 ); - Integer id = (Integer) value[0]; - String name = (String) value[1]; + assertThat( value[0] ).isEqualTo( 1 ); + assertThat( value[1] ).isEqualTo( "a" ); } ); } @@ -91,7 +96,45 @@ public void testResultOfSelect(EntityManagerFactoryScope scope) { q.select( r.get( "id" ) ); List idPairs = entityManager.createQuery( q ).getResultList(); assertThat( idPairs.size() ).isEqualTo( 1 ); - Integer id = idPairs.get( 0 ); + assertThat( idPairs.get( 0 ) ).isEqualTo( 1 ); + } + ); + } + + @Test + public void testValidateSelectItemAgainstArrayComponentType(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery q = cb.createQuery( String[].class ); + Root r = q.from( TestEntity.class ); + q.select( r.get( "id" ) ); + try { + entityManager.createQuery( q ); + fail( "Should fail with a type validation error" ); + } + catch (QueryTypeMismatchException ex) { + assertThat( ex.getMessage() ).contains( String[].class.getName(), Integer.class.getName() ); + } + } + ); + } + + @Test + public void testValidateSelectItemAgainstArrayComponentType2(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery q = cb.createQuery( String[].class ); + Root r = q.from( TestEntity.class ); + q.multiselect( r.get( "name" ), r.get( "id" ) ); + try { + entityManager.createQuery( q ); + fail( "Should fail with a type validation error" ); + } + catch (QueryTypeMismatchException ex) { + assertThat( ex.getMessage() ).contains( String.class.getName(), Integer.class.getName() ); + } } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/TemporalParameterPlusDurationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/TemporalParameterPlusDurationTest.java index 1068e9cd3cba..90d414624f96 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/TemporalParameterPlusDurationTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/TemporalParameterPlusDurationTest.java @@ -21,54 +21,54 @@ public class TemporalParameterPlusDurationTest { @Test void timestampVsTimestampParameterPlusDuration(SessionFactoryScope scope) { scope.inSession( session -> { - session.createQuery( "select count(*) from SimpleEntity where inst > :i + 1 second + 2 second", SimpleEntity.class ) + session.createQuery( "from SimpleEntity where inst > :i + 1 second + 2 second", SimpleEntity.class ) .setParameter( "i", Instant.now() ) - .getResultCount(); + .getResultList(); } ); } @Test void timestampParameterPlusDurationVsTimestamp(SessionFactoryScope scope) { scope.inSession( session -> { - session.createQuery( "select count(*) from SimpleEntity where :i + 1 second + 2 second > inst", SimpleEntity.class ) + session.createQuery( "from SimpleEntity where :i + 1 second + 2 second > inst", SimpleEntity.class ) .setParameter( "i", Instant.now() ) - .getResultCount(); + .getResultList(); } ); } @Test void dateVsDateParameterPlusDuration(SessionFactoryScope scope) { scope.inSession( session -> { - session.createQuery( "select count(*) from SimpleEntity where ldate > :i + 3 day + 2 day", SimpleEntity.class ) + session.createQuery( "from SimpleEntity where ldate > :i + 3 day + 2 day", SimpleEntity.class ) .setParameter( "i", LocalDate.now() ) - .getResultCount(); + .getResultList(); } ); } @Test void dateParameterPlusDurationVsDate(SessionFactoryScope scope) { scope.inSession( session -> { - session.createQuery( "select count(*) from SimpleEntity where :i + 3 day + 2 day > ldate", SimpleEntity.class ) + session.createQuery( "from SimpleEntity where :i + 3 day + 2 day > ldate", SimpleEntity.class ) .setParameter( "i", LocalDate.now() ) - .getResultCount(); + .getResultList(); } ); } @Test void durationVsDurationParameterPlusDuration(SessionFactoryScope scope) { scope.inSession( session -> { - session.createQuery( "select count(*) from SimpleEntity where dur > :i + 1 second", Number.class ) + session.createQuery( "from SimpleEntity where dur > :i + 1 second", SimpleEntity.class ) .setParameter( "i", Duration.ofMinutes( 1 ) ) - .getResultCount(); + .getResultList(); } ); } @Test void durationParameterVsDurationPlusDuration(SessionFactoryScope scope) { scope.inSession( session -> { - session.createQuery( "select count(*) from SimpleEntity where :i + 1 second > dur", Number.class ) + session.createQuery( "from SimpleEntity where :i + 1 second > dur", SimpleEntity.class ) .setParameter( "i", Duration.ofMinutes( 1 ) ) - .getResultCount(); + .getResultList(); } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java index 3a37a28b4ccd..fb8ee7f2bfc6 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java @@ -2105,7 +2105,7 @@ public void testExtractOffsetHourMinute(SessionFactoryScope scope) { @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsFormat.class, comment = "We extract the offset with a format function") public void testExtractFunctionTimeZoneOffset(SessionFactoryScope scope) { scope.inTransaction( - session -> session.createQuery( "select extract(offset from e.theZonedDateTime) from EntityOfBasics e", Integer.class) + session -> session.createQuery( "select extract(offset from e.theZonedDateTime) from EntityOfBasics e", ZoneOffset.class) .list() ); }