diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index d140524d8bd0..632acc93be34 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java @@ -7664,14 +7664,17 @@ else if ( !dialect.supportsRowValueConstructorSyntaxInInList() ) { appendSql( CLOSE_PARENTHESIS ); } else { - String separator = NO_SEPARATOR; + if (inListPredicate.isNegated()) { + appendSql("not "); + } appendSql( OPEN_PARENTHESIS ); - for ( Expression expression : listExpressions ) { - appendSql( separator ); + String separator = NO_SEPARATOR; + for (Expression expression : listExpressions) { + appendSql(separator); emulateTupleComparison( lhsTuple.getExpressions(), - getSqlTuple( expression ).getExpressions(), - comparisonOperator, + SqlTupleContainer.getSqlTuple(expression).getExpressions(), + ComparisonOperator.EQUAL, true ); separator = " or "; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/Foo.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/Foo.java new file mode 100644 index 000000000000..60174b52b959 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/Foo.java @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.criteria.basic; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinColumns; +import jakarta.persistence.ManyToOne; + +import java.util.Objects; + +/** + * Entity used in org.hibernate.orm.test.jpa.criteria.basic.NegatedInPredicateTest. + * + * @author Mike Mannion + */ +@Entity +public class Foo { + + @Id + Long id; + + @ManyToOne + @JoinColumns({ + @JoinColumn(name = "FK_CODE", referencedColumnName = "CODE"), + @JoinColumn(name = "FK_CONTEXT", referencedColumnName = "CONTEXT") + }) + FooType fooType; + + public Foo() { + // For JPA + } + + public Foo(Long id, FooType fooType) { + this.id = id; + this.fooType = fooType; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + + Foo customer = (Foo) o; + return Objects.equals(id, customer.id) && Objects.equals(fooType, customer.fooType); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(id); + result = 31 * result + Objects.hashCode(fooType); + return result; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/FooType.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/FooType.java new file mode 100644 index 000000000000..991732ec458f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/FooType.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.criteria.basic; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +/** + * Entity used in org.hibernate.orm.test.jpa.criteria.basic.NegatedInPredicateTest. + * + * @author Mike Mannion + */ +@Entity +public class FooType { + @Id + @Column(name = "CODE") + String code; + + @Column(name = "CONTEXT") + String context; + + public FooType() { + // For JPA + } + + FooType(String code, String context) { + this.code = code; + this.context = context; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/NegatedInPredicateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/NegatedInPredicateTest.java new file mode 100644 index 000000000000..e1d4a2bc3f23 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/NegatedInPredicateTest.java @@ -0,0 +1,111 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.criteria.basic; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +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 java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Add test which composes not with in. Test introduced after discovery the negated in + * fails under dialects without record-level construction, such as DB2. + * + * @author Mike Mannion + */ +@JiraKey(value = "HHH-19497") +@DomainModel( + annotatedClasses = {Foo.class, FooType.class} +) +@SessionFactory +class NegatedInPredicateTest { + + public static final FooType FOO_TYPE1 = new FooType( "ft1", "ctx1" ); + public static final FooType FOO_TYPE2 = new FooType( "ft2", "ctx1" ); + + @BeforeEach + void setup(SessionFactoryScope scope) { + scope.inTransaction( + em -> { + em.persist( FOO_TYPE1 ); + em.persist( FOO_TYPE2 ); + + Foo foo1 = new Foo( 1L, FOO_TYPE1 ); + Foo foo2 = new Foo( 2L, FOO_TYPE1 ); + Foo foo3 = new Foo( 3L, FOO_TYPE2 ); + + em.persist( foo1 ); + em.persist( foo2 ); + em.persist( foo3 ); + } + ); + } + + @AfterEach + void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + em -> { + em.createQuery( "delete from Foo" ).executeUpdate(); + em.createQuery( "delete from FooType" ).executeUpdate(); + } + ); + } + + @Test + void testSanity(SessionFactoryScope scope) { + scope.inTransaction( + em -> { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery( Foo.class ); + Root root = cq.from( Foo.class ); + assertThat( em.createQuery( cq.select( root ) ).getResultList() ) + .hasSize( 3 ); + } + ); + } + + @Test + void testNegatedPredicate(SessionFactoryScope scope) { + scope.inTransaction( + em -> { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery( Foo.class ); + Root root = cq.from( Foo.class ); + cq.select( root ) + .where( cb.not( root.get( "fooType" ).in( List.of( FOO_TYPE1, FOO_TYPE2 ) ) ) ); + assertThat( em.createQuery( cq ).getResultList() ) + .isEmpty(); + } + ); + } + + @Test + void testNonNegatedInPredicate(SessionFactoryScope scope) { + scope.inTransaction( + em -> { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery( Foo.class ); + Root root = cq.from( Foo.class ); + cq.select( root ) + .where( root.get( "fooType" ).in( List.of( FOO_TYPE1, FOO_TYPE2 ) ) ); + assertThat( em.createQuery( cq ).getResultList() ) + .hasSize( 3 ); + + } + ); + } + +}