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 );
+
+ }
+ );
+ }
+
+}