HHH-18271 Avoid query validations of cached queries by doing validation eagerly. Cache allowed result types per query interpretation

This commit is contained in:
Christian Beikov 2024-08-02 12:48:45 +02:00 committed by Steve Ebersole
parent cf44c30bf2
commit 850a2a0753
20 changed files with 684 additions and 632 deletions

View File

@ -555,6 +555,7 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
queryExpressionContext.accept( this );
insertStatement.onConflict( visitConflictClause( ctx.conflictClause() ) );
insertStatement.validate( query );
return insertStatement;
}
finally {
@ -613,6 +614,7 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
insertStatement.values( valuesList );
insertStatement.onConflict( visitConflictClause( ctx.conflictClause() ) );
insertStatement.validate( query );
return insertStatement;
}
finally {
@ -684,6 +686,7 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
updateStatement.applyPredicate( visitWhereClause( whereClauseContext ) );
}
updateStatement.validate( query );
return updateStatement;
}
finally {

View File

@ -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<R>) sqmStatement ).validateResultType( resultType );
}
};
}

View File

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

View File

@ -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<R>
super( session );
}
protected TupleMetadata buildTupleMetadata(SqmStatement<?> statement, Class<R> resultType) {
if ( statement instanceof SqmSelectStatement<?> ) {
final SqmSelectStatement<?> select = (SqmSelectStatement<?>) statement;
final List<SqmSelection<?>> selections =
select.getQueryPart().getFirstQuerySpec().getSelectClause()
.getSelections();
return isTupleMetadataRequired( resultType, selections.get(0) )
? getTupleMetadata( selections )
: null;
}
else {
return null;
}
}
private static <R> boolean isTupleMetadataRequired(Class<R> resultType, SqmSelection<?> selection) {
return isHqlTuple( selection )
|| !isInstantiableWithoutMetadata( resultType )
&& !isSelectionAssignableToResultType( selection, resultType );
}
private TupleMetadata getTupleMetadata(List<SqmSelection<?>> 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<SqmSelection<?>> selections) {
if ( selections.size() == 1 ) {
final SqmSelectableNode<?> selectableNode = selections.get(0).getSelectableNode();
if ( selectableNode instanceof CompoundSelection<?> ) {
final List<? extends JpaSelection<?>> 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<SqmSelection<?>> selections) {
if ( selections.size() == 1 ) {
final SqmSelectableNode<?> selectableNode = selections.get(0).getSelectableNode();
if ( selectableNode instanceof CompoundSelection<?> ) {
final List<? extends JpaSelection<?>> 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<String, String> 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<R>
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<R> queryPart,
Class<R> expectedResultType,
SessionFactoryImplementor factory) {
if ( queryPart instanceof SqmQuerySpec<?> ) {
final SqmQuerySpec<R> sqmQuerySpec = (SqmQuerySpec<R>) queryPart;
final List<SqmSelection<?>> sqmSelections = sqmQuerySpec.getSelectClause().getSelections();
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" );
}
}
}
if ( expectedResultType != null ) {
checkQueryReturnType( sqmQuerySpec, expectedResultType, factory );
}
}
else {
final SqmQueryGroup<R> queryGroup = (SqmQueryGroup<R>) queryPart;
for ( SqmQueryPart<R> sqmQueryPart : queryGroup.getQueryParts() ) {
visitQueryReturnType( sqmQueryPart, expectedResultType, factory );
}
}
}
protected static <T> void checkQueryReturnType(
SqmQuerySpec<T> querySpec,
Class<T> expectedResultClass,
SessionFactoryImplementor sessionFactory) {
if ( isResultTypeAlwaysAllowed( expectedResultClass ) ) {
// the result-class is always safe to use (Object, ...)
return;
}
final List<SqmSelection<?>> 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 <T> void verifySingularSelectionType(
Class<T> 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 <T> boolean hasMatchingConstructor(Class<T> expectedResultClass, Class<?> selectedJavaType) {
try {
expectedResultClass.getDeclaredConstructor( selectedJavaType );
return true;
}
catch (NoSuchMethodException e) {
return false;
}
}
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()
|| Object.class == resultType
|| List.class == resultType;
}
private static <T> boolean isResultTypeAlwaysAllowed(Class<T> expectedResultClass) {
return expectedResultClass == null
|| expectedResultClass == Object.class
|| expectedResultClass == Object[].class
|| expectedResultClass == List.class
|| expectedResultClass == Map.class
|| expectedResultClass == Tuple.class;
}
protected static <T> void verifyResultType(Class<T> 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 <T> boolean isEntityIdType(SqmExpressible<?> selectionExpressible, Class<T> 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 <T> boolean isMatchingDateType(
Class<?> javaTypeClass,
Class<T> 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 <T> boolean isMatchingDateJdbcType(Class<T> 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 <T> void throwQueryTypeMismatchException(Class<T> 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

View File

@ -20,4 +20,7 @@ public interface HqlInterpretation<R> {
ParameterMetadataImplementor getParameterMetadata();
DomainParameterXref getDomainParameterXref();
void validateResultType(Class<?> resultType);
}

View File

@ -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<R> implements HqlInterpretation<R> {
private final SqmStatement<R> sqmStatement;
private final ParameterMetadataImplementor parameterMetadata;
private final DomainParameterXref domainParameterXref;
private final ConcurrentHashMap<Class<?>, Object> allowedReturnTypes;
public SimpleHqlInterpretationImpl(
SqmStatement<R> sqmStatement,
@ -24,6 +29,7 @@ public class SimpleHqlInterpretationImpl<R> implements HqlInterpretation<R> {
this.sqmStatement = sqmStatement;
this.parameterMetadata = parameterMetadata;
this.domainParameterXref = domainParameterXref;
this.allowedReturnTypes = new ConcurrentHashMap<>();
}
@Override
@ -40,4 +46,15 @@ public class SimpleHqlInterpretationImpl<R> implements HqlInterpretation<R> {
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<R>) sqmStatement ).getQueryPart(), resultType );
allowedReturnTypes.put( resultType, Boolean.TRUE );
}
}
}
}

View File

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

View File

@ -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<R> extends AbstractSelectionQuery<R> {
private <T> SelectQueryPlan<T> buildConcreteQueryPlan(SqmSelectStatement<T> 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<String, String> entry : memento.getParameterTypes().entrySet() ) {
final BasicType<?> type =
basicTypeRegistry.getRegisteredType( entry.getValue() );
getParameterMetadata()
.getQueryParameter( entry.getKey() ).applyAnticipatedType( type );
}
}
}
protected TupleMetadata buildTupleMetadata(SqmStatement<?> statement, Class<R> resultType) {
if ( statement instanceof SqmSelectStatement<?> ) {
final SqmSelectStatement<?> select = (SqmSelectStatement<?>) statement;
final SqmSelectClause selectClause = select.getQueryPart().getFirstQuerySpec().getSelectClause();
final List<SqmSelection<?>> selections =
selectClause
.getSelections();
return isTupleMetadataRequired( resultType, selections.get(0) )
? getTupleMetadata( selections )
: null;
}
else {
return null;
}
}
private static <R> boolean isTupleMetadataRequired(Class<R> 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<SqmSelection<?>> 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<SqmSelection<?>> selections) {
if ( selections.size() == 1 ) {
final SqmSelectableNode<?> selectableNode = selections.get( 0).getSelectableNode();
if ( selectableNode instanceof CompoundSelection<?> ) {
final List<? extends JpaSelection<?>> 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<SqmSelection<?>> selections) {
if ( selections.size() == 1 ) {
final SqmSelectableNode<?> selectableNode = selections.get(0).getSelectableNode();
if ( selectableNode instanceof CompoundSelection<?> ) {
final List<? extends JpaSelection<?>> 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<SqmSelection<?>> selections = sqmQuerySpec.getSelectClause().getSelections();
if ( selections.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" );
}
}
}
else {
final SqmQueryGroup<?> queryGroup = (SqmQueryGroup<?>) queryPart;
for ( SqmQueryPart<?> part : queryGroup.getQueryParts() ) {
validateCriteriaQuery( part );
}
}
}
protected static <T> HqlInterpretation<T> interpretation(
NamedHqlQueryMementoImpl memento,
Class<T> expectedResultType,
SharedSessionContractImplementor session) {
final QueryEngine queryEngine = session.getFactory().getQueryEngine();
final QueryInterpretationCache interpretationCache = queryEngine.getInterpretationCache();
final HqlInterpretation<T> interpretation = interpretationCache.resolveHqlInterpretation(
memento.getHqlString(),
expectedResultType,
queryEngine.getHqlTranslator()
);
return interpretation;
}
}

View File

@ -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<R>
private final ParameterMetadataImplementor parameterMetadata;
private final DomainParameterXref domainParameterXref;
private final QueryParameterBindingsImpl parameterBindings;
private final QueryParameterBindings parameterBindings;
private final Class<R> resultType;
private final TupleMetadata tupleMetadata;
@ -156,29 +145,13 @@ public class QuerySqmImpl<R>
NamedHqlQueryMementoImpl memento,
Class<R> 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<R> 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<R>
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<R>
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<R>
bindCriteriaParameter((SqmJpaCriteriaParameterWrapper<?>) sqmParameter);
}
}
validateStatement( sqm, expectedResultType );
if ( sqm instanceof SqmSelectStatement<?> ) {
final SqmSelectStatement<R> selectStatement = (SqmSelectStatement<R>) sqm;
final SqmQueryPart<R> 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<R>
}
}
private void validateStatement(SqmStatement<R> sqmStatement, Class<R> resultType) {
if ( sqmStatement instanceof SqmSelectStatement<?> ) {
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 ) {
throw new IllegalQueryOperationException( "Result type given for a non-SELECT Query", hql, null );
}
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<?> ) {
verifyInsertTypesMatch( hql, (SqmInsertStatement<R>) sqmStatement );
}
}
}
private void verifyImmutableEntityUpdate(
String hqlString,
SqmUpdateStatement<R> 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<R> sqmStatement) {
final List<SqmAssignment<?>> 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<R> sqmStatement) {
final List<SqmPath<?>> insertionTargetPaths = sqmStatement.getInsertionTargetPaths();
if ( sqmStatement instanceof SqmInsertValuesStatement<?> ) {
final SqmInsertValuesStatement<R> statement = (SqmInsertValuesStatement<R>) sqmStatement;
for ( SqmValues sqmValues : statement.getValuesList() ) {
verifyInsertTypesMatch( hqlString, insertionTargetPaths, sqmValues.getExpressions() );
}
}
else {
final SqmInsertSelectStatement<R> statement = (SqmInsertSelectStatement<R>) sqmStatement;
final List<SqmSelectableNode<?>> selections =
statement.getSelectQueryPart()
.getFirstQuerySpec()
.getSelectClause()
.getSelectionItems();
verifyInsertTypesMatch( hqlString, insertionTargetPaths, selections );
statement.getSelectQueryPart().validateQueryStructureAndFetchOwners();
}
}
private void verifyInsertTypesMatch(
String hqlString,
List<SqmPath<?>> insertionTargetPaths,
List<? extends SqmTypedNode<?>> 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;

View File

@ -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<R> extends AbstractSqmSelectionQuery<R>
private final ParameterMetadataImplementor parameterMetadata;
private final DomainParameterXref domainParameterXref;
private final QueryParameterBindingsImpl parameterBindings;
private final QueryParameterBindings parameterBindings;
private final Class<R> expectedResultType;
private final Class<?> resultType;
@ -100,76 +97,34 @@ public class SqmSelectionQueryImpl<R> extends AbstractSqmSelectionQuery<R>
super( session );
this.hql = hql;
SqmUtil.verifyIsSelectStatement( hqlInterpretation.getSqmStatement(), hql );
this.sqm = (SqmSelectStatement<R>) 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<SqmSelection<?>> 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<R> 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<R> hqlInterpretation =
interpretationCache.resolveHqlInterpretation( hql, resultType, queryEngine.getHqlTranslator() );
SqmUtil.verifyIsSelectStatement( hqlInterpretation.getSqmStatement(), hql );
this.sqm = (SqmSelectStatement<R>) 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<R> extends AbstractSqmSelectionQuery<R>
? 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<R> extends AbstractSqmSelectionQuery<R>
}
this.expectedResultType = expectedResultType;
this.resultType = determineResultType( sqm );
visitQueryReturnType( sqm.getQueryPart(), expectedResultType, getSessionFactory() );
this.resultType = determineResultType( sqm, expectedResultType );
final SqmQueryPart<R> 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<SqmSelection<?>> 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 <T> void bindCriteriaParameter(SqmJpaCriteriaParameterWrapper<T> sqmParameter) {
final JpaCriteriaParameter<T> jpaCriteriaParameter = sqmParameter.getJpaCriteriaParameter();
final T value = jpaCriteriaParameter.getValue();

View File

@ -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<SqmSelection<?>> selections = querySpec.getSelectClause().getSelections();
if ( selections == null || selections.isEmpty() ) {
// make sure there is at least one root
final List<SqmRoot<?>> 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 <T> boolean hasMatchingConstructor(Class<T> expectedResultClass, Class<?> selectedJavaType) {
try {
expectedResultClass.getDeclaredConstructor( selectedJavaType );
return true;
}
catch (NoSuchMethodException e) {
return false;
}
}
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() );
}
}
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()
) );
}
}

View File

@ -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<E>
}
}
public abstract void validate(@Nullable String hql);
@Override
public Collection<SqmCteStatement<?>> getCteStatements() {
return cteStatements.values();

View File

@ -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<T>
return statement;
}
@Override
public void validate(@Nullable String hql) {
// No-op
}
@Override
public SqmDeleteStatement<T> where(Expression<Boolean> restriction) {
setWhere( restriction );

View File

@ -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<T> extends AbstractSqmDmlStatem
}
}
protected void verifyInsertTypesMatch(
List<SqmPath<?>> insertionTargetPaths,
List<? extends SqmTypedNode<?>> 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<T> root) {
if ( root.getModel() instanceof SqmPolymorphicRootDescriptor<?> ) {

View File

@ -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<T> extends AbstractSqmInsertStatement<T> i
);
}
@Override
public void validate(@Nullable String hql) {
final List<SqmPath<?>> insertionTargetPaths = getInsertionTargetPaths();
final List<SqmSelectableNode<?>> selections = getSelectQueryPart()
.getFirstQuerySpec()
.getSelectClause()
.getSelectionItems();
verifyInsertTypesMatch( insertionTargetPaths, selections );
getSelectQueryPart().validateQueryStructureAndFetchOwners();
}
@Override
public SqmInsertSelectStatement<T> select(CriteriaQuery<Tuple> criteriaQuery) {
final SqmSelectStatement<Tuple> selectStatement = (SqmSelectStatement<Tuple>) criteriaQuery;

View File

@ -113,6 +113,14 @@ public class SqmInsertValuesStatement<T> extends AbstractSqmInsertStatement<T> i
);
}
@Override
public void validate(@Nullable String hql) {
final List<SqmPath<?>> insertionTargetPaths = getInsertionTargetPaths();
for ( SqmValues sqmValues : getValuesList() ) {
verifyInsertTypesMatch( insertionTargetPaths, sqmValues.getExpressions() );
}
}
public List<SqmValues> getValuesList() {
return valuesList == null
? Collections.emptyList()

View File

@ -149,6 +149,10 @@ public class SqmSelectStatement<T> extends AbstractSqmSelectQuery<T> implements
return statement;
}
public void validateResultType(Class<?> resultType) {
SqmUtil.validateQueryReturnType( getQueryPart(), resultType );
}
@Override
public SqmQuerySource getQuerySource() {
return querySource;

View File

@ -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<T>
extends AbstractSqmRestrictedDmlStatement<T>
implements SqmDeleteOrUpdateStatement<T>, JpaCriteriaUpdate<T> {
private static final CoreMessageLogger LOG = CoreLogging.messageLogger( SqmUpdateStatement.class );
private boolean versioned;
private SqmSetClause setClause;
@ -110,6 +121,46 @@ public class SqmUpdateStatement<T>
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<SqmAssignment<?>> 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;
}

View File

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

View File

@ -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() {