HHH-17956 Criteria multiselect ignores type of the criteria query and always returns list of Object[]

This commit is contained in:
Andrea Boriero 2024-04-22 10:58:17 +02:00 committed by Christian Beikov
parent f5062b2aef
commit a567226a72
18 changed files with 496 additions and 138 deletions

View File

@ -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.

View File

@ -41,7 +41,7 @@ import static org.hibernate.type.SqlTypes.*;
*
* @author Christian Beikov
*/
class SumReturnTypeResolver implements FunctionReturnTypeResolver {
public class SumReturnTypeResolver implements FunctionReturnTypeResolver {
private final BasicType<Long> longType;
private final BasicType<Double> doubleType;

View File

@ -115,6 +115,8 @@ import jakarta.persistence.criteria.CriteriaDelete;
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 @@ public abstract class AbstractSharedSessionContract implements SharedSessionCont
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 @@ public abstract class AbstractSharedSessionContract implements SharedSessionCont
}
}
private <T> boolean hasJavaTypeDescriptor(Class<T> resultClass) {
final JavaType<Object> descriptor = getTypeConfiguration().getJavaTypeRegistry().findDescriptor( resultClass );
return descriptor != null && descriptor.getClass() != UnknownBasicJavaType.class;
}
@Override
public <T> NativeQueryImplementor<T> createNativeQuery(String sqlString, Class<T> resultClass, String tableAlias) {
@SuppressWarnings("unchecked")

View File

@ -30,6 +30,7 @@ import jakarta.persistence.TemporalType;
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 @@ public abstract class AbstractSelectionQuery<R>
SqmQueryPart<R> queryPart,
Class<R> expectedResultType,
SessionFactoryImplementor factory) {
assert getQueryString().equals( CRITERIA_HQL_STRING );
if ( queryPart instanceof SqmQuerySpec<?> ) {
final SqmQuerySpec<R> sqmQuerySpec = (SqmQuerySpec<R>) queryPart;
final List<SqmSelection<?>> sqmSelections = sqmQuerySpec.getSelectClause().getSelections();
if ( sqmSelections == null || sqmSelections.isEmpty() ) {
// make sure there is at least one root
final List<SqmRoot<?>> 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<SqmRoot<?>> 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 @@ public abstract class AbstractSelectionQuery<R>
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 <T> void verifySelectionType(
Class<T> 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 <T> void verifySelectionType(
Class<T> 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 @@ public abstract class AbstractSelectionQuery<R>
private static <T> boolean isResultTypeAlwaysAllowed(Class<T> expectedResultClass) {
return expectedResultClass == null
|| expectedResultClass == Object.class
|| expectedResultClass == Object[].class
|| expectedResultClass == List.class
|| expectedResultClass == Tuple.class
|| expectedResultClass.isArray();
|| expectedResultClass == Map.class
|| expectedResultClass == Tuple.class;
}
protected static <T> void verifyResultType(Class<T> 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 ) {
protected static <T> void verifyResultType(Class<T> 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
}
}

View File

@ -38,4 +38,8 @@ public interface DomainQueryExecutionContext {
* The underlying session
*/
SharedSessionContractImplementor getSession();
default Class<?> getResultType() {
return null;
}
}

View File

@ -137,11 +137,13 @@ public class ConcreteSqmSelectQueryPlan<R> implements SelectQueryPlan<R> {
jdbcParameterBindings
);
session.autoFlushIfRequired( jdbcSelect.getAffectedTableNames(), true );
//noinspection unchecked
return session.getFactory().getJdbcServices().getJdbcSelectExecutor().list(
jdbcSelect,
jdbcParameterBindings,
listInterpreterExecutionContext( hql, executionContext, jdbcSelect, subSelectFetchKeyHandler ),
rowTransformer,
(Class<R>) executionContext.getResultType(),
uniqueSemantic
);
}

View File

@ -251,34 +251,7 @@ public class QuerySqmImpl<R>
bindCriteriaParameter((SqmJpaCriteriaParameterWrapper<?>) sqmParameter);
}
}
if ( sqm instanceof SqmSelectStatement<?> ) {
SqmUtil.verifyIsSelectStatement( sqm, null );
final SqmQueryPart<R> queryPart = ( (SqmSelectStatement<R>) 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<R> updateStatement = (SqmUpdateStatement<R>) 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<R>) sqm );
}
}
validateStatement( sqm, expectedResultType );
resultType = expectedResultType;
tupleMetadata = buildTupleMetadata( criteria, expectedResultType );
@ -298,7 +271,12 @@ public class QuerySqmImpl<R>
private void validateStatement(SqmStatement<R> sqmStatement, Class<R> resultType) {
if ( sqmStatement instanceof SqmSelectStatement<?> ) {
SqmUtil.verifyIsSelectStatement( sqmStatement, hql );
final SqmQueryPart<R> queryPart = ( (SqmSelectStatement<R>) 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 @@ public class QuerySqmImpl<R>
if ( sqmStatement instanceof SqmUpdateStatement<?> ) {
final SqmUpdateStatement<R> updateStatement = (SqmUpdateStatement<R>) 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<?> ) {

View File

@ -34,6 +34,8 @@ import java.util.Set;
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.UnaryArithmeticOperator;
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<Integer> integerType;
private transient BasicType<Long> longType;
private transient BasicType<Character> characterType;
private transient FunctionReturnTypeResolver sumReturnTypeResolver;
private transient FunctionReturnTypeResolver avgReturnTypeResolver;
private final transient Map<Class<? extends HibernateCriteriaBuilder>, HibernateCriteriaBuilder> extensions;
public SqmCriteriaNodeBuilder(
@ -248,7 +253,6 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext,
}
@Override
public BasicType<Boolean> getBooleanType() {
final BasicType<Boolean> booleanType = this.booleanType;
@ -293,6 +297,22 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext,
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;

View File

@ -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<T> extends AbstractSqmSpecificPluralPartPath<T> {
private final String functionName;
private final ReturnableType<T> returnableType;
public SqmElementAggregateFunction(SqmPath<?> pluralDomainPath, String functionName) {
//noinspection unchecked
@ -27,6 +33,46 @@ public class SqmElementAggregateFunction<T> extends AbstractSqmSpecificPluralPar
( (PluralPersistentAttribute<?, ?, T>) pluralDomainPath.getReferencedPathSource() ).getElementPathSource()
);
this.functionName = functionName;
switch ( functionName ) {
case "sum":
//noinspection unchecked
this.returnableType = (ReturnableType<T>) nodeBuilder().getSumReturnTypeResolver()
.resolveFunctionReturnType(
null,
(SqmToSqlAstConverter) null,
List.of( pluralDomainPath ),
nodeBuilder().getTypeConfiguration()
);
break;
case "avg":
//noinspection unchecked
this.returnableType = (ReturnableType<T>) nodeBuilder().getAvgReturnTypeResolver()
.resolveFunctionReturnType(
null,
(SqmToSqlAstConverter) null,
List.of( pluralDomainPath ),
nodeBuilder().getTypeConfiguration()
);
break;
default:
this.returnableType = null;
break;
}
}
@Override
public SqmExpressible<T> getExpressible() {
return returnableType == null ? super.getExpressible() : returnableType;
}
@Override
public JavaType<T> getJavaTypeDescriptor() {
return returnableType == null ? super.getJavaTypeDescriptor() : returnableType.getExpressibleJavaType();
}
@Override
public JavaType<T> getNodeJavaType() {
return returnableType == null ? super.getNodeJavaType() : returnableType.getExpressibleJavaType();
}
@Override

View File

@ -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<T> extends AbstractSqmSpecificPluralPartPath<T> {
private final String functionName;
private final ReturnableType<T> returnableType;
public SqmIndexAggregateFunction(SqmPath<?> pluralDomainPath, String functionName) {
//noinspection unchecked
@ -27,6 +35,46 @@ public class SqmIndexAggregateFunction<T> extends AbstractSqmSpecificPluralPartP
(SqmPathSource<T>) ( (PluralPersistentAttribute<?, ?, ?>) pluralDomainPath.getReferencedPathSource() ).getIndexPathSource()
);
this.functionName = functionName;
switch ( functionName ) {
case "sum":
//noinspection unchecked
this.returnableType = (ReturnableType<T>) nodeBuilder().getSumReturnTypeResolver()
.resolveFunctionReturnType(
null,
(SqmToSqlAstConverter) null,
List.of( pluralDomainPath.get( CollectionPart.Nature.INDEX.getName() ) ),
nodeBuilder().getTypeConfiguration()
);
break;
case "avg":
//noinspection unchecked
this.returnableType = (ReturnableType<T>) 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<T> getExpressible() {
return returnableType == null ? super.getExpressible() : returnableType;
}
@Override
public JavaType<T> getJavaTypeDescriptor() {
return returnableType == null ? super.getJavaTypeDescriptor() : returnableType.getExpressibleJavaType();
}
@Override
public JavaType<T> getNodeJavaType() {
return returnableType == null ? super.getNodeJavaType() : returnableType.getExpressibleJavaType();
}
@Override

View File

@ -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<T> implements RowReader<T> {
private final RowTransformer<T> rowTransformer;
private final Class<T> domainResultJavaType;
private final ComponentType componentType;
private final Class<?> resultElementClass;
private static final Logger LOGGER = LoadingLogger.LOGGER;
public StandardRowReader(
@ -76,6 +80,19 @@ public class StandardRowReader<T> implements RowReader<T> {
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 StandardRowReader<T> implements RowReader<T> {
}
@Override
public Class<?> getResultJavaType() {
if ( resultAssemblers.length == 1 ) {
return resultAssemblers[0].getAssembledJavaType().getJavaTypeClass();
}
return Object[].class;
}
@Override
public List<JavaType<?>> getResultJavaTypes() {
public List<@Nullable JavaType<?>> getResultJavaTypes() {
List<JavaType<?>> javaTypes = new ArrayList<>( resultAssemblers.length );
for ( DomainResultAssembler resultAssembler : resultAssemblers ) {
javaTypes.add( resultAssembler.getAssembledJavaType() );
@ -128,16 +136,111 @@ public class StandardRowReader<T> implements RowReader<T> {
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];
for ( int i = 0; i < resultAssemblers.length; i++ ) {
final DomainResultAssembler assembler = resultAssemblers[i];
resultFloatRow[i] = (float) assembler.assemble( rowProcessingState );
}
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 );
}
afterRow( rowProcessingState );
return rowTransformer.transformRow( resultRow );
}
private void afterRow(RowProcessingState rowProcessingState) {
@ -188,4 +291,54 @@ public class StandardRowReader<T> implements RowReader<T> {
}
}
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;
}
}
}

View File

@ -25,6 +25,8 @@ import org.hibernate.type.descriptor.java.spi.EntityJavaType;
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 @@ public class ListResultsConsumer<R> implements ResultsConsumer<List<R>, R> {
private JavaType<R> resolveDomainResultJavaType(
Class<R> domainResultResultJavaType,
List<JavaType<?>> resultJavaTypes,
List<@Nullable JavaType<?>> resultJavaTypes,
TypeConfiguration typeConfiguration) {
final JavaTypeRegistry javaTypeRegistry = typeConfiguration.getJavaTypeRegistry();
if ( domainResultResultJavaType != null ) {
return javaTypeRegistry.resolveDescriptor( domainResultResultJavaType );
final JavaType<R> 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<R>) 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<R>) resultJavaTypes.get( 0 );
return (JavaType<R>) 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;

View File

@ -30,19 +30,10 @@ public interface RowReader<R> {
*/
Class<R> 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<JavaType<?>> getResultJavaTypes();
List<@Nullable JavaType<?>> getResultJavaTypes();
int getInitializerCount();

View File

@ -141,7 +141,7 @@ public class ArrayAggregateTest {
@Test
public void testCompareAgainstArray(SessionFactoryScope scope) {
scope.inSession( em -> {
List<String[]> 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<Integer> 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() );
} );

View File

@ -28,15 +28,15 @@ import static org.assertj.core.api.Assertions.assertThat;
* @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 class EmbeddableInheritanceMappedSuperclassAdnGenericsTest {
@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 class EmbeddableInheritanceMappedSuperclassAdnGenericsTest {
@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 );

View File

@ -2,6 +2,8 @@ package org.hibernate.orm.test.jpa.criteria;
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.CriteriaQuery;
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 class MultiSelectResultTypeTest {
List<Integer[]> 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 class MultiSelectResultTypeTest {
List<int[]> 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 class MultiSelectResultTypeTest {
List<Object[]> 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 class MultiSelectResultTypeTest {
q.select( r.get( "id" ) );
List<Integer> 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<String[]> q = cb.createQuery( String[].class );
Root<TestEntity> 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<String[]> q = cb.createQuery( String[].class );
Root<TestEntity> 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() );
}
}
);
}

View File

@ -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();
} );
}

View File

@ -2105,7 +2105,7 @@ public class FunctionTests {
@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()
);
}