Repository: jakartaee/data
Pull request #905 (2024-12-05 23:02:32)

negated restrictions
If I remember the discussion correctly, the 3 ideas for how to do negation of restrictions were:

- restriction.negate()
- Restrict.not(restriction)
- Restrict.notAll(restrictions) / Restrict.notAny(restrictions)

notAll/notAny seemed the most awkward to me, and I realized that the first two are basically the same thing, and it would be trivial to have both ways, where Restrict.not(restriction) is a convenience method that invokes restriction.negate().


Patch:
diff --git a/api/src/main/java/jakarta/data/BasicRestriction.java b/api/src/main/java/jakarta/data/BasicRestriction.java
index f7c3b167..0cc17338 100644
--- a/api/src/main/java/jakarta/data/BasicRestriction.java
+++ b/api/src/main/java/jakarta/data/BasicRestriction.java
@@ -22,5 +22,8 @@ public interface BasicRestriction<T> extends Restriction<T> {
 
     String field();
 
+    @Override
+    BasicRestriction<T> negate();
+
     Object value();
 }
diff --git a/api/src/main/java/jakarta/data/BasicRestrictionRecord.java b/api/src/main/java/jakarta/data/BasicRestrictionRecord.java
index e58ca18f..dfedc845 100644
--- a/api/src/main/java/jakarta/data/BasicRestrictionRecord.java
+++ b/api/src/main/java/jakarta/data/BasicRestrictionRecord.java
@@ -25,7 +25,6 @@ import java.util.Objects;
 
 record BasicRestrictionRecord<T>(
         String field,
-        boolean isNegated,
         Operator comparison,
         Object value) implements BasicRestriction<T> {
 
@@ -33,7 +32,11 @@ record BasicRestrictionRecord<T>(
         Objects.requireNonNull(field, "Field must not be null");
     }
 
-    BasicRestrictionRecord(String field, Operator comparison, Object value) {
-        this(field, false, comparison, value);
+    @Override
+    public BasicRestriction<T> negate() {
+        return new BasicRestrictionRecord<>(
+                field,
+                comparison.negate(),
+                value);
     }
 }
diff --git a/api/src/main/java/jakarta/data/CompositeRestriction.java b/api/src/main/java/jakarta/data/CompositeRestriction.java
index 1eeffbf9..5c5a8ab6 100644
--- a/api/src/main/java/jakarta/data/CompositeRestriction.java
+++ b/api/src/main/java/jakarta/data/CompositeRestriction.java
@@ -20,6 +20,11 @@ package jakarta.data;
 import java.util.List;
 
 public interface CompositeRestriction<T> extends Restriction<T> {
+    boolean isNegated();
+
+    @Override
+    CompositeRestriction<T> negate();
+
     List<Restriction<T>> restrictions();
 
     Type type();
diff --git a/api/src/main/java/jakarta/data/CompositeRestrictionRecord.java b/api/src/main/java/jakarta/data/CompositeRestrictionRecord.java
index a8cd5e0f..40b8d612 100644
--- a/api/src/main/java/jakarta/data/CompositeRestrictionRecord.java
+++ b/api/src/main/java/jakarta/data/CompositeRestrictionRecord.java
@@ -38,4 +38,9 @@ record CompositeRestrictionRecord<T>(
     CompositeRestrictionRecord(Type type, List<Restriction<T>> restrictions) {
         this(type, restrictions, false);
     }
+
+    @Override
+    public CompositeRestriction<T> negate() {
+        return new CompositeRestrictionRecord<>(type, restrictions, !isNegated);
+    }
 }
\ No newline at end of file
diff --git a/api/src/main/java/jakarta/data/Operator.java b/api/src/main/java/jakarta/data/Operator.java
index a8d448c8..003357ef 100644
--- a/api/src/main/java/jakarta/data/Operator.java
+++ b/api/src/main/java/jakarta/data/Operator.java
@@ -24,5 +24,28 @@ public enum Operator {
     IN,
     LESS_THAN,
     LESS_THAN_EQUAL,
-    LIKE
+    LIKE,
+    NOT_EQUAL,
+    NOT_IN,
+    NOT_LIKE;
+
+    /**
+     * Returns the operator that is the negation of this operator.
+     *
+     * @return the operator that is the negation of this operator.
+     */
+    Operator negate() {
+        return switch (this) {
+            case EQUAL -> NOT_EQUAL;
+            case GREATER_THAN -> LESS_THAN_EQUAL;
+            case GREATER_THAN_EQUAL -> LESS_THAN;
+            case IN -> NOT_IN;
+            case LESS_THAN -> GREATER_THAN_EQUAL;
+            case LESS_THAN_EQUAL -> GREATER_THAN;
+            case LIKE -> NOT_LIKE;
+            case NOT_EQUAL -> EQUAL;
+            case NOT_IN -> IN;
+            case NOT_LIKE -> LIKE;
+        };
+    }
 }
diff --git a/api/src/main/java/jakarta/data/Restrict.java b/api/src/main/java/jakarta/data/Restrict.java
index 96b24d8e..c9e402ad 100644
--- a/api/src/main/java/jakarta/data/Restrict.java
+++ b/api/src/main/java/jakarta/data/Restrict.java
@@ -18,6 +18,7 @@
 package jakarta.data;
 
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 
 // TODO document
@@ -31,7 +32,6 @@ public class Restrict {
 
     // used internally for more readable code
     private static final boolean ESCAPED = true;
-    private static final boolean NOT = true;
 
     private static final char STRING_WILDCARD = '%';
 
@@ -132,30 +132,36 @@ public class Restrict {
         return new TextRestrictionRecord<>(field, Operator.LIKE, ESCAPED, p);
     }
 
+    // convenience method for those who would prefer to avoid .negate()
+    public static <T> Restriction<T> not(Restriction<T> restriction) {
+        Objects.requireNonNull(restriction, "Restriction must not be null");
+        return restriction.negate();
+    }
+
     public static <T> Restriction<T> notEqualTo(Object value, String field) {
-        return new BasicRestrictionRecord<>(field, NOT, Operator.EQUAL, value);
+        return new BasicRestrictionRecord<>(field, Operator.NOT_EQUAL, value);
     }
 
     public static <T> TextRestriction<T> notEqualTo(String value, String field) {
-        return new TextRestrictionRecord<>(field, NOT, Operator.EQUAL, value);
+        return new TextRestrictionRecord<>(field, Operator.NOT_EQUAL, value);
     }
 
     public static <T> TextRestriction<T> notContains(String substring, String field) {
         String pattern = toLikeEscaped(CHAR_WILDCARD, STRING_WILDCARD, true, substring, true);
-        return new TextRestrictionRecord<>(field, NOT, Operator.LIKE, ESCAPED, pattern);
+        return new TextRestrictionRecord<>(field, Operator.NOT_LIKE, ESCAPED, pattern);
     }
 
     public static <T> TextRestriction<T> notEndsWith(String suffix, String field) {
         String pattern = toLikeEscaped(CHAR_WILDCARD, STRING_WILDCARD, true, suffix, false);
-        return new TextRestrictionRecord<>(field, NOT, Operator.LIKE, ESCAPED, pattern);
+        return new TextRestrictionRecord<>(field, Operator.NOT_LIKE, ESCAPED, pattern);
     }
 
     public static <T> Restriction<T> notIn(Set<Object> values, String field) {
-        return new BasicRestrictionRecord<>(field, NOT, Operator.IN, values);
+        return new BasicRestrictionRecord<>(field, Operator.NOT_IN, values);
     }
 
     public static <T> TextRestriction<T> notLike(String pattern, String field) {
-        return new TextRestrictionRecord<>(field, NOT, Operator.LIKE, pattern);
+        return new TextRestrictionRecord<>(field, Operator.NOT_LIKE, pattern);
     }
 
     public static <T> TextRestriction<T> notLike(String pattern,
@@ -163,12 +169,12 @@ public class Restrict {
                                                   char stringWildcard,
                                                   String field) {
         String p = toLikeEscaped(charWildcard, stringWildcard, false, pattern, false);
-        return new TextRestrictionRecord<>(field, NOT, Operator.LIKE, ESCAPED, p);
+        return new TextRestrictionRecord<>(field, Operator.NOT_LIKE, ESCAPED, p);
     }
 
     public static <T> TextRestriction<T> notStartsWith(String prefix, String field) {
         String pattern = toLikeEscaped(CHAR_WILDCARD, STRING_WILDCARD, false, prefix, true);
-        return new TextRestrictionRecord<>(field, NOT, Operator.LIKE, ESCAPED, pattern);
+        return new TextRestrictionRecord<>(field, Operator.NOT_LIKE, ESCAPED, pattern);
     }
 
     public static <T> TextRestriction<T> startsWith(String prefix, String field) {
diff --git a/api/src/main/java/jakarta/data/Restriction.java b/api/src/main/java/jakarta/data/Restriction.java
index 0548fb4f..91504c71 100644
--- a/api/src/main/java/jakarta/data/Restriction.java
+++ b/api/src/main/java/jakarta/data/Restriction.java
@@ -18,5 +18,5 @@
 package jakarta.data;
 
 public interface Restriction<T> {
-    boolean isNegated();
+    Restriction<T> negate();
 }
diff --git a/api/src/main/java/jakarta/data/TextRestriction.java b/api/src/main/java/jakarta/data/TextRestriction.java
index b6c9850c..d1a44fa8 100644
--- a/api/src/main/java/jakarta/data/TextRestriction.java
+++ b/api/src/main/java/jakarta/data/TextRestriction.java
@@ -18,7 +18,7 @@
 package jakarta.data;
 
 public interface TextRestriction<T> extends BasicRestriction<T> {
-    Restriction<T> ignoreCase();
+    TextRestriction<T> ignoreCase();
 
     // TODO can mention in the JavaDoc that a value of true will be ignored
     // if the database is not not capable of case sensitive comparisons
@@ -26,6 +26,9 @@ public interface TextRestriction<T> extends BasicRestriction<T> {
 
     boolean isEscaped();
 
+    @Override
+    TextRestriction<T> negate();
+
     @Override
     String value();
 }
diff --git a/api/src/main/java/jakarta/data/TextRestrictionRecord.java b/api/src/main/java/jakarta/data/TextRestrictionRecord.java
index 831fec41..c957860e 100644
--- a/api/src/main/java/jakarta/data/TextRestrictionRecord.java
+++ b/api/src/main/java/jakarta/data/TextRestrictionRecord.java
@@ -25,7 +25,6 @@ import java.util.Objects;
 
 record TextRestrictionRecord<T>(
         String field,
-        boolean isNegated,
         Operator comparison,
         boolean isCaseSensitive,
         boolean isEscaped,
@@ -35,24 +34,27 @@ record TextRestrictionRecord<T>(
         Objects.requireNonNull(field, "Field must not be null");
     }
 
-    TextRestrictionRecord(String field, boolean negated, Operator comparison, boolean escaped, String value) {
-        this(field, negated, comparison, true, escaped, value);
-    }
-
-    TextRestrictionRecord(String field, boolean negated, Operator comparison, String value) {
-        this(field, negated, comparison, true, false, value);
-    }
-
     TextRestrictionRecord(String field, Operator comparison, boolean escaped, String value) {
-        this(field, false, comparison, true, escaped, value);
+        this(field, comparison, true, escaped, value);
     }
 
     TextRestrictionRecord(String field, Operator comparison, String value) {
-        this(field, false, comparison, true, false, value);
+        this(field, comparison, true, false, value);
+    }
+
+    @Override
+    public TextRestriction<T> ignoreCase() {
+        return new TextRestrictionRecord<>(field, comparison, false, isEscaped, value);
     }
 
     @Override
-    public Restriction<T> ignoreCase() {
-        return new TextRestrictionRecord<>(field, isNegated, comparison, false, isEscaped, value);
+    public TextRestriction<T> negate() {
+
+        return new TextRestrictionRecord<>(
+                field,
+                comparison.negate(),
+                isCaseSensitive,
+                isEscaped,
+                value);
     }
 }


Test patch:
diff --git a/api/src/test/java/jakarta/data/BasicRestrictionRecordTest.java b/api/src/test/java/jakarta/data/BasicRestrictionRecordTest.java
index c46bfb0d..e0c3c2c1 100644
--- a/api/src/test/java/jakarta/data/BasicRestrictionRecordTest.java
+++ b/api/src/test/java/jakarta/data/BasicRestrictionRecordTest.java
@@ -24,6 +24,9 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
 
 
 class BasicRestrictionRecordTest {
+    // A mock entity class for tests
+    static class Book {
+    }
 
     @Test
     void shouldCreateBasicRestrictionWithDefaultNegation() {
@@ -31,7 +34,6 @@ class BasicRestrictionRecordTest {
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.field()).isEqualTo("title");
-            soft.assertThat(restriction.isNegated()).isFalse();
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.EQUAL);
             soft.assertThat(restriction.value()).isEqualTo("Java Guide");
         });
@@ -39,12 +41,13 @@ class BasicRestrictionRecordTest {
 
     @Test
     void shouldCreateBasicRestrictionWithExplicitNegation() {
-        BasicRestrictionRecord<String> restriction = new BasicRestrictionRecord<>("title", true, Operator.EQUAL, "Java Guide");
+        BasicRestriction<Book> restriction =
+                (BasicRestriction<Book>) Restrict.<Book>equalTo("Java Guide", "title")
+                        .negate();
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.field()).isEqualTo("title");
-            soft.assertThat(restriction.isNegated()).isTrue();
-            soft.assertThat(restriction.comparison()).isEqualTo(Operator.EQUAL);
+            soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_EQUAL);
             soft.assertThat(restriction.value()).isEqualTo("Java Guide");
         });
     }
@@ -56,21 +59,63 @@ class BasicRestrictionRecordTest {
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.field()).isEqualTo("title");
-            soft.assertThat(restriction.isNegated()).isFalse();
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.EQUAL);
             soft.assertThat(restriction.value()).isNull();
         });
     }
 
+    @Test
+    void shouldNegateLTERestriction() {
+        Restriction<Book> numChaptersLTE10 = Restrict.lessThanEqual(10, "numChapters");
+        BasicRestriction<Book> numChaptersLTE10Basic = (BasicRestriction<Book>) numChaptersLTE10;
+        BasicRestriction<Book> numChaptersGT10Basic = (BasicRestriction<Book>) numChaptersLTE10Basic.negate();
+
+        SoftAssertions.assertSoftly(soft -> {
+            soft.assertThat(numChaptersLTE10Basic.comparison()).isEqualTo(Operator.LESS_THAN_EQUAL);
+            soft.assertThat(numChaptersLTE10Basic.value()).isEqualTo(10);
+
+            soft.assertThat(numChaptersGT10Basic.comparison()).isEqualTo(Operator.GREATER_THAN);
+            soft.assertThat(numChaptersGT10Basic.value()).isEqualTo(10);
+        });
+    }
+
+    @Test
+    void shouldNegateNegatedRestriction() {
+        Restriction<Book> titleRestriction =
+                Restrict.equalTo("A Developer's Guide to Jakarta Data", "title");
+        BasicRestriction<Book> titleRestrictionBasic =
+                (BasicRestriction<Book>) titleRestriction;
+        BasicRestriction<Book> negatedTitleRestrictionBasic =
+                (BasicRestriction<Book>) titleRestriction.negate();
+        BasicRestriction<Book> negatedNegatedTitleRestrictionBasic =
+                (BasicRestriction<Book>) negatedTitleRestrictionBasic.negate();
+
+        SoftAssertions.assertSoftly(soft -> {
+            soft.assertThat(titleRestrictionBasic.comparison())
+                .isEqualTo(Operator.EQUAL);
+            soft.assertThat(titleRestrictionBasic.value())
+                .isEqualTo("A Developer's Guide to Jakarta Data");
+
+            soft.assertThat(negatedTitleRestrictionBasic.comparison())
+                .isEqualTo(Operator.NOT_EQUAL);
+            soft.assertThat(negatedTitleRestrictionBasic.value())
+                .isEqualTo("A Developer's Guide to Jakarta Data");
+
+            soft.assertThat(negatedNegatedTitleRestrictionBasic.comparison())
+                .isEqualTo(Operator.EQUAL);
+            soft.assertThat(negatedNegatedTitleRestrictionBasic.value())
+                .isEqualTo("A Developer's Guide to Jakarta Data");
+        });
+    }
+
     @Test
     void shouldSupportNegatedRestrictionUsingDefaultConstructor() {
-        BasicRestrictionRecord<String> restriction = new BasicRestrictionRecord<>("author", Operator.EQUAL, "Unknown");
-        BasicRestrictionRecord<String> negatedRestriction = new BasicRestrictionRecord<>(restriction.field(), true, restriction.comparison(), restriction.value());
+        BasicRestriction<Book> negatedRestriction =
+                (BasicRestriction<Book>) Restrict.<Book>notEqualTo((Object) "Unknown", "author");
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(negatedRestriction.field()).isEqualTo("author");
-            soft.assertThat(negatedRestriction.isNegated()).isTrue();
-            soft.assertThat(negatedRestriction.comparison()).isEqualTo(Operator.EQUAL);
+            soft.assertThat(negatedRestriction.comparison()).isEqualTo(Operator.NOT_EQUAL);
             soft.assertThat(negatedRestriction.value()).isEqualTo("Unknown");
         });
     }
diff --git a/api/src/test/java/jakarta/data/CompositeRestrictionRecordTest.java b/api/src/test/java/jakarta/data/CompositeRestrictionRecordTest.java
index 79764f64..67e3f9df 100644
--- a/api/src/test/java/jakarta/data/CompositeRestrictionRecordTest.java
+++ b/api/src/test/java/jakarta/data/CompositeRestrictionRecordTest.java
@@ -27,7 +27,9 @@ import java.util.List;
 
 
 class CompositeRestrictionRecordTest {
-
+    // A mock entity class for tests
+    static class Person {
+    }
 
     @Test
     void shouldCreateCompositeRestrictionWithDefaultNegation() {
@@ -71,6 +73,44 @@ class CompositeRestrictionRecordTest {
                 .hasMessage("Cannot create a composite restriction without any restrictions to combine.");
     }
 
+    @Test
+    void shouldNegateCompositeRestriction() {
+        Restriction<Person> ageLessThan50 = Restrict.lessThan(50, "age");
+        Restriction<Person> nameStartsWithDuke = Restrict.startsWith("Duke ", "name");
+        CompositeRestriction<Person> all =
+                (CompositeRestriction<Person>) Restrict.all(ageLessThan50, nameStartsWithDuke);
+        CompositeRestriction<Person> allNegated = all.negate();
+        CompositeRestriction<Person> notAll =
+                (CompositeRestriction<Person>) Restrict.not(all);
+
+        SoftAssertions.assertSoftly(soft -> {
+            soft.assertThat(all.isNegated()).isEqualTo(false);
+
+            soft.assertThat(allNegated.isNegated()).isEqualTo(true);
+
+            soft.assertThat(notAll.isNegated()).isEqualTo(true);
+        });
+    }
+
+    @Test
+    void shouldNegateNegatedCompositeRestriction() {
+        Restriction<Person> ageBetween20and30 = Restrict.between(20, 30, "age");
+        Restriction<Person> nameContainsDuke = Restrict.contains("Duke", "name");
+        CompositeRestriction<Person> any =
+                (CompositeRestriction<Person>) Restrict.any(ageBetween20and30, nameContainsDuke);
+        CompositeRestriction<Person> anyNegated = any.negate();
+        CompositeRestriction<Person> anyNotNegated =
+                (CompositeRestriction<Person>) Restrict.not(anyNegated);
+
+        SoftAssertions.assertSoftly(soft -> {
+            soft.assertThat(any.isNegated()).isEqualTo(false);
+
+            soft.assertThat(anyNegated.isNegated()).isEqualTo(true);
+
+            soft.assertThat(anyNotNegated.isNegated()).isEqualTo(false);
+        });
+    }
+
     @Test
     void shouldPreserveRestrictionsOrder() {
         Restriction<String> restriction1 = new BasicRestrictionRecord<>("title", Operator.EQUAL, "Java Guide");
diff --git a/api/src/test/java/jakarta/data/RestrictTest.java b/api/src/test/java/jakarta/data/RestrictTest.java
index d2914e08..04113be8 100644
--- a/api/src/test/java/jakarta/data/RestrictTest.java
+++ b/api/src/test/java/jakarta/data/RestrictTest.java
@@ -37,7 +37,6 @@ class RestrictTest {
             soft.assertThat(basic.field()).isEqualTo("field");
             soft.assertThat(basic.comparison()).isEqualTo(Operator.EQUAL);
             soft.assertThat(basic.value()).isEqualTo("value");
-            soft.assertThat(basic.isNegated()).isFalse();
         });
     }
 
@@ -50,9 +49,8 @@ class RestrictTest {
         TextRestrictionRecord<String> basic = (TextRestrictionRecord<String>) restriction;
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(basic.field()).isEqualTo("field");
-            soft.assertThat(basic.comparison()).isEqualTo(Operator.EQUAL);
+            soft.assertThat(basic.comparison()).isEqualTo(Operator.NOT_EQUAL);
             soft.assertThat(basic.value()).isEqualTo("value");
-            soft.assertThat(basic.isNegated()).isTrue();
         });
     }
 
@@ -81,7 +79,6 @@ class RestrictTest {
             soft.assertThat(restriction.field()).isEqualTo("field");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
             soft.assertThat(restriction.value()).isEqualTo("%substring%");
-            soft.assertThat(restriction.isNegated()).isFalse();
         });
     }
 
@@ -91,9 +88,8 @@ class RestrictTest {
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.field()).isEqualTo("field");
-            soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
+            soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_LIKE);
             soft.assertThat(restriction.value()).isEqualTo("%substring%");
-            soft.assertThat(restriction.isNegated()).isTrue();
         });
     }
 
@@ -105,7 +101,6 @@ class RestrictTest {
             soft.assertThat(restriction.field()).isEqualTo("field");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
             soft.assertThat(restriction.value()).isEqualTo("prefix%");
-            soft.assertThat(restriction.isNegated()).isFalse();
         });
     }
 
@@ -115,9 +110,8 @@ class RestrictTest {
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.field()).isEqualTo("field");
-            soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
+            soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_LIKE);
             soft.assertThat(restriction.value()).isEqualTo("prefix%");
-            soft.assertThat(restriction.isNegated()).isTrue();
         });
     }
 
@@ -129,7 +123,6 @@ class RestrictTest {
             soft.assertThat(restriction.field()).isEqualTo("field");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
             soft.assertThat(restriction.value()).isEqualTo("%suffix");
-            soft.assertThat(restriction.isNegated()).isFalse();
         });
     }
 
@@ -139,9 +132,8 @@ class RestrictTest {
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.field()).isEqualTo("field");
-            soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
+            soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_LIKE);
             soft.assertThat(restriction.value()).isEqualTo("%suffix");
-            soft.assertThat(restriction.isNegated()).isTrue();
         });
     }
 
diff --git a/api/src/test/java/jakarta/data/TextRestrictionRecordTest.java b/api/src/test/java/jakarta/data/TextRestrictionRecordTest.java
index 5b7dcb2a..14b7f2b2 100644
--- a/api/src/test/java/jakarta/data/TextRestrictionRecordTest.java
+++ b/api/src/test/java/jakarta/data/TextRestrictionRecordTest.java
@@ -20,6 +20,8 @@ package jakarta.data;
 import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.Test;
 
+import jakarta.data.BasicRestrictionRecordTest.Book;
+
 import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
 
 
@@ -35,7 +37,6 @@ class TextRestrictionRecordTest {
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.field()).isEqualTo("title");
-            soft.assertThat(restriction.isNegated()).isFalse();
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
             soft.assertThat(restriction.value()).isEqualTo("%Java%");
             soft.assertThat(restriction.isCaseSensitive()).isTrue();
@@ -47,15 +48,13 @@ class TextRestrictionRecordTest {
     void shouldCreateTextRestrictionWithExplicitNegation() {
         TextRestrictionRecord<String> restriction = new TextRestrictionRecord<>(
                 "title",
-                true,
-                Operator.LIKE,
+                Operator.NOT_LIKE,
                 "%Java%"
         );
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.field()).isEqualTo("title");
-            soft.assertThat(restriction.isNegated()).isTrue();
-            soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
+            soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_LIKE);
             soft.assertThat(restriction.value()).isEqualTo("%Java%");
             soft.assertThat(restriction.isCaseSensitive()).isTrue();
             soft.assertThat(restriction.isEscaped()).isFalse();
@@ -76,7 +75,6 @@ class TextRestrictionRecordTest {
             soft.assertThat(caseInsensitiveRestriction).isInstanceOf(TextRestrictionRecord.class);
             TextRestrictionRecord<String> textRestriction = (TextRestrictionRecord<String>) caseInsensitiveRestriction;
             soft.assertThat(textRestriction.field()).isEqualTo("title");
-            soft.assertThat(textRestriction.isNegated()).isFalse();
             soft.assertThat(textRestriction.comparison()).isEqualTo(Operator.LIKE);
             soft.assertThat(textRestriction.value()).isEqualTo("%Java%");
             soft.assertThat(textRestriction.isCaseSensitive()).isFalse();
@@ -95,7 +93,6 @@ class TextRestrictionRecordTest {
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.field()).isEqualTo("title");
-            soft.assertThat(restriction.isNegated()).isFalse();
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
             soft.assertThat(restriction.value()).isEqualTo("%Java%");
             soft.assertThat(restriction.isCaseSensitive()).isTrue();
@@ -103,19 +100,57 @@ class TextRestrictionRecordTest {
         });
     }
 
+    @Test
+    void shouldNegateLikeRestriction() {
+        TextRestriction<Book> likeJakartaEE = Restrict.like("%Jakarta EE%", "title");
+        TextRestriction<Book> notLikeJakartaEE = likeJakartaEE.negate();
+        TextRestriction<Book> anyCaseNotLikeJakartaEE = likeJakartaEE.ignoreCase().negate();
+        TextRestriction<Book> notLikeJakartaEEAnyCase = likeJakartaEE.negate().ignoreCase();
+
+        SoftAssertions.assertSoftly(soft -> {
+            soft.assertThat(likeJakartaEE.comparison()).isEqualTo(Operator.LIKE);
+            soft.assertThat(likeJakartaEE.isCaseSensitive()).isTrue();
+
+            soft.assertThat(notLikeJakartaEE.comparison()).isEqualTo(Operator.NOT_LIKE);
+            soft.assertThat(notLikeJakartaEE.isCaseSensitive()).isTrue();
+
+            soft.assertThat(anyCaseNotLikeJakartaEE.comparison()).isEqualTo(Operator.NOT_LIKE);
+            soft.assertThat(anyCaseNotLikeJakartaEE.isCaseSensitive()).isFalse();
+
+            soft.assertThat(notLikeJakartaEEAnyCase.comparison()).isEqualTo(Operator.NOT_LIKE);
+            soft.assertThat(notLikeJakartaEEAnyCase.isCaseSensitive()).isFalse();
+        });
+    }
+
+    @Test
+    void shouldNegateNegatedRestriction() {
+        TextRestriction<Book> endsWithJakartaEE = Restrict.endsWith("Jakarta EE", "title");
+        TextRestriction<Book> notEndsWithJakartaEE = endsWithJakartaEE.negate();
+        TextRestriction<Book> notNotEndsWithJakartaEE = notEndsWithJakartaEE.negate();
+
+        SoftAssertions.assertSoftly(soft -> {
+            soft.assertThat(endsWithJakartaEE.comparison()).isEqualTo(Operator.LIKE);
+            soft.assertThat(endsWithJakartaEE.value()).isEqualTo("%Jakarta EE");
+
+            soft.assertThat(notEndsWithJakartaEE.comparison()).isEqualTo(Operator.NOT_LIKE);
+            soft.assertThat(notEndsWithJakartaEE.value()).isEqualTo("%Jakarta EE");
+
+            soft.assertThat(notNotEndsWithJakartaEE.comparison()).isEqualTo(Operator.LIKE);
+            soft.assertThat(notNotEndsWithJakartaEE.value()).isEqualTo("%Jakarta EE");
+        });
+    }
+
     @Test
     void shouldSupportNegationForTextRestriction() {
         TextRestrictionRecord<String> restriction = new TextRestrictionRecord<>(
                 "author",
-                true,
-                Operator.EQUAL,
+                Operator.NOT_EQUAL,
                 "John Doe"
         );
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.field()).isEqualTo("author");
-            soft.assertThat(restriction.isNegated()).isTrue();
-            soft.assertThat(restriction.comparison()).isEqualTo(Operator.EQUAL);
+            soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_EQUAL);
             soft.assertThat(restriction.value()).isEqualTo("John Doe");
             soft.assertThat(restriction.isCaseSensitive()).isTrue();
             soft.assertThat(restriction.isEscaped()).isFalse();
diff --git a/api/src/test/java/jakarta/data/metamodel/AttributeTest.java b/api/src/test/java/jakarta/data/metamodel/AttributeTest.java
index f087674c..e1a962b9 100644
--- a/api/src/test/java/jakarta/data/metamodel/AttributeTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/AttributeTest.java
@@ -40,7 +40,6 @@ class AttributeTest {
             soft.assertThat(basic.field()).isEqualTo("testAttribute");
             soft.assertThat(basic.value()).isEqualTo("testValue");
             soft.assertThat(basic.comparison()).isEqualTo(Operator.EQUAL);
-            soft.assertThat(basic.isNegated()).isFalse();
         });
     }
 
@@ -53,8 +52,7 @@ class AttributeTest {
             BasicRestriction<String> basic = (BasicRestriction<String>) restriction;
             soft.assertThat(basic.field()).isEqualTo("testAttribute");
             soft.assertThat(basic.value()).isEqualTo("testValue");
-            soft.assertThat(basic.comparison()).isEqualTo(Operator.EQUAL);
-            soft.assertThat(basic.isNegated()).isTrue();
+            soft.assertThat(basic.comparison()).isEqualTo(Operator.NOT_EQUAL);
         });
     }
 
@@ -68,7 +66,6 @@ class AttributeTest {
             soft.assertThat(basic.field()).isEqualTo("testAttribute");
             soft.assertThat(basic.value()).isEqualTo(Set.of("value1", "value2"));
             soft.assertThat(basic.comparison()).isEqualTo(Operator.IN);
-            soft.assertThat(basic.isNegated()).isFalse();
         });
     }
 
@@ -88,8 +85,7 @@ class AttributeTest {
             BasicRestriction<String> basic = (BasicRestriction<String>) restriction;
             soft.assertThat(basic.field()).isEqualTo("testAttribute");
             soft.assertThat(basic.value()).isEqualTo(Set.of("value1", "value2"));
-            soft.assertThat(basic.comparison()).isEqualTo(Operator.IN);
-            soft.assertThat(basic.isNegated()).isTrue();
+            soft.assertThat(basic.comparison()).isEqualTo(Operator.NOT_IN);
         });
     }
 
@@ -110,7 +106,6 @@ class AttributeTest {
             soft.assertThat(basic.field()).isEqualTo("testAttribute");
             soft.assertThat(basic.value()).isNull();
             soft.assertThat(basic.comparison()).isEqualTo(Operator.EQUAL);
-            soft.assertThat(basic.isNegated()).isFalse();
         });
     }
 
@@ -123,8 +118,7 @@ class AttributeTest {
             BasicRestriction<String> basic = (BasicRestriction<String>) restriction;
             soft.assertThat(basic.field()).isEqualTo("testAttribute");
             soft.assertThat(basic.value()).isNull();
-            soft.assertThat(basic.comparison()).isEqualTo(Operator.EQUAL);
-            soft.assertThat(basic.isNegated()).isTrue();
+            soft.assertThat(basic.comparison()).isEqualTo(Operator.NOT_EQUAL);
         });
     }
 }
diff --git a/api/src/test/java/jakarta/data/metamodel/SortableAttributeTest.java b/api/src/test/java/jakarta/data/metamodel/SortableAttributeTest.java
index 697284cd..fcc204ce 100644
--- a/api/src/test/java/jakarta/data/metamodel/SortableAttributeTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/SortableAttributeTest.java
@@ -56,7 +56,6 @@ class SortableAttributeTest {
             soft.assertThat(basic.field()).isEqualTo("testAttribute");
             soft.assertThat(basic.value()).isEqualTo(10);
             soft.assertThat(basic.comparison()).isEqualTo(Operator.GREATER_THAN);
-            soft.assertThat(basic.isNegated()).isFalse();
         });
     }
 
@@ -70,7 +69,6 @@ class SortableAttributeTest {
             soft.assertThat(basic.field()).isEqualTo("testAttribute");
             soft.assertThat(basic.value()).isEqualTo(10);
             soft.assertThat(basic.comparison()).isEqualTo(Operator.GREATER_THAN_EQUAL);
-            soft.assertThat(basic.isNegated()).isFalse();
         });
     }
 
@@ -84,7 +82,6 @@ class SortableAttributeTest {
             soft.assertThat(basic.field()).isEqualTo("testAttribute");
             soft.assertThat(basic.value()).isEqualTo(10);
             soft.assertThat(basic.comparison()).isEqualTo(Operator.LESS_THAN);
-            soft.assertThat(basic.isNegated()).isFalse();
         });
     }
 
@@ -98,7 +95,6 @@ class SortableAttributeTest {
             soft.assertThat(basic.field()).isEqualTo("testAttribute");
             soft.assertThat(basic.value()).isEqualTo(10);
             soft.assertThat(basic.comparison()).isEqualTo(Operator.LESS_THAN_EQUAL);
-            soft.assertThat(basic.isNegated()).isFalse();
         });
     }
 
@@ -118,7 +114,6 @@ class SortableAttributeTest {
             soft.assertThat(lower.field()).isEqualTo("testAttribute");
             soft.assertThat(lower.value()).isEqualTo(5);
             soft.assertThat(lower.comparison()).isEqualTo(Operator.GREATER_THAN_EQUAL);
-            soft.assertThat(lower.isNegated()).isFalse();
 
             Restriction<String> upperBound = composite.restrictions().get(1);
             soft.assertThat(upperBound).isInstanceOf(BasicRestriction.class);
@@ -126,7 +121,6 @@ class SortableAttributeTest {
             soft.assertThat(upper.field()).isEqualTo("testAttribute");
             soft.assertThat(upper.value()).isEqualTo(15);
             soft.assertThat(upper.comparison()).isEqualTo(Operator.LESS_THAN_EQUAL);
-            soft.assertThat(upper.isNegated()).isFalse();
         });
     }
 }
diff --git a/api/src/test/java/jakarta/data/metamodel/TextAttributeTest.java b/api/src/test/java/jakarta/data/metamodel/TextAttributeTest.java
index b14f15a7..453f509d 100644
--- a/api/src/test/java/jakarta/data/metamodel/TextAttributeTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/TextAttributeTest.java
@@ -61,7 +61,6 @@ class TextAttributeTest {
             soft.assertThat(restriction.field()).isEqualTo("testAttribute");
             soft.assertThat(restriction.value()).isEqualTo("%testValue%");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
-            soft.assertThat(restriction.isNegated()).isFalse();
         });
     }
 
@@ -73,7 +72,6 @@ class TextAttributeTest {
             soft.assertThat(restriction.field()).isEqualTo("testAttribute");
             soft.assertThat(restriction.value()).isEqualTo("testValue%");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
-            soft.assertThat(restriction.isNegated()).isFalse();
         });
     }
 
@@ -85,7 +83,6 @@ class TextAttributeTest {
             soft.assertThat(restriction.field()).isEqualTo("testAttribute");
             soft.assertThat(restriction.value()).isEqualTo("%testValue");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
-            soft.assertThat(restriction.isNegated()).isFalse();
         });
     }
 
@@ -97,7 +94,6 @@ class TextAttributeTest {
             soft.assertThat(restriction.field()).isEqualTo("testAttribute");
             soft.assertThat(restriction.value()).isEqualTo("%test%");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
-            soft.assertThat(restriction.isNegated()).isFalse();
         });
     }
 
@@ -108,8 +104,7 @@ class TextAttributeTest {
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.field()).isEqualTo("testAttribute");
             soft.assertThat(restriction.value()).isEqualTo("%testValue%");
-            soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
-            soft.assertThat(restriction.isNegated()).isTrue();
+            soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_LIKE);
         });
     }
 
@@ -120,8 +115,7 @@ class TextAttributeTest {
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.field()).isEqualTo("testAttribute");
             soft.assertThat(restriction.value()).isEqualTo("%test%");
-            soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
-            soft.assertThat(restriction.isNegated()).isTrue();
+            soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_LIKE);
         });
     }
 
@@ -132,8 +126,7 @@ class TextAttributeTest {
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.field()).isEqualTo("testAttribute");
             soft.assertThat(restriction.value()).isEqualTo("testValue%");
-            soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
-            soft.assertThat(restriction.isNegated()).isTrue();
+            soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_LIKE);
         });
     }
 
@@ -144,8 +137,7 @@ class TextAttributeTest {
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.field()).isEqualTo("testAttribute");
             soft.assertThat(restriction.value()).isEqualTo("%testValue");
-            soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
-            soft.assertThat(restriction.isNegated()).isTrue();
+            soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_LIKE);
         });
     }
 }