From 0c20980be27be9fbc1ebb2e5bae211bf024dd6f2 Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Mon, 27 Feb 2023 18:06:38 -0600 Subject: [PATCH] HHH-16182 - Converted boolean values not always properly handled in predicates --- .../mapping/basic/BooleanMappingTests.java | 222 +++++++++++++++++- .../sqm/sql/BaseSqmToSqlAstConverter.java | 92 ++++---- .../AbstractNegatableSqmPredicate.java | 7 +- .../SqmBooleanExpressionPredicate.java | 13 +- 4 files changed, 272 insertions(+), 62 deletions(-) diff --git a/documentation/src/test/java/org/hibernate/userguide/mapping/basic/BooleanMappingTests.java b/documentation/src/test/java/org/hibernate/userguide/mapping/basic/BooleanMappingTests.java index eb817a76fa..68eeeca076 100644 --- a/documentation/src/test/java/org/hibernate/userguide/mapping/basic/BooleanMappingTests.java +++ b/documentation/src/test/java/org/hibernate/userguide/mapping/basic/BooleanMappingTests.java @@ -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 criteria = builder.createQuery( Long.class ); + criteria.select( builder.count( builder.literal( 1 ) ) ); + final JpaRoot root = criteria.from( EntityOfBooleans.class ); + final JpaPath 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") diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java index 50032eb788..f1c5deae3b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java @@ -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 extends Base private boolean negativeAdjustment; private final Set visitedAssociationKeys = new HashSet<>(); + private final MappingMetamodel domainModel; public BaseSqmToSqlAstConverter( SqlAstCreationContext creationContext, @@ -569,8 +570,8 @@ public abstract class BaseSqmToSqlAstConverter 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 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 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 subclassEntityNames = entityDescriptor.getSubclassEntityNames(); return createTreatTypeRestriction( lhs, subclassEntityNames ); } private Predicate createTreatTypeRestriction(SqmPath lhs, Set 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 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 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 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 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 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 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 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 diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/AbstractNegatableSqmPredicate.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/AbstractNegatableSqmPredicate.java index 23a5728860..51163c5333 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/AbstractNegatableSqmPredicate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/AbstractNegatableSqmPredicate.java @@ -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 type, boolean negated, NodeBuilder nodeBuilder) { + super( type, nodeBuilder ); this.negated = negated; } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmBooleanExpressionPredicate.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmBooleanExpressionPredicate.java index f860b896f5..aa77091c80 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmBooleanExpressionPredicate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmBooleanExpressionPredicate.java @@ -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 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 + " )"; + } + } }