From 850a2a0753554126ecf92e3f9d5a72c0f64980b2 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Fri, 2 Aug 2024 12:48:45 +0200 Subject: [PATCH] HHH-18271 Avoid query validations of cached queries by doing validation eagerly. Cache allowed result types per query interpretation --- .../hql/internal/SemanticQueryBuilder.java | 3 + .../QueryInterpretationCacheDisabledImpl.java | 9 +- .../QueryInterpretationCacheStandardImpl.java | 2 +- .../query/spi/AbstractSelectionQuery.java | 396 ------------------ .../query/spi/HqlInterpretation.java | 3 + .../spi/SimpleHqlInterpretationImpl.java | 17 + .../query/sqm/InterpretationException.java | 4 +- .../internal/AbstractSqmSelectionQuery.java | 175 ++++++++ .../query/sqm/internal/QuerySqmImpl.java | 192 ++------- .../sqm/internal/SqmSelectionQueryImpl.java | 112 +++-- .../hibernate/query/sqm/internal/SqmUtil.java | 260 +++++++++++- .../sqm/tree/AbstractSqmDmlStatement.java | 3 + .../sqm/tree/delete/SqmDeleteStatement.java | 7 +- .../insert/AbstractSqmInsertStatement.java | 43 ++ .../tree/insert/SqmInsertSelectStatement.java | 13 + .../tree/insert/SqmInsertValuesStatement.java | 8 + .../sqm/tree/select/SqmSelectStatement.java | 4 + .../sqm/tree/update/SqmUpdateStatement.java | 53 ++- ...yUpdateQueryHandlingModeExceptionTest.java | 8 +- ...ityUpdateQueryHandlingModeWarningTest.java | 4 +- 20 files changed, 684 insertions(+), 632 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index 59e49e0edb..f6ba427a3f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -555,6 +555,7 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem queryExpressionContext.accept( this ); insertStatement.onConflict( visitConflictClause( ctx.conflictClause() ) ); + insertStatement.validate( query ); return insertStatement; } finally { @@ -613,6 +614,7 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem insertStatement.values( valuesList ); insertStatement.onConflict( visitConflictClause( ctx.conflictClause() ) ); + insertStatement.validate( query ); return insertStatement; } finally { @@ -684,6 +686,7 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem updateStatement.applyPredicate( visitWhereClause( whereClauseContext ) ); } + updateStatement.validate( query ); return updateStatement; } finally { diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheDisabledImpl.java b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheDisabledImpl.java index 8d85252d13..f2ac2c04af 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheDisabledImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheDisabledImpl.java @@ -19,6 +19,7 @@ import org.hibernate.query.spi.SelectQueryPlan; import org.hibernate.query.sql.spi.ParameterInterpretation; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.tree.SqmStatement; +import org.hibernate.query.sqm.tree.select.SqmSelectStatement; import org.hibernate.stat.spi.StatisticsImplementor; /** @@ -72,7 +73,7 @@ public class QueryInterpretationCacheDisabledImpl implements QueryInterpretation final DomainParameterXref domainParameterXref; final ParameterMetadataImplementor parameterMetadata; if ( sqmStatement.getSqmParameters().isEmpty() ) { - domainParameterXref = DomainParameterXref.empty(); + domainParameterXref = DomainParameterXref.EMPTY; parameterMetadata = ParameterMetadataImpl.EMPTY; } else { @@ -101,6 +102,12 @@ public class QueryInterpretationCacheDisabledImpl implements QueryInterpretation public DomainParameterXref getDomainParameterXref() { return domainParameterXref; } + + @Override + public void validateResultType(Class resultType) { + assert sqmStatement instanceof SqmSelectStatement; + ( (SqmSelectStatement) sqmStatement ).validateResultType( resultType ); + } }; } diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheStandardImpl.java b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheStandardImpl.java index 7d1905b9a4..e954097ff7 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheStandardImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheStandardImpl.java @@ -147,7 +147,7 @@ public class QueryInterpretationCacheStandardImpl implements QueryInterpretation final DomainParameterXref domainParameterXref; if ( sqmStatement.getSqmParameters().isEmpty() ) { - domainParameterXref = DomainParameterXref.empty(); + domainParameterXref = DomainParameterXref.EMPTY; parameterMetadata = ParameterMetadataImpl.EMPTY; } else { 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 6aabdc020b..df42432cff 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 @@ -6,7 +6,6 @@ */ package org.hibernate.query.spi; -import java.sql.Types; import java.time.Instant; import java.util.Calendar; import java.util.Collection; @@ -33,40 +32,14 @@ import org.hibernate.graph.GraphSemantic; import org.hibernate.graph.spi.AppliedGraph; import org.hibernate.graph.spi.RootGraphImplementor; import org.hibernate.jpa.internal.util.LockModeTypeHelper; -import org.hibernate.metamodel.model.domain.BasicDomainType; -import org.hibernate.metamodel.model.domain.DomainType; -import org.hibernate.metamodel.model.domain.EntityDomainType; -import org.hibernate.metamodel.model.domain.IdentifiableDomainType; -import org.hibernate.metamodel.model.domain.SimpleDomainType; -import org.hibernate.metamodel.model.domain.internal.EntitySqmPathSource; import org.hibernate.query.BindableType; import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.QueryParameter; -import org.hibernate.query.QueryTypeMismatchException; import org.hibernate.query.SelectionQuery; -import org.hibernate.query.criteria.JpaSelection; import org.hibernate.query.internal.ScrollableResultsIterator; import org.hibernate.query.named.NamedQueryMemento; -import org.hibernate.query.sqm.SqmExpressible; -import org.hibernate.query.sqm.SqmPathSource; -import org.hibernate.query.sqm.spi.NamedSqmQueryMemento; -import org.hibernate.query.sqm.tree.SqmStatement; -import org.hibernate.query.sqm.tree.expression.SqmParameter; -import org.hibernate.query.sqm.tree.from.SqmRoot; -import org.hibernate.query.sqm.tree.select.SqmQueryGroup; -import org.hibernate.query.sqm.tree.select.SqmQueryPart; -import org.hibernate.query.sqm.tree.select.SqmQuerySpec; -import org.hibernate.query.sqm.tree.select.SqmSelectStatement; -import org.hibernate.query.sqm.tree.select.SqmSelectableNode; -import org.hibernate.query.sqm.tree.select.SqmSelection; import org.hibernate.sql.exec.internal.CallbackImpl; import org.hibernate.sql.exec.spi.Callback; -import org.hibernate.sql.results.internal.TupleMetadata; -import org.hibernate.type.BasicType; -import org.hibernate.type.BasicTypeRegistry; -import org.hibernate.type.descriptor.java.JavaType; -import org.hibernate.type.descriptor.java.spi.PrimitiveJavaType; -import org.hibernate.type.descriptor.jdbc.JdbcType; import jakarta.persistence.CacheRetrieveMode; import jakarta.persistence.CacheStoreMode; @@ -76,10 +49,6 @@ import jakarta.persistence.LockModeType; import jakarta.persistence.NoResultException; import jakarta.persistence.Parameter; 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 static java.util.Spliterators.spliteratorUnknownSize; import static org.hibernate.CacheMode.fromJpaModes; @@ -94,8 +63,6 @@ import static org.hibernate.jpa.HibernateHints.HINT_CACHE_REGION; import static org.hibernate.jpa.HibernateHints.HINT_FETCH_SIZE; import static org.hibernate.jpa.HibernateHints.HINT_FOLLOW_ON_LOCKING; import static org.hibernate.jpa.HibernateHints.HINT_READ_ONLY; -import static org.hibernate.query.sqm.internal.SqmUtil.isHqlTuple; -import static org.hibernate.query.sqm.internal.SqmUtil.isSelectionAssignableToResultType; /** * @author Steve Ebersole @@ -114,110 +81,6 @@ public abstract class AbstractSelectionQuery super( session ); } - protected TupleMetadata buildTupleMetadata(SqmStatement statement, Class resultType) { - if ( statement instanceof SqmSelectStatement ) { - final SqmSelectStatement select = (SqmSelectStatement) statement; - final List> selections = - select.getQueryPart().getFirstQuerySpec().getSelectClause() - .getSelections(); - return isTupleMetadataRequired( resultType, selections.get(0) ) - ? getTupleMetadata( selections ) - : null; - } - else { - return null; - } - } - - private static boolean isTupleMetadataRequired(Class resultType, SqmSelection selection) { - return isHqlTuple( selection ) - || !isInstantiableWithoutMetadata( resultType ) - && !isSelectionAssignableToResultType( selection, resultType ); - } - - private TupleMetadata getTupleMetadata(List> selections) { - if ( getQueryOptions().getTupleTransformer() == null ) { - return new TupleMetadata( buildTupleElementArray( selections ), buildTupleAliasArray( selections ) ); - } - else { - throw new IllegalArgumentException( - "Illegal combination of Tuple resultType and (non-JpaTupleBuilder) TupleTransformer: " - + getQueryOptions().getTupleTransformer() - ); - } - } - - private static TupleElement[] buildTupleElementArray(List> selections) { - if ( selections.size() == 1 ) { - final SqmSelectableNode selectableNode = selections.get(0).getSelectableNode(); - if ( selectableNode instanceof CompoundSelection ) { - final List> selectionItems = selectableNode.getSelectionItems(); - final TupleElement[] elements = new TupleElement[ selectionItems.size() ]; - for ( int i = 0; i < selectionItems.size(); i++ ) { - elements[i] = selectionItems.get( i ); - } - return elements; - } - else { - return new TupleElement[] { selectableNode }; - } - } - else { - final TupleElement[] elements = new TupleElement[ selections.size() ]; - for ( int i = 0; i < selections.size(); i++ ) { - elements[i] = selections.get( i ).getSelectableNode(); - } - return elements; - } - } - - private static String[] buildTupleAliasArray(List> selections) { - if ( selections.size() == 1 ) { - final SqmSelectableNode selectableNode = selections.get(0).getSelectableNode(); - if ( selectableNode instanceof CompoundSelection ) { - final List> selectionItems = selectableNode.getSelectionItems(); - final String[] elements = new String[ selectionItems.size() ]; - for ( int i = 0; i < selectionItems.size(); i++ ) { - elements[i] = selectionItems.get( i ).getAlias(); - } - return elements; - } - else { - return new String[] { selectableNode.getAlias() }; - } - } - else { - final String[] elements = new String[ selections.size() ]; - for ( int i = 0; i < selections.size(); i++ ) { - elements[i] = selections.get( i ).getAlias(); - } - return elements; - } - } - - protected void applyOptions(NamedSqmQueryMemento memento) { - applyOptions( (NamedQueryMemento) memento ); - - if ( memento.getFirstResult() != null ) { - setFirstResult( memento.getFirstResult() ); - } - - if ( memento.getMaxResults() != null ) { - setMaxResults( memento.getMaxResults() ); - } - - if ( memento.getParameterTypes() != null ) { - final BasicTypeRegistry basicTypeRegistry = - getSessionFactory().getTypeConfiguration().getBasicTypeRegistry(); - for ( Map.Entry entry : memento.getParameterTypes().entrySet() ) { - final BasicType type = - basicTypeRegistry.getRegisteredType( entry.getValue() ); - getParameterMetadata() - .getQueryParameter( entry.getKey() ).applyAnticipatedType( type ); - } - } - } - protected void applyOptions(NamedQueryMemento memento) { if ( memento.getHints() != null ) { memento.getHints().forEach( this::applyHint ); @@ -258,265 +121,6 @@ public abstract class AbstractSelectionQuery protected abstract String getQueryString(); - /** - * Used to validate that the specified query return type is valid (i.e. the user - * did not pass {@code Integer.class} when the selection is an entity) - */ - protected void visitQueryReturnType( - SqmQueryPart queryPart, - Class expectedResultType, - SessionFactoryImplementor factory) { - if ( queryPart instanceof SqmQuerySpec ) { - final SqmQuerySpec sqmQuerySpec = (SqmQuerySpec) queryPart; - final List> sqmSelections = sqmQuerySpec.getSelectClause().getSelections(); - - 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" ); - } - } - } - - if ( expectedResultType != null ) { - checkQueryReturnType( sqmQuerySpec, expectedResultType, factory ); - } - } - else { - final SqmQueryGroup queryGroup = (SqmQueryGroup) queryPart; - for ( SqmQueryPart sqmQueryPart : queryGroup.getQueryParts() ) { - visitQueryReturnType( sqmQueryPart, expectedResultType, factory ); - } - } - } - - protected static void checkQueryReturnType( - SqmQuerySpec querySpec, - Class expectedResultClass, - SessionFactoryImplementor sessionFactory) { - if ( isResultTypeAlwaysAllowed( expectedResultClass ) ) { - // the result-class is always safe to use (Object, ...) - return; - } - - final List> selections = querySpec.getSelectClause().getSelections(); - if ( selections.size() == 1 ) { - 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 ); - } - } - else { - verifySingularSelectionType( expectedResultClass, sessionFactory, sqmSelection ); - } - } - else if ( expectedResultClass.isArray() ) { - final Class componentType = expectedResultClass.getComponentType(); - for ( SqmSelection selection : selections ) { - verifySelectionType( componentType, sessionFactory, selection.getSelectableNode() ); - } - } - } - - /** - * Special case for a single, non-compound selection-item. It is essentially - * a special case of {@linkplain #verifySelectionType} which additionally - * handles the case where the type of the selection-item can be used to - * instantiate the result-class (result-class has a matching constructor). - * - * @apiNote We don't want to hoist this into {@linkplain #verifySelectionType} - * itself because this can only happen for the root non-compound case, and we - * want to avoid the try/catch otherwise - */ - private static void verifySingularSelectionType( - Class expectedResultClass, - SessionFactoryImplementor sessionFactory, - SqmSelection sqmSelection) { - final SqmSelectableNode selectableNode = sqmSelection.getSelectableNode(); - try { - verifySelectionType( expectedResultClass, sessionFactory, selectableNode ); - } - catch (QueryTypeMismatchException mismatchException) { - // Check for special case of a single selection item and implicit instantiation. - // See if the selected type can be used to instantiate the expected-type - final JavaType javaTypeDescriptor = selectableNode.getJavaTypeDescriptor(); - if ( javaTypeDescriptor != null ) { - final Class selectedJavaType = javaTypeDescriptor.getJavaTypeClass(); - // ignore the exception if the expected type has a constructor accepting the selected item type - if ( hasMatchingConstructor( expectedResultClass, selectedJavaType ) ) { - // ignore it - } - else { - throw mismatchException; - } - } - } - } - - private static boolean hasMatchingConstructor(Class expectedResultClass, Class selectedJavaType) { - try { - expectedResultClass.getDeclaredConstructor( selectedJavaType ); - return true; - } - catch (NoSuchMethodException e) { - return false; - } - } - - 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() - || Object.class == resultType - || List.class == resultType; - } - - private static boolean isResultTypeAlwaysAllowed(Class expectedResultClass) { - return expectedResultClass == null - || expectedResultClass == Object.class - || expectedResultClass == Object[].class - || expectedResultClass == List.class - || expectedResultClass == Map.class - || expectedResultClass == Tuple.class; - } - - protected static void verifyResultType(Class resultClass, @Nullable SqmExpressible selectionExpressible) { - if ( selectionExpressible == null ) { - // nothing we can validate - return; - } - - final JavaType selectionExpressibleJavaType = selectionExpressible.getExpressibleJavaType(); - assert selectionExpressibleJavaType != null; - - final Class selectionExpressibleJavaTypeClass = selectionExpressibleJavaType.getJavaTypeClass(); - if ( selectionExpressibleJavaTypeClass == Object.class ) { - - } - if ( selectionExpressibleJavaTypeClass != Object.class ) { - // performs a series of opt-out checks for validity... each if branch and return indicates a valid case - if ( resultClass.isAssignableFrom( selectionExpressibleJavaTypeClass ) ) { - return; - } - - if ( selectionExpressibleJavaType instanceof PrimitiveJavaType ) { - final PrimitiveJavaType primitiveJavaType = (PrimitiveJavaType) selectionExpressibleJavaType; - if ( primitiveJavaType.getPrimitiveClass() == resultClass ) { - return; - } - } - - if ( isMatchingDateType( selectionExpressibleJavaTypeClass, resultClass, selectionExpressible ) ) { - return; - } - - if ( isEntityIdType( selectionExpressible, resultClass ) ) { - return; - } - - throwQueryTypeMismatchException( resultClass, selectionExpressible ); - } - } - - private static boolean isEntityIdType(SqmExpressible selectionExpressible, Class resultClass) { - if ( selectionExpressible instanceof IdentifiableDomainType ) { - final IdentifiableDomainType identifiableDomainType = (IdentifiableDomainType) selectionExpressible; - final SimpleDomainType idType = identifiableDomainType.getIdType(); - return resultClass.isAssignableFrom( idType.getBindableJavaType() ); - } - else if ( selectionExpressible instanceof EntitySqmPathSource ) { - final EntitySqmPathSource entityPath = (EntitySqmPathSource) selectionExpressible; - final EntityDomainType entityType = entityPath.getSqmPathType(); - final SimpleDomainType idType = entityType.getIdType(); - return resultClass.isAssignableFrom( idType.getBindableJavaType() ); - } - - return false; - } - - // Special case for date because we always report java.util.Date as expression type - // But the expected resultClass could be a subtype of that, so we need to check the JdbcType - private static boolean isMatchingDateType( - Class javaTypeClass, - Class resultClass, - SqmExpressible sqmExpressible) { - return javaTypeClass == Date.class - && isMatchingDateJdbcType( resultClass, getJdbcType( sqmExpressible ) ); - } - - private static JdbcType getJdbcType(SqmExpressible sqmExpressible) { - if ( sqmExpressible instanceof BasicDomainType ) { - return ( (BasicDomainType) sqmExpressible).getJdbcType(); - } - else if ( sqmExpressible instanceof SqmPathSource ) { - final SqmPathSource pathSource = (SqmPathSource) sqmExpressible; - final DomainType domainType = pathSource.getSqmPathType(); - if ( domainType instanceof BasicDomainType ) { - return ( (BasicDomainType) domainType ).getJdbcType(); - } - } - return null; - } - - private static boolean isMatchingDateJdbcType(Class resultClass, JdbcType jdbcType) { - if ( jdbcType != null ) { - switch ( jdbcType.getDefaultSqlTypeCode() ) { - case Types.DATE: - return resultClass.isAssignableFrom( java.sql.Date.class ); - case Types.TIME: - return resultClass.isAssignableFrom( java.sql.Time.class ); - case Types.TIMESTAMP: - return resultClass.isAssignableFrom( java.sql.Timestamp.class ); - default: - return false; - } - } - else { - return false; - } - } - - private static void throwQueryTypeMismatchException(Class resultClass, SqmExpressible sqmExpressible) { - throw new QueryTypeMismatchException( String.format( - "Specified result type [%s] did not match Query selection type [%s] - multiple selections: use Tuple or array", - resultClass.getName(), - sqmExpressible.getTypeName() - ) ); - } - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // execution diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/HqlInterpretation.java b/hibernate-core/src/main/java/org/hibernate/query/spi/HqlInterpretation.java index d10c9ca028..454411d788 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/HqlInterpretation.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/HqlInterpretation.java @@ -20,4 +20,7 @@ public interface HqlInterpretation { ParameterMetadataImplementor getParameterMetadata(); DomainParameterXref getDomainParameterXref(); + + void validateResultType(Class resultType); + } diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/SimpleHqlInterpretationImpl.java b/hibernate-core/src/main/java/org/hibernate/query/spi/SimpleHqlInterpretationImpl.java index 09dea2707b..58b1a62a21 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/SimpleHqlInterpretationImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/SimpleHqlInterpretationImpl.java @@ -6,8 +6,12 @@ */ package org.hibernate.query.spi; +import java.util.concurrent.ConcurrentHashMap; + import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.internal.SqmUtil; import org.hibernate.query.sqm.tree.SqmStatement; +import org.hibernate.query.sqm.tree.select.SqmSelectStatement; /** * @author Steve Ebersole @@ -16,6 +20,7 @@ public class SimpleHqlInterpretationImpl implements HqlInterpretation { private final SqmStatement sqmStatement; private final ParameterMetadataImplementor parameterMetadata; private final DomainParameterXref domainParameterXref; + private final ConcurrentHashMap, Object> allowedReturnTypes; public SimpleHqlInterpretationImpl( SqmStatement sqmStatement, @@ -24,6 +29,7 @@ public class SimpleHqlInterpretationImpl implements HqlInterpretation { this.sqmStatement = sqmStatement; this.parameterMetadata = parameterMetadata; this.domainParameterXref = domainParameterXref; + this.allowedReturnTypes = new ConcurrentHashMap<>(); } @Override @@ -40,4 +46,15 @@ public class SimpleHqlInterpretationImpl implements HqlInterpretation { public DomainParameterXref getDomainParameterXref() { return domainParameterXref.copy(); } + + @Override + public void validateResultType(Class resultType) { + assert sqmStatement instanceof SqmSelectStatement; + if ( resultType != null && !SqmUtil.isResultTypeAlwaysAllowed( resultType ) ) { + if ( !allowedReturnTypes.containsKey( resultType ) ) { + SqmUtil.checkQueryReturnType( ( (SqmSelectStatement) sqmStatement ).getQueryPart(), resultType ); + allowedReturnTypes.put( resultType, Boolean.TRUE ); + } + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/InterpretationException.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/InterpretationException.java index c0f4de7630..2d6ba087cb 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/InterpretationException.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/InterpretationException.java @@ -31,13 +31,13 @@ public class InterpretationException extends QueryException { public InterpretationException(String query, String message) { super( - "Error interpreting query [" + message + "] [" + query + "]", + "Error interpreting query [" + message + "]", query ); } public InterpretationException(String query, Exception cause) { super( - "Error interpreting query [" + cause.getMessage() + "] [" + query + "]", + "Error interpreting query [" + cause.getMessage() + "]", query, cause ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java index 97a80e1438..7d7783cc14 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java @@ -16,18 +16,38 @@ import org.hibernate.query.Order; import org.hibernate.query.Page; import org.hibernate.query.QueryLogging; import org.hibernate.query.SelectionQuery; +import org.hibernate.query.criteria.JpaSelection; import org.hibernate.query.criteria.ValueHandlingMode; +import org.hibernate.query.hql.internal.NamedHqlQueryMementoImpl; import org.hibernate.query.hql.internal.QuerySplitter; +import org.hibernate.query.named.NamedQueryMemento; import org.hibernate.query.spi.AbstractSelectionQuery; +import org.hibernate.query.spi.HqlInterpretation; import org.hibernate.query.spi.MutableQueryOptions; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.spi.QueryInterpretationCache; import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.spi.SelectQueryPlan; import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.spi.NamedSqmQueryMemento; import org.hibernate.query.sqm.tree.SqmStatement; +import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.query.sqm.tree.select.SqmQueryGroup; +import org.hibernate.query.sqm.tree.select.SqmQueryPart; +import org.hibernate.query.sqm.tree.select.SqmQuerySpec; +import org.hibernate.query.sqm.tree.select.SqmSelectClause; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; +import org.hibernate.query.sqm.tree.select.SqmSelectableNode; +import org.hibernate.query.sqm.tree.select.SqmSelection; import org.hibernate.sql.results.internal.TupleMetadata; +import org.hibernate.type.BasicType; +import org.hibernate.type.BasicTypeRegistry; import java.util.List; +import java.util.Map; + +import jakarta.persistence.TupleElement; +import jakarta.persistence.criteria.CompoundSelection; import static java.util.stream.Collectors.toList; import static org.hibernate.cfg.QuerySettings.FAIL_ON_PAGINATION_OVER_COLLECTION_FETCH; @@ -35,6 +55,8 @@ import static org.hibernate.query.KeyedPage.KeyInterpretation.KEY_OF_FIRST_ON_NE import static org.hibernate.query.sqm.internal.KeyBasedPagination.paginate; import static org.hibernate.query.sqm.internal.KeyedResult.collectKeys; import static org.hibernate.query.sqm.internal.KeyedResult.collectResults; +import static org.hibernate.query.sqm.internal.SqmUtil.isHqlTuple; +import static org.hibernate.query.sqm.internal.SqmUtil.isSelectionAssignableToResultType; import static org.hibernate.query.sqm.internal.SqmUtil.sortSpecification; import static org.hibernate.query.sqm.tree.SqmCopyContext.noParamCopyContext; @@ -257,4 +279,157 @@ abstract class AbstractSqmSelectionQuery extends AbstractSelectionQuery { private SelectQueryPlan buildConcreteQueryPlan(SqmSelectStatement sqmStatement, QueryOptions options) { return buildConcreteQueryPlan( sqmStatement, null, null, options ); } + + protected void applyOptions(NamedSqmQueryMemento memento) { + applyOptions( (NamedQueryMemento) memento ); + + if ( memento.getFirstResult() != null ) { + setFirstResult( memento.getFirstResult() ); + } + + if ( memento.getMaxResults() != null ) { + setMaxResults( memento.getMaxResults() ); + } + + if ( memento.getParameterTypes() != null ) { + final BasicTypeRegistry basicTypeRegistry = + getSessionFactory().getTypeConfiguration().getBasicTypeRegistry(); + for ( Map.Entry entry : memento.getParameterTypes().entrySet() ) { + final BasicType type = + basicTypeRegistry.getRegisteredType( entry.getValue() ); + getParameterMetadata() + .getQueryParameter( entry.getKey() ).applyAnticipatedType( type ); + } + } + } + + protected TupleMetadata buildTupleMetadata(SqmStatement statement, Class resultType) { + if ( statement instanceof SqmSelectStatement ) { + final SqmSelectStatement select = (SqmSelectStatement) statement; + final SqmSelectClause selectClause = select.getQueryPart().getFirstQuerySpec().getSelectClause(); + final List> selections = + selectClause + .getSelections(); + return isTupleMetadataRequired( resultType, selections.get(0) ) + ? getTupleMetadata( selections ) + : null; + } + else { + return null; + } + } + + private static boolean isTupleMetadataRequired(Class resultType, SqmSelection selection) { + return isHqlTuple( selection ) + || !isInstantiableWithoutMetadata( resultType ) + && !isSelectionAssignableToResultType( selection, resultType ); + } + + private static boolean isInstantiableWithoutMetadata(Class resultType) { + return resultType == null + || resultType.isArray() + || Object.class == resultType + || List.class == resultType; + } + + private TupleMetadata getTupleMetadata(List> selections) { + if ( getQueryOptions().getTupleTransformer() == null ) { + return new TupleMetadata( buildTupleElementArray( selections ), buildTupleAliasArray( selections ) ); + } + else { + throw new IllegalArgumentException( + "Illegal combination of Tuple resultType and (non-JpaTupleBuilder) TupleTransformer: " + + getQueryOptions().getTupleTransformer() + ); + } + } + + private static TupleElement[] buildTupleElementArray(List> selections) { + if ( selections.size() == 1 ) { + final SqmSelectableNode selectableNode = selections.get( 0).getSelectableNode(); + if ( selectableNode instanceof CompoundSelection ) { + final List> selectionItems = selectableNode.getSelectionItems(); + final TupleElement[] elements = new TupleElement[ selectionItems.size() ]; + for ( int i = 0; i < selectionItems.size(); i++ ) { + elements[i] = selectionItems.get( i ); + } + return elements; + } + else { + return new TupleElement[] { selectableNode }; + } + } + else { + final TupleElement[] elements = new TupleElement[ selections.size() ]; + for ( int i = 0; i < selections.size(); i++ ) { + elements[i] = selections.get( i ).getSelectableNode(); + } + return elements; + } + } + + private static String[] buildTupleAliasArray(List> selections) { + if ( selections.size() == 1 ) { + final SqmSelectableNode selectableNode = selections.get(0).getSelectableNode(); + if ( selectableNode instanceof CompoundSelection ) { + final List> selectionItems = selectableNode.getSelectionItems(); + final String[] elements = new String[ selectionItems.size() ]; + for ( int i = 0; i < selectionItems.size(); i++ ) { + elements[i] = selectionItems.get( i ).getAlias(); + } + return elements; + } + else { + return new String[] { selectableNode.getAlias() }; + } + } + else { + final String[] elements = new String[ selections.size() ]; + for ( int i = 0; i < selections.size(); i++ ) { + elements[i] = selections.get( i ).getAlias(); + } + return elements; + } + } + + protected static void validateCriteriaQuery(SqmQueryPart queryPart) { + if ( queryPart instanceof SqmQuerySpec ) { + final SqmQuerySpec sqmQuerySpec = (SqmQuerySpec) queryPart; + final List> selections = sqmQuerySpec.getSelectClause().getSelections(); + if ( selections.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" ); + } + } + } + else { + final SqmQueryGroup queryGroup = (SqmQueryGroup) queryPart; + for ( SqmQueryPart part : queryGroup.getQueryParts() ) { + validateCriteriaQuery( part ); + } + } + } + + protected static HqlInterpretation interpretation( + NamedHqlQueryMementoImpl memento, + Class expectedResultType, + SharedSessionContractImplementor session) { + final QueryEngine queryEngine = session.getFactory().getQueryEngine(); + final QueryInterpretationCache interpretationCache = queryEngine.getInterpretationCache(); + final HqlInterpretation interpretation = interpretationCache.resolveHqlInterpretation( + memento.getHqlString(), + expectedResultType, + queryEngine.getHqlTranslator() + ); + return interpretation; + } } 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 82b12007bd..785f0ccaf0 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 @@ -9,7 +9,6 @@ package org.hibernate.query.sqm.internal; import java.io.Serializable; import java.time.Instant; import java.util.ArrayList; -import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; @@ -28,7 +27,6 @@ import org.hibernate.LockOptions; import org.hibernate.ScrollMode; import org.hibernate.engine.query.spi.EntityGraphQueryHint; import org.hibernate.engine.spi.LoadQueryInfluencers; -import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.Generator; import org.hibernate.graph.GraphSemantic; @@ -47,13 +45,11 @@ import org.hibernate.persister.entity.AbstractEntityPersister; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.BindableType; import org.hibernate.query.IllegalQueryOperationException; -import org.hibernate.query.ImmutableEntityUpdateQueryHandlingMode; import org.hibernate.query.Order; import org.hibernate.query.Page; import org.hibernate.query.Query; import org.hibernate.query.QueryParameter; import org.hibernate.query.ResultListTransformer; -import org.hibernate.query.SemanticException; import org.hibernate.query.TupleTransformer; import org.hibernate.query.criteria.internal.NamedCriteriaQueryMementoImpl; import org.hibernate.query.hql.internal.NamedHqlQueryMementoImpl; @@ -61,7 +57,6 @@ import org.hibernate.query.hql.internal.QuerySplitter; import org.hibernate.query.hql.spi.SqmQueryImplementor; import org.hibernate.query.internal.DelegatingDomainQueryExecutionContext; import org.hibernate.query.internal.ParameterMetadataImpl; -import org.hibernate.query.internal.QueryParameterBindingsImpl; import org.hibernate.query.named.NamedQueryMemento; import org.hibernate.query.spi.DelegatingQueryOptions; import org.hibernate.query.spi.DomainQueryExecutionContext; @@ -69,7 +64,6 @@ import org.hibernate.query.spi.HqlInterpretation; import org.hibernate.query.spi.MutableQueryOptions; import org.hibernate.query.spi.NonSelectQueryPlan; import org.hibernate.query.spi.ParameterMetadataImplementor; -import org.hibernate.query.spi.QueryEngine; import org.hibernate.query.spi.QueryInterpretationCache; import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.spi.QueryParameterBindings; @@ -78,24 +72,20 @@ import org.hibernate.query.spi.SelectQueryPlan; import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.query.sqm.internal.SqmInterpretationsKey.InterpretationsKeySource; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; +import org.hibernate.query.sqm.tree.AbstractSqmDmlStatement; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.SqmStatement; -import org.hibernate.query.sqm.tree.SqmTypedNode; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.expression.JpaCriteriaParameter; -import org.hibernate.query.sqm.tree.expression.SqmExpression; import org.hibernate.query.sqm.tree.expression.SqmJpaCriteriaParameterWrapper; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.from.SqmRoot; -import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement; import org.hibernate.query.sqm.tree.insert.SqmValues; import org.hibernate.query.sqm.tree.select.SqmQueryPart; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; -import org.hibernate.query.sqm.tree.select.SqmSelectableNode; -import org.hibernate.query.sqm.tree.update.SqmAssignment; import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; import org.hibernate.sql.results.internal.TupleMetadata; import org.hibernate.sql.results.spi.ListResultsConsumer; @@ -126,7 +116,6 @@ import static org.hibernate.query.sqm.internal.SqmInterpretationsKey.createInter import static org.hibernate.query.sqm.internal.SqmInterpretationsKey.generateNonSelectKey; import static org.hibernate.query.sqm.internal.SqmUtil.isSelect; import static org.hibernate.query.sqm.internal.SqmUtil.verifyIsNonSelectStatement; -import static org.hibernate.query.sqm.internal.TypecheckUtil.assertAssignable; /** * {@link Query} implementation based on an SQM @@ -144,7 +133,7 @@ public class QuerySqmImpl private final ParameterMetadataImplementor parameterMetadata; private final DomainParameterXref domainParameterXref; - private final QueryParameterBindingsImpl parameterBindings; + private final QueryParameterBindings parameterBindings; private final Class resultType; private final TupleMetadata tupleMetadata; @@ -156,29 +145,13 @@ public class QuerySqmImpl NamedHqlQueryMementoImpl memento, Class expectedResultType, SharedSessionContractImplementor session) { - super( session ); - - this.hql = memento.getHqlString(); - this.resultType = expectedResultType; - - final QueryEngine queryEngine = session.getFactory().getQueryEngine(); - final QueryInterpretationCache interpretationCache = queryEngine.getInterpretationCache(); - final HqlInterpretation hqlInterpretation = - interpretationCache.resolveHqlInterpretation( hql, expectedResultType, queryEngine.getHqlTranslator() ); - - this.sqm = hqlInterpretation.getSqmStatement(); - - this.parameterMetadata = hqlInterpretation.getParameterMetadata(); - this.domainParameterXref = hqlInterpretation.getDomainParameterXref(); - - this.parameterBindings = QueryParameterBindingsImpl.from( parameterMetadata, session.getFactory() ); - - validateStatement( sqm, resultType ); - setComment( hql ); - - + this( + memento.getHqlString(), + interpretation( memento, expectedResultType, session ), + expectedResultType, + session + ); applyOptions( memento ); - this.tupleMetadata = buildTupleMetadata( sqm, resultType ); } public QuerySqmImpl( @@ -207,9 +180,16 @@ public class QuerySqmImpl this.parameterMetadata = hqlInterpretation.getParameterMetadata(); this.domainParameterXref = hqlInterpretation.getDomainParameterXref(); - this.parameterBindings = QueryParameterBindingsImpl.from( parameterMetadata, session.getFactory() ); + this.parameterBindings = parameterMetadata.createBindings( session.getFactory() ); - validateStatement( sqm, resultType ); + if ( sqm instanceof SqmSelectStatement ) { + hqlInterpretation.validateResultType( resultType ); + } + else { + if ( resultType != null ) { + throw new IllegalQueryOperationException( "Result type given for a non-SELECT Query", hql, null ); + } + } setComment( hql ); this.tupleMetadata = buildTupleMetadata( sqm, resultType ); @@ -243,7 +223,7 @@ public class QuerySqmImpl parameterMetadata = new ParameterMetadataImpl( domainParameterXref.getQueryParameters() ); } - parameterBindings = QueryParameterBindingsImpl.from( parameterMetadata, producer.getFactory() ); + this.parameterBindings = parameterMetadata.createBindings( producer.getFactory() ); // Parameters might be created through HibernateCriteriaBuilder.value which we need to bind here for ( SqmParameter sqmParameter : domainParameterXref.getParameterResolutions().getSqmParameters() ) { @@ -251,7 +231,20 @@ public class QuerySqmImpl bindCriteriaParameter((SqmJpaCriteriaParameterWrapper) sqmParameter); } } - validateStatement( sqm, expectedResultType ); + if ( sqm instanceof SqmSelectStatement ) { + final SqmSelectStatement selectStatement = (SqmSelectStatement) sqm; + final SqmQueryPart queryPart = selectStatement.getQueryPart(); + // For criteria queries, we have to validate the fetch structure here + queryPart.validateQueryStructureAndFetchOwners(); + validateCriteriaQuery( queryPart ); + selectStatement.validateResultType( expectedResultType ); + } + else { + if ( expectedResultType != null ) { + throw new IllegalQueryOperationException( "Result type given for a non-SELECT Query", hql, null ); + } + ( (AbstractSqmDmlStatement) sqm ).validate( hql ); + } resultType = expectedResultType; tupleMetadata = buildTupleMetadata( criteria, expectedResultType ); @@ -269,127 +262,6 @@ public class QuerySqmImpl } } - private void validateStatement(SqmStatement sqmStatement, Class resultType) { - if ( sqmStatement instanceof SqmSelectStatement ) { - 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 ) { - throw new IllegalQueryOperationException( "Result type given for a non-SELECT Query", hql, null ); - } - 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 ) { - verifyInsertTypesMatch( hql, (SqmInsertStatement) sqmStatement ); - } - } - } - - private void verifyImmutableEntityUpdate( - String hqlString, - SqmUpdateStatement sqmStatement, - SessionFactoryImplementor factory) { - final EntityPersister persister = - factory.getMappingMetamodel().getEntityDescriptor( sqmStatement.getTarget().getEntityName() ); - if ( !persister.isMutable() ) { - final ImmutableEntityUpdateQueryHandlingMode mode = - factory.getSessionFactoryOptions().getImmutableEntityUpdateQueryHandlingMode(); - final String querySpaces = Arrays.toString( persister.getQuerySpaces() ); - switch ( mode ) { - case WARNING: - LOG.immutableEntityUpdateQuery( hqlString, querySpaces ); - break; - case EXCEPTION: - throw new HibernateException( "The query [" + hqlString + "] attempts to update an immutable entity: " - + querySpaces ); - default: - throw new UnsupportedOperationException( "The " + mode + " is not supported" ); - } - } - } - - private void verifyUpdateTypesMatch(String hqlString, SqmUpdateStatement sqmStatement) { - final List> assignments = sqmStatement.getSetClause().getAssignments(); - for ( int i = 0; i < assignments.size(); i++ ) { - final SqmAssignment assignment = assignments.get( i ); - final SqmPath targetPath = assignment.getTargetPath(); - final SqmExpression expression = assignment.getValue(); - assertAssignable( hqlString, targetPath, expression, getSessionFactory() ); - } - } - - - private void verifyInsertTypesMatch(String hqlString, SqmInsertStatement sqmStatement) { - final List> insertionTargetPaths = sqmStatement.getInsertionTargetPaths(); - if ( sqmStatement instanceof SqmInsertValuesStatement ) { - final SqmInsertValuesStatement statement = (SqmInsertValuesStatement) sqmStatement; - for ( SqmValues sqmValues : statement.getValuesList() ) { - verifyInsertTypesMatch( hqlString, insertionTargetPaths, sqmValues.getExpressions() ); - } - } - else { - final SqmInsertSelectStatement statement = (SqmInsertSelectStatement) sqmStatement; - final List> selections = - statement.getSelectQueryPart() - .getFirstQuerySpec() - .getSelectClause() - .getSelectionItems(); - verifyInsertTypesMatch( hqlString, insertionTargetPaths, selections ); - statement.getSelectQueryPart().validateQueryStructureAndFetchOwners(); - } - } - - private void verifyInsertTypesMatch( - String hqlString, - List> insertionTargetPaths, - List> expressions) { - final int size = insertionTargetPaths.size(); - final int expressionsSize = expressions.size(); - if ( size != expressionsSize ) { - throw new SemanticException( - String.format( - "Expected insert attribute count [%d] did not match Query selection count [%d]", - size, - expressionsSize - ), - hqlString, - null - ); - } - for ( int i = 0; i < expressionsSize; i++ ) { - final SqmTypedNode expression = expressions.get( i ); - final SqmPath targetPath = insertionTargetPaths.get(i); - assertAssignable( hqlString, targetPath, expression, getSessionFactory() ); -// if ( expression.getNodeJavaType() == null ) { -// continue; -// } -// if ( insertionTargetPaths.get( i ).getJavaTypeDescriptor() != expression.getNodeJavaType() ) { -// throw new SemanticException( -// String.format( -// "Expected insert attribute type [%s] did not match Query selection type [%s] at selection index [%d]", -// insertionTargetPaths.get( i ).getJavaTypeDescriptor().getTypeName(), -// expression.getNodeJavaType().getTypeName(), -// i -// ), -// hqlString, -// null -// ); -// } - } - } - @Override public TupleMetadata getTupleMetadata() { return tupleMetadata; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java index 3792553909..47b1ac97cb 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java @@ -31,19 +31,15 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.graph.spi.AppliedGraph; import org.hibernate.internal.util.collections.IdentitySet; import org.hibernate.query.BindableType; -import org.hibernate.query.Page; import org.hibernate.query.QueryParameter; -import org.hibernate.query.SelectionQuery; import org.hibernate.query.criteria.internal.NamedCriteriaQueryMementoImpl; import org.hibernate.query.hql.internal.NamedHqlQueryMementoImpl; import org.hibernate.query.internal.DelegatingDomainQueryExecutionContext; import org.hibernate.query.internal.ParameterMetadataImpl; -import org.hibernate.query.internal.QueryParameterBindingsImpl; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.spi.HqlInterpretation; import org.hibernate.query.spi.MutableQueryOptions; import org.hibernate.query.spi.ParameterMetadataImplementor; -import org.hibernate.query.spi.QueryEngine; import org.hibernate.query.spi.QueryInterpretationCache; import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.spi.QueryParameterBindings; @@ -56,6 +52,7 @@ import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.expression.JpaCriteriaParameter; import org.hibernate.query.sqm.tree.expression.SqmJpaCriteriaParameterWrapper; import org.hibernate.query.sqm.tree.expression.SqmParameter; +import org.hibernate.query.sqm.tree.select.SqmQueryPart; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; import org.hibernate.query.sqm.tree.select.SqmSelection; import org.hibernate.sql.results.internal.TupleMetadata; @@ -86,7 +83,7 @@ public class SqmSelectionQueryImpl extends AbstractSqmSelectionQuery private final ParameterMetadataImplementor parameterMetadata; private final DomainParameterXref domainParameterXref; - private final QueryParameterBindingsImpl parameterBindings; + private final QueryParameterBindings parameterBindings; private final Class expectedResultType; private final Class resultType; @@ -100,76 +97,34 @@ public class SqmSelectionQueryImpl extends AbstractSqmSelectionQuery super( session ); this.hql = hql; + SqmUtil.verifyIsSelectStatement( hqlInterpretation.getSqmStatement(), hql ); this.sqm = (SqmSelectStatement) hqlInterpretation.getSqmStatement(); this.parameterMetadata = hqlInterpretation.getParameterMetadata(); this.domainParameterXref = hqlInterpretation.getDomainParameterXref(); - this.parameterBindings = QueryParameterBindingsImpl.from( parameterMetadata, session.getFactory() ); + this.parameterBindings = parameterMetadata.createBindings( session.getFactory() ); + this.expectedResultType = expectedResultType; - visitQueryReturnType( sqm.getQueryPart(), expectedResultType, getSessionFactory() ); - this.resultType = determineResultType( sqm ); + this.resultType = determineResultType( sqm, expectedResultType ); this.tupleMetadata = buildTupleMetadata( sqm, expectedResultType ); + hqlInterpretation.validateResultType( resultType ); setComment( hql ); } - private Class determineResultType(SqmSelectStatement sqm) { - final List> selections = sqm.getQuerySpec().getSelectClause().getSelections(); - if ( selections.size() == 1 ) { - if ( Object[].class.equals( expectedResultType ) ) { - // for JPA compatibility - return Object[].class; - } - else { - final SqmSelection selection = selections.get(0); - if ( isSelectionAssignableToResultType( selection, expectedResultType ) ) { - return selection.getNodeJavaType().getJavaTypeClass(); - } - else { - // let's assume there's some - // way to instantiate it - return expectedResultType; - } - } - } - else if ( expectedResultType != null ) { - // assume we can repackage the tuple as - // the given type (worry about how later) - return expectedResultType; - } - else { - // for JPA compatibility - return Object[].class; - } - } - public SqmSelectionQueryImpl( NamedHqlQueryMementoImpl memento, Class resultType, SharedSessionContractImplementor session) { - super( session ); - this.hql = memento.getHqlString(); - this.expectedResultType = resultType; - this.resultType = resultType; + this( + memento.getHqlString(), + interpretation( memento, resultType, session ), + resultType, + session + ); - final QueryEngine queryEngine = session.getFactory().getQueryEngine(); - final QueryInterpretationCache interpretationCache = queryEngine.getInterpretationCache(); - final HqlInterpretation hqlInterpretation = - interpretationCache.resolveHqlInterpretation( hql, resultType, queryEngine.getHqlTranslator() ); - - SqmUtil.verifyIsSelectStatement( hqlInterpretation.getSqmStatement(), hql ); - this.sqm = (SqmSelectStatement) hqlInterpretation.getSqmStatement(); - - this.parameterMetadata = hqlInterpretation.getParameterMetadata(); - this.domainParameterXref = hqlInterpretation.getDomainParameterXref(); - - this.parameterBindings = QueryParameterBindingsImpl.from( parameterMetadata, session.getFactory() ); - - setComment( hql ); applyOptions( memento ); - - this.tupleMetadata = buildTupleMetadata( sqm, resultType ); } public SqmSelectionQueryImpl( @@ -201,7 +156,7 @@ public class SqmSelectionQueryImpl extends AbstractSqmSelectionQuery ? new ParameterMetadataImpl( domainParameterXref.getQueryParameters() ) : ParameterMetadataImpl.EMPTY; - this.parameterBindings = QueryParameterBindingsImpl.from( parameterMetadata, session.getFactory() ); + this.parameterBindings = parameterMetadata.createBindings( session.getFactory() ); // Parameters might be created through HibernateCriteriaBuilder.value which we need to bind here for ( SqmParameter sqmParameter : domainParameterXref.getParameterResolutions().getSqmParameters() ) { @@ -211,14 +166,49 @@ public class SqmSelectionQueryImpl extends AbstractSqmSelectionQuery } this.expectedResultType = expectedResultType; - this.resultType = determineResultType( sqm ); - visitQueryReturnType( sqm.getQueryPart(), expectedResultType, getSessionFactory() ); + this.resultType = determineResultType( sqm, expectedResultType ); + + final SqmQueryPart queryPart = sqm.getQueryPart(); + // For criteria queries, we have to validate the fetch structure here + queryPart.validateQueryStructureAndFetchOwners(); + validateCriteriaQuery( queryPart ); + sqm.validateResultType( resultType ); setComment( hql ); this.tupleMetadata = buildTupleMetadata( sqm, expectedResultType ); } + private static Class determineResultType(SqmSelectStatement sqm, Class expectedResultType) { + final List> selections = sqm.getQuerySpec().getSelectClause().getSelections(); + if ( selections.size() == 1 ) { + if ( Object[].class.equals( expectedResultType ) ) { + // for JPA compatibility + return Object[].class; + } + else { + final SqmSelection selection = selections.get(0); + if ( isSelectionAssignableToResultType( selection, expectedResultType ) ) { + return selection.getNodeJavaType().getJavaTypeClass(); + } + else { + // let's assume there's some + // way to instantiate it + return expectedResultType; + } + } + } + else if ( expectedResultType != null ) { + // assume we can repackage the tuple as + // the given type (worry about how later) + return expectedResultType; + } + else { + // for JPA compatibility + return Object[].class; + } + } + private void bindCriteriaParameter(SqmJpaCriteriaParameterWrapper sqmParameter) { final JpaCriteriaParameter jpaCriteriaParameter = sqmParameter.getJpaCriteriaParameter(); final T value = jpaCriteriaParameter.getValue(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java index e272212ea1..1f02cb1229 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java @@ -6,16 +6,17 @@ */ package org.hibernate.query.sqm.internal; +import java.sql.Types; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Date; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.StringTokenizer; import java.util.function.Function; @@ -36,10 +37,18 @@ import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.ModelPartContainer; import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.model.domain.BasicDomainType; +import org.hibernate.metamodel.model.domain.DomainType; +import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.metamodel.model.domain.IdentifiableDomainType; +import org.hibernate.metamodel.model.domain.SimpleDomainType; +import org.hibernate.metamodel.model.domain.internal.EntitySqmPathSource; import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.IllegalSelectQueryException; import org.hibernate.query.Order; +import org.hibernate.query.QueryTypeMismatchException; import org.hibernate.query.criteria.JpaOrder; +import org.hibernate.query.criteria.JpaSelection; import org.hibernate.query.spi.QueryParameterBinding; import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.query.spi.QueryParameterImplementor; @@ -66,6 +75,7 @@ import org.hibernate.query.sqm.tree.from.SqmJoin; import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.select.SqmOrderByClause; +import org.hibernate.query.sqm.tree.select.SqmQueryGroup; import org.hibernate.query.sqm.tree.select.SqmQueryPart; import org.hibernate.query.sqm.tree.select.SqmQuerySpec; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; @@ -84,10 +94,15 @@ import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.type.JavaObjectType; import org.hibernate.type.descriptor.converter.spi.BasicValueConverter; import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.java.spi.PrimitiveJavaType; +import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.internal.BasicTypeImpl; import org.hibernate.type.internal.ConvertedBasicTypeImpl; import org.hibernate.type.spi.TypeConfiguration; +import jakarta.persistence.Tuple; +import org.checkerframework.checker.nullness.qual.Nullable; + import static java.util.stream.Collectors.toList; import static org.hibernate.internal.util.NullnessUtil.castNonNull; import static org.hibernate.query.sqm.tree.jpa.ParameterCollector.collectParameters; @@ -857,4 +872,247 @@ public class SqmUtil { return jpaCriteriaParamResolutions; } } + + /** + * Used to validate that the specified query return type is valid (i.e. the user + * did not pass {@code Integer.class} when the selection is an entity) + */ + public static void validateQueryReturnType(SqmQueryPart queryPart, @Nullable Class expectedResultType) { + if ( expectedResultType != null && !isResultTypeAlwaysAllowed( expectedResultType ) ) { + // the result-class is always safe to use (Object, ...) + checkQueryReturnType( queryPart, expectedResultType ); + } + } + + /** + * Similar to {@link #validateQueryReturnType(SqmQueryPart, Class)} but does not check if {@link #isResultTypeAlwaysAllowed(Class)}. + */ + public static void checkQueryReturnType(SqmQueryPart queryPart, Class expectedResultType) { + if ( queryPart instanceof SqmQuerySpec ) { + checkQueryReturnType( (SqmQuerySpec) queryPart, expectedResultType ); + } + else { + final SqmQueryGroup queryGroup = (SqmQueryGroup) queryPart; + for ( SqmQueryPart sqmQueryPart : queryGroup.getQueryParts() ) { + checkQueryReturnType( sqmQueryPart, expectedResultType ); + } + } + } + + private static void checkQueryReturnType(SqmQuerySpec querySpec, Class expectedResultClass) { + final SessionFactoryImplementor sessionFactory = querySpec.nodeBuilder().getSessionFactory(); + final List> selections = querySpec.getSelectClause().getSelections(); + if ( selections == null || selections.isEmpty() ) { + // make sure there is at least one root + final List> sqmRoots = querySpec.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 ) { + verifySingularSelectionType( expectedResultClass, sessionFactory, sqmRoots.get( 0 ) ); + } + else { + throw new IllegalArgumentException( "Criteria has multiple query roots" ); + } + } + else if ( selections.size() == 1 ) { + 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 ); + } + } + else { + verifySingularSelectionType( expectedResultClass, sessionFactory, sqmSelection.getSelectableNode() ); + } + } + else if ( expectedResultClass.isArray() ) { + final Class componentType = expectedResultClass.getComponentType(); + for ( SqmSelection selection : selections ) { + verifySelectionType( componentType, sessionFactory, selection.getSelectableNode() ); + } + } + } + + /** + * Special case for a single, non-compound selection-item. It is essentially + * a special case of {@linkplain #verifySelectionType} which additionally + * handles the case where the type of the selection-item can be used to + * instantiate the result-class (result-class has a matching constructor). + * + * @apiNote We don't want to hoist this into {@linkplain #verifySelectionType} + * itself because this can only happen for the root non-compound case, and we + * want to avoid the try/catch otherwise + */ + private static void verifySingularSelectionType( + Class expectedResultClass, + SessionFactoryImplementor sessionFactory, + SqmSelectableNode selectableNode) { + try { + verifySelectionType( expectedResultClass, sessionFactory, selectableNode ); + } + catch (QueryTypeMismatchException mismatchException) { + // Check for special case of a single selection item and implicit instantiation. + // See if the selected type can be used to instantiate the expected-type + final JavaType javaTypeDescriptor = selectableNode.getJavaTypeDescriptor(); + if ( javaTypeDescriptor != null ) { + final Class selectedJavaType = javaTypeDescriptor.getJavaTypeClass(); + // ignore the exception if the expected type has a constructor accepting the selected item type + if ( hasMatchingConstructor( expectedResultClass, selectedJavaType ) ) { + // ignore it + } + else { + throw mismatchException; + } + } + } + } + + private static boolean hasMatchingConstructor(Class expectedResultClass, Class selectedJavaType) { + try { + expectedResultClass.getDeclaredConstructor( selectedJavaType ); + return true; + } + catch (NoSuchMethodException e) { + return false; + } + } + + 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() ); + } + } + + public static boolean isResultTypeAlwaysAllowed(Class expectedResultClass) { + return expectedResultClass == null + || expectedResultClass == Object.class + || expectedResultClass == Object[].class + || expectedResultClass == List.class + || expectedResultClass == Map.class + || expectedResultClass == Tuple.class; + } + + protected static void verifyResultType(Class resultClass, @Nullable SqmExpressible selectionExpressible) { + if ( selectionExpressible == null ) { + // nothing we can validate + return; + } + + final JavaType selectionExpressibleJavaType = selectionExpressible.getExpressibleJavaType(); + assert selectionExpressibleJavaType != null; + + final Class selectionExpressibleJavaTypeClass = selectionExpressibleJavaType.getJavaTypeClass(); + if ( selectionExpressibleJavaTypeClass == Object.class ) { + + } + if ( selectionExpressibleJavaTypeClass != Object.class ) { + // performs a series of opt-out checks for validity... each if branch and return indicates a valid case + if ( resultClass.isAssignableFrom( selectionExpressibleJavaTypeClass ) ) { + return; + } + + if ( selectionExpressibleJavaType instanceof PrimitiveJavaType ) { + final PrimitiveJavaType primitiveJavaType = (PrimitiveJavaType) selectionExpressibleJavaType; + if ( primitiveJavaType.getPrimitiveClass() == resultClass ) { + return; + } + } + + if ( isMatchingDateType( selectionExpressibleJavaTypeClass, resultClass, selectionExpressible ) ) { + return; + } + + if ( isEntityIdType( selectionExpressible, resultClass ) ) { + return; + } + + throwQueryTypeMismatchException( resultClass, selectionExpressible ); + } + } + + private static boolean isEntityIdType(SqmExpressible selectionExpressible, Class resultClass) { + if ( selectionExpressible instanceof IdentifiableDomainType ) { + final IdentifiableDomainType identifiableDomainType = (IdentifiableDomainType) selectionExpressible; + final SimpleDomainType idType = identifiableDomainType.getIdType(); + return resultClass.isAssignableFrom( idType.getBindableJavaType() ); + } + else if ( selectionExpressible instanceof EntitySqmPathSource ) { + final EntitySqmPathSource entityPath = (EntitySqmPathSource) selectionExpressible; + final EntityDomainType entityType = entityPath.getSqmPathType(); + final SimpleDomainType idType = entityType.getIdType(); + return resultClass.isAssignableFrom( idType.getBindableJavaType() ); + } + + return false; + } + + // Special case for date because we always report java.util.Date as expression type + // But the expected resultClass could be a subtype of that, so we need to check the JdbcType + private static boolean isMatchingDateType( + Class javaTypeClass, + Class resultClass, + SqmExpressible sqmExpressible) { + return javaTypeClass == Date.class + && isMatchingDateJdbcType( resultClass, getJdbcType( sqmExpressible ) ); + } + + private static JdbcType getJdbcType(SqmExpressible sqmExpressible) { + if ( sqmExpressible instanceof BasicDomainType ) { + return ( (BasicDomainType) sqmExpressible).getJdbcType(); + } + else if ( sqmExpressible instanceof SqmPathSource ) { + final SqmPathSource pathSource = (SqmPathSource) sqmExpressible; + final DomainType domainType = pathSource.getSqmPathType(); + if ( domainType instanceof BasicDomainType ) { + return ( (BasicDomainType) domainType ).getJdbcType(); + } + } + return null; + } + + private static boolean isMatchingDateJdbcType(Class resultClass, JdbcType jdbcType) { + if ( jdbcType != null ) { + switch ( jdbcType.getDefaultSqlTypeCode() ) { + case Types.DATE: + return resultClass.isAssignableFrom( java.sql.Date.class ); + case Types.TIME: + return resultClass.isAssignableFrom( java.sql.Time.class ); + case Types.TIMESTAMP: + return resultClass.isAssignableFrom( java.sql.Timestamp.class ); + default: + return false; + } + } + else { + return false; + } + } + + private static void throwQueryTypeMismatchException(Class resultClass, SqmExpressible sqmExpressible) { + throw new QueryTypeMismatchException( String.format( + "Specified result type [%s] did not match Query selection type [%s] - multiple selections: use Tuple or array", + resultClass.getName(), + sqmExpressible.getTypeName() + ) ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmDmlStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmDmlStatement.java index 23857af453..d35526fc5b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmDmlStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmDmlStatement.java @@ -25,6 +25,7 @@ import org.hibernate.query.sqm.tree.select.SqmSelectQuery; import org.hibernate.query.sqm.tree.select.SqmSubQuery; import jakarta.persistence.criteria.AbstractQuery; +import org.checkerframework.checker.nullness.qual.Nullable; /** * @author Steve Ebersole @@ -72,6 +73,8 @@ public abstract class AbstractSqmDmlStatement } } + public abstract void validate(@Nullable String hql); + @Override public Collection> getCteStatements() { return cteStatements.values(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/delete/SqmDeleteStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/delete/SqmDeleteStatement.java index c6d5d6752e..0acd3cee00 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/delete/SqmDeleteStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/delete/SqmDeleteStatement.java @@ -20,10 +20,10 @@ import org.hibernate.query.sqm.tree.cte.SqmCteStatement; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.from.SqmFromClause; import org.hibernate.query.sqm.tree.from.SqmRoot; -import org.hibernate.query.sqm.tree.predicate.SqmWhereClause; import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.Predicate; +import org.checkerframework.checker.nullness.qual.Nullable; /** * @author Steve Ebersole @@ -103,6 +103,11 @@ public class SqmDeleteStatement return statement; } + @Override + public void validate(@Nullable String hql) { + // No-op + } + @Override public SqmDeleteStatement where(Expression restriction) { setWhere( restriction ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/AbstractSqmInsertStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/AbstractSqmInsertStatement.java index 7d6f6a9f3b..889e7d6add 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/AbstractSqmInsertStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/AbstractSqmInsertStatement.java @@ -14,6 +14,7 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.SemanticException; import org.hibernate.query.criteria.JpaConflictClause; import org.hibernate.query.criteria.JpaCriteriaInsert; @@ -22,6 +23,7 @@ import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SqmQuerySource; import org.hibernate.query.sqm.tree.AbstractSqmDmlStatement; import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmTypedNode; import org.hibernate.query.sqm.tree.cte.SqmCteStatement; import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.domain.SqmPolymorphicRootDescriptor; @@ -31,6 +33,8 @@ import org.hibernate.query.sqm.tree.from.SqmRoot; import jakarta.persistence.criteria.Path; import org.checkerframework.checker.nullness.qual.Nullable; +import static org.hibernate.query.sqm.internal.TypecheckUtil.assertAssignable; + /** * Convenience base class for InsertSqmStatement implementations. * @@ -85,6 +89,45 @@ public abstract class AbstractSqmInsertStatement extends AbstractSqmDmlStatem } } + protected void verifyInsertTypesMatch( + List> insertionTargetPaths, + List> expressions) { + final int size = insertionTargetPaths.size(); + final int expressionsSize = expressions.size(); + if ( size != expressionsSize ) { + throw new SemanticException( + String.format( + "Expected insert attribute count [%d] did not match Query selection count [%d]", + size, + expressionsSize + ), + null, + null + ); + } + final SessionFactoryImplementor factory = nodeBuilder().getSessionFactory(); + for ( int i = 0; i < expressionsSize; i++ ) { + final SqmTypedNode expression = expressions.get( i ); + final SqmPath targetPath = insertionTargetPaths.get(i); + assertAssignable( null, targetPath, expression, factory ); +// if ( expression.getNodeJavaType() == null ) { +// continue; +// } +// if ( insertionTargetPaths.get( i ).getJavaTypeDescriptor() != expression.getNodeJavaType() ) { +// throw new SemanticException( +// String.format( +// "Expected insert attribute type [%s] did not match Query selection type [%s] at selection index [%d]", +// insertionTargetPaths.get( i ).getJavaTypeDescriptor().getTypeName(), +// expression.getNodeJavaType().getTypeName(), +// i +// ), +// hqlString, +// null +// ); +// } + } + } + @Override public void setTarget(JpaRoot root) { if ( root.getModel() instanceof SqmPolymorphicRootDescriptor ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertSelectStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertSelectStatement.java index 8b431eab82..90d0396bf2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertSelectStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertSelectStatement.java @@ -25,10 +25,12 @@ import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.select.SqmQueryPart; import org.hibernate.query.sqm.tree.select.SqmQuerySpec; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; +import org.hibernate.query.sqm.tree.select.SqmSelectableNode; import jakarta.persistence.Tuple; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Path; +import org.checkerframework.checker.nullness.qual.Nullable; /** * @author Steve Ebersole @@ -90,6 +92,17 @@ public class SqmInsertSelectStatement extends AbstractSqmInsertStatement i ); } + @Override + public void validate(@Nullable String hql) { + final List> insertionTargetPaths = getInsertionTargetPaths(); + final List> selections = getSelectQueryPart() + .getFirstQuerySpec() + .getSelectClause() + .getSelectionItems(); + verifyInsertTypesMatch( insertionTargetPaths, selections ); + getSelectQueryPart().validateQueryStructureAndFetchOwners(); + } + @Override public SqmInsertSelectStatement select(CriteriaQuery criteriaQuery) { final SqmSelectStatement selectStatement = (SqmSelectStatement) criteriaQuery; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertValuesStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertValuesStatement.java index fac062cbe1..d51a1b0977 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertValuesStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertValuesStatement.java @@ -113,6 +113,14 @@ public class SqmInsertValuesStatement extends AbstractSqmInsertStatement i ); } + @Override + public void validate(@Nullable String hql) { + final List> insertionTargetPaths = getInsertionTargetPaths(); + for ( SqmValues sqmValues : getValuesList() ) { + verifyInsertTypesMatch( insertionTargetPaths, sqmValues.getExpressions() ); + } + } + public List getValuesList() { return valuesList == null ? Collections.emptyList() diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java index c92455af9d..631d3eeff9 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java @@ -149,6 +149,10 @@ public class SqmSelectStatement extends AbstractSqmSelectQuery implements return statement; } + public void validateResultType(Class resultType) { + SqmUtil.validateQueryReturnType( getQueryPart(), resultType ); + } + @Override public SqmQuerySource getQuerySource() { return querySource; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmUpdateStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmUpdateStatement.java index a2facfbf86..f3accf2906 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmUpdateStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmUpdateStatement.java @@ -6,9 +6,17 @@ */ package org.hibernate.query.sqm.tree.update; +import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.Set; +import org.hibernate.HibernateException; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.CoreLogging; +import org.hibernate.internal.CoreMessageLogger; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.query.ImmutableEntityUpdateQueryHandlingMode; import org.hibernate.query.SemanticException; import org.hibernate.query.criteria.JpaCriteriaUpdate; import org.hibernate.query.criteria.JpaRoot; @@ -20,7 +28,6 @@ import org.hibernate.query.sqm.tree.AbstractSqmRestrictedDmlStatement; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.SqmDeleteOrUpdateStatement; import org.hibernate.query.sqm.tree.cte.SqmCteStatement; -import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.domain.SqmPolymorphicRootDescriptor; import org.hibernate.query.sqm.tree.expression.SqmExpression; @@ -32,6 +39,7 @@ import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.metamodel.SingularAttribute; +import org.checkerframework.checker.nullness.qual.Nullable; import static org.hibernate.query.sqm.internal.TypecheckUtil.assertAssignable; @@ -41,6 +49,9 @@ import static org.hibernate.query.sqm.internal.TypecheckUtil.assertAssignable; public class SqmUpdateStatement extends AbstractSqmRestrictedDmlStatement implements SqmDeleteOrUpdateStatement, JpaCriteriaUpdate { + + private static final CoreMessageLogger LOG = CoreLogging.messageLogger( SqmUpdateStatement.class ); + private boolean versioned; private SqmSetClause setClause; @@ -110,6 +121,46 @@ public class SqmUpdateStatement return statement; } + @Override + public void validate(@Nullable String hql) { + verifyImmutableEntityUpdate( hql ); + if ( getSetClause() == null || getSetClause().getAssignments().isEmpty() ) { + throw new IllegalArgumentException( "No assignments specified as part of UPDATE criteria" ); + } + verifyUpdateTypesMatch(); + } + + private void verifyImmutableEntityUpdate(String hql) { + final SessionFactoryImplementor factory = nodeBuilder().getSessionFactory(); + final EntityPersister persister = + factory.getMappingMetamodel().getEntityDescriptor( getTarget().getEntityName() ); + if ( !persister.isMutable() ) { + final ImmutableEntityUpdateQueryHandlingMode mode = + factory.getSessionFactoryOptions().getImmutableEntityUpdateQueryHandlingMode(); + final String querySpaces = Arrays.toString( persister.getQuerySpaces() ); + switch ( mode ) { + case WARNING: + LOG.immutableEntityUpdateQuery( hql, querySpaces ); + break; + case EXCEPTION: + throw new HibernateException( "The query attempts to update an immutable entity: " + querySpaces ); + default: + throw new UnsupportedOperationException( "The " + mode + " is not supported" ); + } + } + } + + private void verifyUpdateTypesMatch() { + final SessionFactoryImplementor factory = nodeBuilder().getSessionFactory(); + final List> assignments = getSetClause().getAssignments(); + for ( int i = 0; i < assignments.size(); i++ ) { + final SqmAssignment assignment = assignments.get( i ); + final SqmPath targetPath = assignment.getTargetPath(); + final SqmExpression expression = assignment.getValue(); + assertAssignable( null, targetPath, expression, factory ); + } + } + public SqmSetClause getSetClause() { return setClause; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/immutable/ImmutableEntityUpdateQueryHandlingModeExceptionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/immutable/ImmutableEntityUpdateQueryHandlingModeExceptionTest.java index f38739fc91..902c1259aa 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/immutable/ImmutableEntityUpdateQueryHandlingModeExceptionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/immutable/ImmutableEntityUpdateQueryHandlingModeExceptionTest.java @@ -57,18 +57,14 @@ public class ImmutableEntityUpdateQueryHandlingModeExceptionTest extends BaseNon try { doInHibernate( this::sessionFactory, session -> { - session.createQuery( - "update Country " + - "set name = :name" ) - .setParameter( "name", "N/A" ) - .executeUpdate(); + session.createQuery("update Country set name = :name" ); } ); fail("Should throw PersistenceException"); } catch (PersistenceException e) { assertTrue( e instanceof HibernateException ); assertEquals( - "The query [update Country set name = :name] attempts to update an immutable entity: [Country]", + "Error interpreting query [The query attempts to update an immutable entity: [Country]] [update Country set name = :name]", e.getMessage() ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/immutable/ImmutableEntityUpdateQueryHandlingModeWarningTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/immutable/ImmutableEntityUpdateQueryHandlingModeWarningTest.java index f35b3a1e80..2ac4c0d88d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/immutable/ImmutableEntityUpdateQueryHandlingModeWarningTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/immutable/ImmutableEntityUpdateQueryHandlingModeWarningTest.java @@ -7,7 +7,7 @@ package org.hibernate.orm.test.annotations.immutable; import org.hibernate.internal.CoreMessageLogger; -import org.hibernate.query.sqm.internal.QuerySqmImpl; +import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; import org.hibernate.testing.TestForIssue; import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; @@ -30,7 +30,7 @@ public class ImmutableEntityUpdateQueryHandlingModeWarningTest extends BaseNonCo @Rule public LoggerInspectionRule logInspection = new LoggerInspectionRule( - Logger.getMessageLogger( CoreMessageLogger.class, QuerySqmImpl.class.getName() ) ); + Logger.getMessageLogger( CoreMessageLogger.class, SqmUpdateStatement.class.getName() ) ); @Override protected Class[] getAnnotatedClasses() {