HHH-16182 - Converted boolean values not always properly handled in predicates

This commit is contained in:
Steve Ebersole 2023-02-27 18:06:38 -06:00
parent 51ef9f494b
commit 0c20980be2
4 changed files with 272 additions and 62 deletions

View File

@ -7,17 +7,25 @@
package org.hibernate.userguide.mapping.basic;
import java.sql.Types;
import java.util.List;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.metamodel.mapping.JdbcMapping;
import org.hibernate.metamodel.mapping.internal.BasicAttributeMapping;
import org.hibernate.metamodel.spi.MappingMetamodelImplementor;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.query.criteria.HibernateCriteriaBuilder;
import org.hibernate.query.criteria.JpaCriteriaQuery;
import org.hibernate.query.criteria.JpaPath;
import org.hibernate.query.criteria.JpaRoot;
import org.hibernate.type.internal.ConvertedBasicTypeImpl;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.Jira;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import jakarta.persistence.Basic;
@ -28,6 +36,7 @@ import jakarta.persistence.Table;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.isOneOf;
@ -97,22 +106,211 @@ public class BooleanMappingTests {
}
}
@BeforeEach
public void createTestData(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final EntityOfBooleans entity = new EntityOfBooleans();
entity.id = 1;
assert !entity.convertedYesNo;
assert !entity.convertedTrueFalse;
assert !entity.convertedNumeric;
session.persist( entity );
} );
}
@AfterEach
public void dropTestData(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
session.createMutationQuery( "delete EntityOfBooleans" ).executeUpdate();
} );
}
@Test
@Jira( "https://hibernate.atlassian.net/browse/HHH-16182" )
public void testQueryLiteralUsage(SessionFactoryScope scope) {
public void testComparisonLiteralHandling(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
session.createSelectionQuery( "from EntityOfBooleans where convertedYesNo = true" ).list();
session.createSelectionQuery( "from EntityOfBooleans where convertedTrueFalse = true" ).list();
session.createSelectionQuery( "from EntityOfBooleans where convertedNumeric = true" ).list();
session.createMutationQuery( "delete EntityOfBooleans where convertedYesNo = true" ).executeUpdate();
session.createMutationQuery( "delete EntityOfBooleans where convertedTrueFalse = true" ).executeUpdate();
session.createMutationQuery( "delete EntityOfBooleans where convertedNumeric = true" ).executeUpdate();
session.createMutationQuery( "update EntityOfBooleans set convertedYesNo = true" ).executeUpdate();
session.createMutationQuery( "update EntityOfBooleans set convertedTrueFalse = true" ).executeUpdate();
session.createMutationQuery( "update EntityOfBooleans set convertedNumeric = true" ).executeUpdate();
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where convertedYesNo = true" ).list(),
hasSize( 0 )
);
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where convertedTrueFalse = true" ).list(),
hasSize( 0 )
);
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where convertedNumeric = true" ).list(),
hasSize( 0 )
);
} );
scope.inTransaction( (session) -> {
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where convertedYesNo = false" ).list(),
hasSize( 1 )
);
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where convertedTrueFalse = false" ).list(),
hasSize( 1 )
);
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where convertedNumeric = false" ).list(),
hasSize( 1 )
);
} );
scope.inTransaction( (session) -> {
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where convertedYesNo != true" ).list(),
hasSize( 1 )
);
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where convertedTrueFalse != true" ).list(),
hasSize( 1 )
);
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where convertedNumeric != true" ).list(),
hasSize( 1 )
);
} );
}
@Test
@Jira( "https://hibernate.atlassian.net/browse/HHH-16182" )
public void testExpressionAsPredicateUsage(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where convertedYesNo" ).list(),
hasSize( 0 )
);
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where convertedTrueFalse" ).list(),
hasSize( 0 )
);
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where convertedNumeric" ).list(),
hasSize( 0 )
);
} );
scope.inTransaction( (session) -> {
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where (convertedYesNo)" ).list(),
hasSize( 0 )
);
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where (convertedTrueFalse)" ).list(),
hasSize( 0 )
);
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where (convertedNumeric)" ).list(),
hasSize( 0 )
);
} );
}
@Test
@Jira( "https://hibernate.atlassian.net/browse/HHH-16182" )
public void testNegatedExpressionAsPredicateUsage(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where not convertedYesNo" ).list(),
hasSize( 1 )
);
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where not convertedTrueFalse" ).list(),
hasSize( 1 )
);
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where not convertedNumeric" ).list(),
hasSize( 1 )
);
} );
scope.inTransaction( (session) -> {
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where not (convertedYesNo)" ).list(),
hasSize( 1 )
);
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where not (convertedTrueFalse)" ).list(),
hasSize( 1 )
);
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where not (convertedNumeric)" ).list(),
hasSize( 1 )
);
} );
}
@Test
@Jira( "https://hibernate.atlassian.net/browse/HHH-16182" )
public void testSetClauseUsage(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
assertThat(
session.createMutationQuery( "update EntityOfBooleans set convertedYesNo = true" ).executeUpdate(),
equalTo( 1 )
);
assertThat(
session.createMutationQuery( "update EntityOfBooleans set convertedTrueFalse = true" ).executeUpdate(),
equalTo( 1 )
);
assertThat(
session.createMutationQuery( "update EntityOfBooleans set convertedNumeric = true" ).executeUpdate(),
equalTo( 1 )
);
} );
scope.inTransaction( (session) -> {
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where convertedYesNo" ).list(),
hasSize( 1 )
);
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where convertedTrueFalse" ).list(),
hasSize( 1 )
);
assertThat(
session.createSelectionQuery( "from EntityOfBooleans where convertedNumeric" ).list(),
hasSize( 1 )
);
} );
}
@Test
@Jira( "https://hibernate.atlassian.net/browse/HHH-16182" )
public void testCriteriaUsage(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
assertThat( countByCriteria( "convertedYesNo", true, session ), equalTo( 0 ) );
assertThat( countByCriteria( "convertedTrueFalse", true, session ), equalTo( 0 ) );
assertThat( countByCriteria( "convertedNumeric", true, session ), equalTo( 0 ) );
} );
}
@Test
@Jira( "https://hibernate.atlassian.net/browse/HHH-16182" )
public void testNegatedCriteriaUsage(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
assertThat( countByCriteria( "convertedYesNo", false, session ), equalTo( 1 ) );
assertThat( countByCriteria( "convertedTrueFalse", false, session ), equalTo( 1 ) );
assertThat( countByCriteria( "convertedNumeric", false, session ), equalTo( 1 ) );
} );
}
private int countByCriteria(String attributeName, boolean matchValue, SessionImplementor session) {
final HibernateCriteriaBuilder builder = session.getCriteriaBuilder();
final JpaCriteriaQuery<Long> criteria = builder.createQuery( Long.class );
criteria.select( builder.count( builder.literal( 1 ) ) );
final JpaRoot<EntityOfBooleans> root = criteria.from( EntityOfBooleans.class );
final JpaPath<Boolean> convertedYesNo = root.get( attributeName );
if ( matchValue ) {
criteria.where( convertedYesNo );
}
else {
criteria.where( builder.not( convertedYesNo ) );
}
final Long result = session.createQuery( criteria ).uniqueResult();
return result.intValue();
}
@Entity(name = "EntityOfBooleans")

View File

@ -88,16 +88,12 @@ import org.hibernate.metamodel.mapping.internal.OneToManyCollectionPart;
import org.hibernate.metamodel.mapping.internal.SqlTypedMappingImpl;
import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping;
import org.hibernate.metamodel.mapping.ordering.OrderByFragment;
import org.hibernate.type.descriptor.converter.internal.OrdinalEnumValueConverter;
import org.hibernate.type.descriptor.converter.spi.BasicValueConverter;
import org.hibernate.metamodel.model.domain.BasicDomainType;
import org.hibernate.metamodel.model.domain.EmbeddableDomainType;
import org.hibernate.metamodel.model.domain.EntityDomainType;
import org.hibernate.metamodel.model.domain.PluralPersistentAttribute;
import org.hibernate.metamodel.model.domain.internal.AnyDiscriminatorSqmPath;
import org.hibernate.metamodel.model.domain.internal.AnyDiscriminatorSqmPathSource;
import org.hibernate.query.derived.AnonymousTupleTableGroupProducer;
import org.hibernate.query.derived.AnonymousTupleType;
import org.hibernate.metamodel.model.domain.internal.BasicSqmPathSource;
import org.hibernate.metamodel.model.domain.internal.CompositeSqmPathSource;
import org.hibernate.metamodel.model.domain.internal.DiscriminatorSqmPath;
@ -115,6 +111,8 @@ import org.hibernate.query.criteria.JpaCteCriteriaAttribute;
import org.hibernate.query.criteria.JpaPath;
import org.hibernate.query.criteria.JpaSearchOrder;
import org.hibernate.query.derived.AnonymousTupleEntityValuedModelPart;
import org.hibernate.query.derived.AnonymousTupleTableGroupProducer;
import org.hibernate.query.derived.AnonymousTupleType;
import org.hibernate.query.spi.QueryEngine;
import org.hibernate.query.spi.QueryOptions;
import org.hibernate.query.spi.QueryParameterBinding;
@ -392,6 +390,8 @@ import org.hibernate.type.CustomType;
import org.hibernate.type.EnumType;
import org.hibernate.type.JavaObjectType;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.descriptor.converter.internal.OrdinalEnumValueConverter;
import org.hibernate.type.descriptor.converter.spi.BasicValueConverter;
import org.hibernate.type.descriptor.java.EnumJavaType;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.java.TemporalJavaType;
@ -501,6 +501,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
private boolean negativeAdjustment;
private final Set<AssociationKey> visitedAssociationKeys = new HashSet<>();
private final MappingMetamodel domainModel;
public BaseSqmToSqlAstConverter(
SqlAstCreationContext creationContext,
@ -569,8 +570,8 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
this.loadQueryInfluencers = loadQueryInfluencers;
this.domainParameterXref = domainParameterXref;
this.domainParameterBindings = domainParameterBindings;
this.jpaCriteriaParamResolutions = domainParameterXref.getParameterResolutions()
.getJpaCriteriaParamResolutions();
this.jpaCriteriaParamResolutions = domainParameterXref.getParameterResolutions().getJpaCriteriaParamResolutions();
this.domainModel = creationContext.getSessionFactory().getRuntimeMetamodels().getMappingMetamodel();
}
private static Boolean stackMatchHelper(SqlAstProcessingState processingState, SqlAstProcessingState c) {
@ -4813,14 +4814,11 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
// A case wrapper for non-basic paths is not possible,
// because a case expression must return a scalar value,
// so we instead add the type restriction predicate as conjunct
final MappingMetamodel domainModel = creationContext.getSessionFactory()
.getRuntimeMetamodels()
.getMappingMetamodel();
final EntityPersister entityDescriptor = domainModel.findEntityDescriptor(
treatedPath.getTreatTarget().getHibernateEntityName()
);
conjunctTreatUsages.computeIfAbsent( wrappedPath, p -> new HashSet<>( 1 ) )
.addAll( entityDescriptor.getSubclassEntityNames() );
final String treatedName = treatedPath.getTreatTarget().getHibernateEntityName();
final EntityPersister entityDescriptor = domainModel.findEntityDescriptor( treatedName );
conjunctTreatUsages
.computeIfAbsent( wrappedPath, p -> new HashSet<>( 1 ) )
.addAll( entityDescriptor.getSubclassEntityNames() );
return expression;
}
if ( wrappedPath instanceof DiscriminatorSqmPath ) {
@ -4869,18 +4867,12 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
}
private Predicate createTreatTypeRestriction(SqmPath<?> lhs, EntityDomainType<?> treatTarget) {
final MappingMetamodel domainModel = creationContext.getSessionFactory()
.getRuntimeMetamodels()
.getMappingMetamodel();
final EntityPersister entityDescriptor = domainModel.findEntityDescriptor( treatTarget.getHibernateEntityName() );
final Set<String> subclassEntityNames = entityDescriptor.getSubclassEntityNames();
return createTreatTypeRestriction( lhs, subclassEntityNames );
}
private Predicate createTreatTypeRestriction(SqmPath<?> lhs, Set<String> subclassEntityNames) {
final MappingMetamodel domainModel = creationContext.getSessionFactory()
.getRuntimeMetamodels()
.getMappingMetamodel();
// Do what visitSelfInterpretingSqmPath does, except for calling preparingReusablePath
// as that would register a type usage for the table group that we don't want here
final DiscriminatorSqmPath discriminatorSqmPath = (DiscriminatorSqmPath) lhs.type();
@ -5264,25 +5256,31 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
if ( sqmExpression instanceof SqmParameter ) {
return determineValueMapping( (SqmParameter<?>) sqmExpression );
}
else if ( sqmExpression instanceof SqmPath ) {
if ( sqmExpression instanceof SqmPath ) {
log.debugf( "Determining mapping-model type for SqmPath : %s ", sqmExpression );
final MappingMetamodel domainModel = creationContext.getSessionFactory()
.getRuntimeMetamodels()
.getMappingMetamodel();
return SqmMappingModelHelper.resolveMappingModelExpressible(
sqmExpression,
domainModel,
fromClauseIndex::findTableGroup
);
}
if ( sqmExpression instanceof SqmBooleanExpressionPredicate ) {
final SqmBooleanExpressionPredicate expressionPredicate = (SqmBooleanExpressionPredicate) sqmExpression;
return determineValueMapping( expressionPredicate.getBooleanExpression(), fromClauseIndex );
}
// The model type of an enum literal is always inferred
else if ( sqmExpression instanceof SqmEnumLiteral<?> ) {
if ( sqmExpression instanceof SqmEnumLiteral<?> ) {
final MappingModelExpressible<?> mappingModelExpressible = resolveInferredType();
if ( mappingModelExpressible != null ) {
return mappingModelExpressible;
}
}
else if ( sqmExpression instanceof SqmSubQuery<?> ) {
if ( sqmExpression instanceof SqmSubQuery<?> ) {
final SqmSubQuery<?> subQuery = (SqmSubQuery<?>) sqmExpression;
final SqmSelectClause selectClause = subQuery.getQuerySpec().getSelectClause();
if ( selectClause.getSelections().size() == 1 ) {
@ -5293,9 +5291,6 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
}
final SqmExpressible<?> selectionNodeType = subQuerySelection.getNodeType();
if ( selectionNodeType != null ) {
final MappingMetamodel domainModel = creationContext.getSessionFactory()
.getRuntimeMetamodels()
.getMappingMetamodel();
final MappingModelExpressible<?> expressible = domainModel.resolveMappingExpressible(selectionNodeType, this::findTableGroupByPath );
if ( expressible != null ) {
@ -5321,6 +5316,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
// We can't determine the type of the expression
return null;
}
if ( nodeType instanceof EmbeddedSqmPathSource<?> ) {
if ( sqmExpression instanceof SqmBinaryArithmetic<?> ) {
final SqmBinaryArithmetic<?> binaryArithmetic = (SqmBinaryArithmetic<?>) sqmExpression;
@ -5332,9 +5328,8 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
}
}
}
final MappingMetamodel domainModel = creationContext.getSessionFactory()
.getRuntimeMetamodels()
.getMappingMetamodel();
final MappingModelExpressible<?> valueMapping = domainModel.resolveMappingExpressible(
nodeType,
fromClauseIndex::getTableGroup
@ -5348,7 +5343,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
}
if ( valueMapping == null ) {
// For literals it is totally possible that we can't figure out a mapping type
// For literals, it is totally possible that we can't figure out a mapping type
if ( sqmExpression instanceof SqmLiteral<?> ) {
return null;
}
@ -6785,10 +6780,10 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
finally {
inferrableTypeAccessStack.pop();
}
ComparisonOperator sqmOperator = predicate.getSqmOperator();
if ( predicate.isNegated() ) {
sqmOperator = sqmOperator.negated();
}
final ComparisonOperator sqmOperator = predicate.isNegated()
? predicate.getSqmOperator().negated()
: predicate.getSqmOperator();
return new ComparisonPredicate( lhs, sqmOperator, rhs, getBooleanType() );
}
@ -7099,20 +7094,21 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
@Override
public Object visitBooleanExpressionPredicate(SqmBooleanExpressionPredicate predicate) {
final Expression booleanExpression = (Expression) predicate.getBooleanExpression().accept( this );
if ( booleanExpression instanceof SelfRenderingExpression ) {
final Predicate sqlPredicate = new SelfRenderingPredicate( (SelfRenderingExpression) booleanExpression );
if ( predicate.isNegated() ) {
return new NegatedPredicate( sqlPredicate );
}
return sqlPredicate;
}
else {
return new BooleanExpressionPredicate(
final JdbcMapping jdbcMapping = booleanExpression.getExpressionType().getJdbcMapping( 0 );
if ( jdbcMapping.getValueConverter() != null ) {
// handle converted booleans (yes-no, etc)
return new ComparisonPredicate(
booleanExpression,
predicate.isNegated(),
getBooleanType()
ComparisonOperator.EQUAL,
new JdbcLiteral<>( jdbcMapping.convertToRelationalValue( !predicate.isNegated() ), jdbcMapping )
);
}
return new BooleanExpressionPredicate(
booleanExpression,
predicate.isNegated(),
getBooleanType()
);
}
@Override

View File

@ -7,6 +7,7 @@
package org.hibernate.query.sqm.tree.predicate;
import org.hibernate.query.sqm.NodeBuilder;
import org.hibernate.query.sqm.SqmExpressible;
/**
* @author Steve Ebersole
@ -19,7 +20,11 @@ public abstract class AbstractNegatableSqmPredicate extends AbstractSqmPredicate
}
public AbstractNegatableSqmPredicate(boolean negated, NodeBuilder nodeBuilder) {
super( nodeBuilder.getBooleanType(), nodeBuilder );
this( nodeBuilder.getBooleanType(), negated, nodeBuilder );
}
public AbstractNegatableSqmPredicate(SqmExpressible<Boolean> type, boolean negated, NodeBuilder nodeBuilder) {
super( type, nodeBuilder );
this.negated = negated;
}

View File

@ -18,6 +18,7 @@ import jakarta.persistence.criteria.Expression;
/**
* Represents an expression whose type is boolean, and can therefore be used as a predicate.
* E.g. {@code `from Employee e where e.isActive`}
*
* @author Steve Ebersole
*/
@ -34,7 +35,7 @@ public class SqmBooleanExpressionPredicate extends AbstractNegatableSqmPredicate
SqmExpression<Boolean> booleanExpression,
boolean negated,
NodeBuilder nodeBuilder) {
super( negated, nodeBuilder );
super( booleanExpression.getExpressible(), negated, nodeBuilder );
assert booleanExpression.getNodeType() != null;
final Class<?> expressionJavaType = booleanExpression.getNodeType().getExpressibleJavaType().getJavaTypeClass();
@ -86,4 +87,14 @@ public class SqmBooleanExpressionPredicate extends AbstractNegatableSqmPredicate
protected SqmNegatablePredicate createNegatedNode() {
return new SqmBooleanExpressionPredicate( booleanExpression, !isNegated(), nodeBuilder() );
}
@Override
public String toString() {
if ( isNegated() ) {
return "SqmBooleanExpressionPredicate( (not) " + booleanExpression + " )";
}
else {
return "SqmBooleanExpressionPredicate( " + booleanExpression + " )";
}
}
}