Repository: jakartaee/data
Pull request #975 (2025-02-14 17:46:02)

introduce Constraint and friends
I'm sharing this work in progress so that you guys can monitor the progress and offer feedback.

Note that this is a minimal-impact first phase of what I have in my head.

There are two problems with this design:

1. We still have `UnaryRestriction` - nullability isn't handled as a Range. Ideally, I would like to see `UnaryRestriction` go away.
2. There's a redundancy between `Operator` and `Range` - really `Operator` is unnecessary in this design, but we could perhaps move it into `Range`.

So in a second phase I would try to address these issues, though I would like to hear some reactions first.

Note that this change removes `TextRestriction` (case insensitivity becomes an aspect of the `Pattern`). If case-insensitive equality is needed:

- we could introduce a `TextValue` range type, or
- `TextRestriction` could be rescued in a simpler form.

gavinking: I've now done the second phase.

> 1. We still have `UnaryRestriction` - nullability isn't handled as a Range. Ideally, I would like to see `UnaryRestriction` go away.

I've removed `UnaryRestriction` and `UnaryOperator` and replaced them with the `Null` range.

> 2. There's a redundancy between `Operator` and `Range`

I've made `Operator` a thing which is inferred from the `Range` subtype, to eliminate the redundancy. `Operator` could now be removed completely, but I intuit that you guys might like to keep it around.

> - we could introduce a `TextValue` range type, or
> - `TextRestriction` could be rescued in a simpler form.

I've gone with the second option, to demonstrate how this can have a minimal impact on the existing API.
gavinking: In the third phase, we need to nail down exactly the API for this stuff:

1. Where do application developers go to get instance of `Range`? The `Range` subtypes themselves aren't a perfect place for that. We could perhaps add static methods to the `Range` interface.
2. Should `Range` be a sealed type as I have it here? The reason for doing it is that users aren't allowed to define their own implementations. On the other hand, a Data *provider* might like to define extensions.
3. Should we attempt to hide the `Range` subtypes from clients?

Etc.

But we need to discuss this stuff.
njr-11: > In the third phase, we need to nail down exactly the API for this stuff:
> 
>     1. Where do application developers go to get instance of `Range`? The `Range` subtypes themselves aren't a perfect place for that. We could perhaps add static methods to the `Range` interface.
> 
>     2. Should `Range` be a sealed type as I have it here? The reason for doing it is that users aren't allowed to define their own implementations. On the other hand, a Data _provider_ might like to define extensions.
> 
>     3. Should we attempt to hide the `Range` subtypes from clients?

I agree with hiding the Range subtypes and having them be obtained from Range. It would otherwise be a lot of clutter for the user to sift through trying to figure what are all the available classes that they can use and where to find them.  Having them be obtained from Range makes them easily discoverable from a single place - the place that users will first go looking because the repository method signature told them to supply something of type `Range`.

A better way to hide them will be to use package protected or private where they will not be part of the API at all rather than in an `impl` package that users can see but need to ignore. There will be the question of how does the Data provider figure out which type of range it is, but the Data provider will need to be invoking methods on the Range anyways to obtain the values against which to compare and so forth, so it is natural to also obtain the Operator from the Range.

Also, while looking over this, I was wondering why we have different method names on Range, for example,
```
Range.upperBound(value)
Range.lowerBound(value)
Range.enumeration(value1, value2, value3)
```
rather than
```
Range.lessThanEqual(value)
Range.greaterThanEqual(value)
Range.in(value1, value2, value3)
```
where the latter exactly match the corresponding method names from Restrict and the static metamodel attribute interfaces (and also generally match up with Query by Method Name).

gavinking: > I agree with hiding the `Range` subtypes and having them be obtained from `Range`. 

OK, so this is what I actually implemented, the factory methods are currently on `Range` and `TextRange`.

> A better way to hide them will be to use package protected or private where they will not be part of the API at all rather than in an impl package that users can see but need to ignore.

Unfortunately Jakarta Data _implementors_ are going to need to access to them, so what I did was move them to a `.impl` package, which is the strategy we've used for other SPI stuff.

gavinking: > Also, while looking over this, I was wondering why we have different method names on `Range`

I'm OK with the idea of changing `Range.upperBound(value)` to `Range.lessThanEqual(value)`.

Slightly less keen on `Range.in()`, but if you have a strong opinion on that I guess I don't mind.
njr-11: > Unfortunately Jakarta Data _implementors_ are going to need to access to them

It isn't yet clear to me why that is.  Range is basically a Restriction that doesn't specify the entity attribute name.  For Restriction, the provider implementation only needs the attribute name/value/operator (plus case insensitivity if TextAttribute). As best I can see, excluding the attribute name which we instead obtain/infer it from the method signature, all of those same things should be similarly obtainable from a Range.  I see methods already there to get the   `range.operator()` and `textrange.caseSensitive()`.  I think only the value is missing, but I'm sure we can figure out how to add that.
gavinking: > It isn't yet clear to me why that is. 

Because they need the state, which is not always a single value, for example:

- `Interval` has an `upperBound` and a `lowerBound`
- `Enumeration` has a list of values,
- etc.
gavinking: I mean, sure, you could have `getValue()` that might return a single value or might return a list, or might return an ordered pair of two values, but that's just nasty.
njr-11: > I mean, sure, you could have `getValue()` that might return a single value or might return a list, or might return an ordered pair of two values, but that's just nasty.

Hmmm. It seems like we do already have that approach for Restriction (using value for single or set of values). I assumed we had something better.

```
        Restriction<Employee> r = _Person.yearOfBirth.in(Set.of(2020, 2022, 2024));
        Object value = ((BasicRestriction<Employee>) r).value(); // this is the Set<Integer>
```

This was an oversight that we didn't think about it, and it needs to be considered and done properly for `Restriction`, too.
gavinking: >  I assumed we had something better.

Same. I missed that.
gavinking: Actually I believe `Range` already solves it. You do `basicRestriction.range()` and you switch on its concrete type.
gavinking: Casting to `BasicRestriction` is perfectly fine, FTR. It's an API.
njr-11: Yes, casting to BasicRestriction is fine.  Switching the concrete type is not fine if we have the concrete types in an impl package.  Technically it works, but the idea of an impl package is that it's the internal implementation of spec, and a part of the API that anyone should ever use.  And we really shouldn't have externalized it in the first place.  I would like to get away from the impl packages as well given that it's a bad practice that we basically allowed as a compromise to hurriedly get Jakarta Data 1.0 done in time.  The implementation stuff should have been private or package protected.  We should definitely not add more of it.
gavinking: > the idea of an impl package is that it's the internal implementation of spec

But that's not true ... every one of the existing `impl` packages is an SPI that's intended for use by Jakarta Data implementors. They're all exported by the module descriptor. None of them are module-private.

And there's nothing at all wrong with the pattern of switching over implementations of a sealed interface. That's Java's way of expressing a sum type, and it's a basic programming technique in languages with non-stupid type systems (e.g ML, Haskell, etc).
njr-11: > But that's not true ... every one of the existing `impl` packages is an SPI that's intended for use by Jakarta Data implementors. They're all exported by the module descriptor. None of them are module-private.

I'm saying they shouldn't have been.  It was a mistake.  Anything that was truly intended to be spi would have been more appropriately place in a package named `spi` rather than `impl`.
gavinking: (There is, to be clear, no internal implementation detail of Jakarta Data exposed by any exported package.)
njr-11: > If this is just a question of _naming_ then I have no objection to renaming the package `jakarta.data.metammodel.range.spi`.

If we decide SPI is the right way to go, then yes, that would be appropriate.  Some discussion needs to be had by everyone about what the proper place is for these Range subtypes.  Initially, I wanted to see them all hidden completely, and that is still an option, although we would have some additional work to do in determining a good way to externalize the value/values. Alternatively, they could be SPI as you point out.  Or, they could be API.  There is at least one use case I can see for the latter, although I don't know if it is a good idea or not.  If they became API, then repository methods could be written to enforce/encode more requirements upfront, such as I was trying to achieve with `@Is`, but not quite as nicely because they force more work (wrapping values with ranges) and complexity (figuring out how to create a range instance that is consistent with the intent of the method) onto the repository user.  Still, they could potentially used for that purpose if we were willing to put up with the explosion of repository special parameter types that this would introduce.  Lots of different options here.  I'm not sure which directly is the best to go in at this point.

njr-11: I have been giving some thought to the terminology that is used in the API for Range.  The ideas behind the pattern are sound, including the concept of splitting up a Restriction (BasicRestriction) into two parts: an entity attribute and what that entity attribute value needs to satisfy in order to be considered matching.  In some cases, this can be easily conceptualized as a range, but for others the term range seems awkward and not very intuitive, especially those related to patterns of text, but some others as well.  I decided to try to think of some different names than `Range` that might make sense.

If I think of an entity attribute whose value "must end with the letters 'EE'", or "must be larger than 10", or "must not be null", or "must contain the text `Jakarta`", or "must match the pattern `Ja%a EE` independent of case", I could describe all of these things as conditions that the entity attribute value must meet.  I think we should consider renaming `Range` to `Condition`.  It makes a lot of sense to me to think of a `Restriction` on an entity attribute as a pairing of the entity attribute with a condition it must meet.  I was already wanting to have consistency with Restrictions on method names, and Condition fits very nicely with this:  `Condition.lessThanEqual(max)`, `Condition.startsWith(prefix)`, `Condition.like(pattern)` ...

I understand that renaming `Range` would also impact the meaningfulness of the names of the subtypes, which we are currently trying to figure out a place for: internal, SPI, or API.  This might help here as well.  If going with the name `Condition`, it would make a lot of sense for the subtypes to simply match the names of the core Restrict/Condition methods (and which also match Query by Method Name) -- `LessThan`, `Like`, `Between`, and so forth.  This added consistency would make it more agreeable to have them on the API or SPI because they would now be less likely to confuse users, and where they do have a potential benefit of enabling a repository author to encode more requirements into methods upfront and name them more meaningfully.  I don't want to push for that too heavily though.  I'd rather see us cover this with `@Is`, but if `@Is` is unwanted by others, this could at least be a decent alternative.
gavinking: If you want to go down that path, `Constraint` might be better than `Condition`.
gavinking: what time is the meeting? 9AM EST?
njr-11: > what time is the meeting? 9AM EST?

Yes, Here's the Jakarta calendar for US Eastern Time, which shows 9AM on Wednesday, February 19:
https://calendar.google.com/calendar/u/0/embed?src=eclipse-foundation.org_e9ki8t2gc75sh07qdh95c8ofvc@group.calendar.google.com&ctz=America/New_York
gavinking: Thanks, just checking.
gavinking: I've implemented the things we discussed on the call.

Questions:

1. Do we need to kinds of `TextConstraint`, or it it enough to just have `Like` with two internal implementations, which is what I have just pushed.
2. Do we need `LessThan` and `LessThanOrEqual` instead of the `boolean strict()`. I guess that's better.
gavinking: > I guess that's better.

I have done it.
njr-11: > 1. Do we need to kinds of `TextConstraint`, or it it enough to just have `Like` with two internal implementations, which is what I have just pushed.

I would say let's go with the approach you chose of not having `TextConstraint` and see if everything works without it (I think it will).

Patch:
diff --git a/api/src/main/java/jakarta/data/metamodel/BasicAttribute.java b/api/src/main/java/jakarta/data/metamodel/BasicAttribute.java
index 7c31ce7a..61fc4075 100644
--- a/api/src/main/java/jakarta/data/metamodel/BasicAttribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/BasicAttribute.java
@@ -17,6 +17,7 @@
  */
 package jakarta.data.metamodel;
 
+import jakarta.data.metamodel.constraint.Constraint;
 import jakarta.data.metamodel.restrict.Restrict;
 import jakarta.data.metamodel.restrict.Restriction;
 
@@ -54,4 +55,8 @@ public interface BasicAttribute<T,V> extends Attribute<T> {
         return Restrict.notNull(name());
     }
 
+    default Restriction<T> restrict(Constraint<V> constraint) {
+        return Restrict.restrict(constraint, name());
+    }
+
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java b/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java
index 10cc6fab..c145bf5d 100644
--- a/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java
@@ -27,6 +27,10 @@ public interface ComparableAttribute<T,V extends Comparable<V>>
         return Restrict.between(min, max, name());
     }
 
+    default Restriction<T> notBetween(V min, V max) {
+        return Restrict.notBetween(min, max, name());
+    }
+
     default Restriction<T> greaterThan(V value) {
         return Restrict.greaterThan(value, name());
     }
diff --git a/api/src/main/java/jakarta/data/metamodel/TextAttribute.java b/api/src/main/java/jakarta/data/metamodel/TextAttribute.java
index efedc1dc..5157da1f 100644
--- a/api/src/main/java/jakarta/data/metamodel/TextAttribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/TextAttribute.java
@@ -18,6 +18,7 @@
 package jakarta.data.metamodel;
 
 import jakarta.data.Sort;
+import jakarta.data.metamodel.constraint.Like;
 import jakarta.data.metamodel.restrict.Restrict;
 import jakarta.data.metamodel.restrict.TextRestriction;
 
@@ -35,10 +36,6 @@ public interface TextAttribute<T> extends ComparableAttribute<T,String> {
      */
     Sort<T> ascIgnoreCase();
 
-    default TextRestriction<T> contains(String substring) {
-        return Restrict.contains(substring, name());
-    }
-
     /**
      * Obtain a request for a descending, case insensitive {@link Sort} based on the entity attribute.
      *
@@ -46,79 +43,66 @@ public interface TextAttribute<T> extends ComparableAttribute<T,String> {
      */
     Sort<T> descIgnoreCase();
 
-    default TextRestriction<T> endsWith(String suffix) {
-        return Restrict.endsWith(suffix, name());
-    }
-
     @Override
     default TextRestriction<T> equalTo(String value) {
         return Restrict.equalTo(value, name());
     }
 
     @Override
-    default TextRestriction<T> greaterThan(String value) {
-        return Restrict.greaterThan(value, name());
+    default TextRestriction<T> notEqualTo(String value) {
+        return Restrict.notEqualTo(value, name());
     }
 
-    @Override
-    default TextRestriction<T> greaterThanEqual(String value) {
-        return Restrict.greaterThanEqual(value, name());
+    default TextRestriction<T> like(Like pattern) {
+        return Restrict.like(pattern, name());
     }
 
-    @Override
-    default TextRestriction<T> lessThan(String value) {
-        return Restrict.lessThan(value, name());
+    default TextRestriction<T> like(String pattern) {
+        return Restrict.like(pattern, name());
     }
 
-    @Override
-    default TextRestriction<T> lessThanEqual(String value) {
-        return Restrict.lessThanEqual(value, name());
+    default TextRestriction<T> like(String pattern, char charWildcard, char stringWildcard) {
+        return Restrict.like(pattern, charWildcard, stringWildcard, name());
     }
 
-    // TODO once we have Pattern:
-    //default TextRestriction<T> like(Pattern pattern) {
-    //    return Restrict.like(pattern, name());
-    //}
-
-    default TextRestriction<T> like(String pattern) {
-        return Restrict.like(pattern, name());
+    default TextRestriction<T> like(String pattern, char charWildcard, char stringWildcard, char escape) {
+        return Restrict.like(pattern, charWildcard, stringWildcard, escape, name());
     }
 
-    default TextRestriction<T> like(String pattern,
-                                    char charWildcard,
-                                    char stringWildcard) {
-        return Restrict.like(pattern, charWildcard, stringWildcard, name());
+    default TextRestriction<T> notLike(String pattern) {
+        return Restrict.notLike(pattern, name());
     }
 
-    default TextRestriction<T> notContains(String substring) {
-        return Restrict.notContains(substring, name());
+    default TextRestriction<T> notLike(String pattern, char charWildcard, char stringWildcard) {
+        return Restrict.notLike(pattern, charWildcard, stringWildcard, name());
     }
 
-    default TextRestriction<T> notEndsWith(String suffix) {
-        return Restrict.notEndsWith(suffix, name());
+    default TextRestriction<T> notLike(String pattern, char charWildcard, char stringWildcard, char escape) {
+        return Restrict.notLike(pattern, charWildcard, stringWildcard, escape, name());
     }
 
-    @Override
-    default TextRestriction<T> notEqualTo(String value) {
-        return Restrict.notEqualTo(value, name());
+    default TextRestriction<T> contains(String substring) {
+        return Restrict.contains(substring, name());
     }
 
-    default TextRestriction<T> notLike(String pattern) {
-        return Restrict.notLike(pattern, name());
+    default TextRestriction<T> notContains(String substring) {
+        return Restrict.notContains(substring, name());
     }
 
-    default TextRestriction<T> notLike(String pattern,
-                                       char charWildcard,
-                                       char stringWildcard) {
-        return Restrict.notLike(pattern, charWildcard, stringWildcard, name());
+    default TextRestriction<T> endsWith(String suffix) {
+        return Restrict.endsWith(suffix, name());
     }
 
-    default TextRestriction<T> notStartsWith(String prefix) {
-        return Restrict.notStartsWith(prefix, name());
+    default TextRestriction<T> notEndsWith(String suffix) {
+        return Restrict.notEndsWith(suffix, name());
     }
 
     default TextRestriction<T> startsWith(String prefix) {
         return Restrict.startsWith(prefix, name());
     }
 
+    default TextRestriction<T> notStartsWith(String prefix) {
+        return Restrict.notStartsWith(prefix, name());
+    }
+
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/restrict/UnaryRestriction.java b/api/src/main/java/jakarta/data/metamodel/constraint/Between.java
similarity index 76%
rename from api/src/main/java/jakarta/data/metamodel/restrict/UnaryRestriction.java
rename to api/src/main/java/jakarta/data/metamodel/constraint/Between.java
index 689a83da..8c9aeddf 100644
--- a/api/src/main/java/jakarta/data/metamodel/restrict/UnaryRestriction.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/Between.java
@@ -15,13 +15,9 @@
  *
  * SPDX-License-Identifier: Apache-2.0
  */
-package jakarta.data.metamodel.restrict;
+package jakarta.data.metamodel.constraint;
 
-public interface UnaryRestriction<T> extends Restriction<T> {
-    String attribute();
-
-    UnaryOperator operator();
-
-    @Override
-    UnaryRestriction<T> negate();
+public interface Between<T extends Comparable<T>> extends Constraint<T> {
+    T lowerBound();
+    T upperBound();
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/BetweenRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/BetweenRecord.java
new file mode 100644
index 00000000..b99d5a38
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/BetweenRecord.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package jakarta.data.metamodel.constraint;
+
+import jakarta.data.metamodel.restrict.Operator;
+
+import java.util.Objects;
+
+record BetweenRecord<T extends Comparable<T>>(T lowerBound, T upperBound)
+        implements Between<T> {
+    public BetweenRecord {
+        Objects.requireNonNull(lowerBound);
+        Objects.requireNonNull(upperBound);
+    }
+
+    @Override
+    public Operator operator() {
+        return Operator.BETWEEN;
+    }
+
+    @Override
+    public String toString() {
+        return "BETWEEN " + lowerBound + " AND " + upperBound;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return obj instanceof BetweenRecord<?> that
+            && lowerBound.equals(that.lowerBound)
+            && upperBound.equals(that.upperBound);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(lowerBound, upperBound);
+    }
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/Constraint.java b/api/src/main/java/jakarta/data/metamodel/constraint/Constraint.java
new file mode 100644
index 00000000..5b80ca87
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/Constraint.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package jakarta.data.metamodel.constraint;
+
+import jakarta.data.metamodel.restrict.Operator;
+
+import java.util.Set;
+
+public interface Constraint<T> {
+    Operator operator();
+
+    static <T> Constraint<T> equalTo(T value) {
+        return new EqualToRecord<>(value);
+    }
+
+    @SafeVarargs
+    static <T> Constraint<T> in(T... values) {
+        return new InRecord<>(Set.of(values));
+    }
+
+    static <T> Constraint<T> in(Set<T> values) {
+        return new InRecord<>(values);
+    }
+
+    static <T> Constraint<T> isNull() {
+        return new NullRecord<>();
+    }
+
+    static <T extends Comparable<T>> Constraint<T> greaterThan(T bound) {
+        return new GreaterThanRecord<>(bound);
+    }
+
+    static <T extends Comparable<T>> Constraint<T> lessThan(T bound) {
+        return new LessThanRecord<>(bound);
+    }
+
+    static <T extends Comparable<T>> Constraint<T> greaterThanOrEqual(T bound) {
+        return new GreaterThanOrEqualRecord<>(bound);
+    }
+
+    static <T extends Comparable<T>> Constraint<T> lessThanOrEqual(T bound) {
+        return new LessThanOrEqualRecord<>(bound);
+    }
+
+    static <T extends Comparable<T>> Constraint<T> between(T lowerBound, T upperBound) {
+        return new BetweenRecord<>(lowerBound, upperBound);
+    }
+
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/EqualTo.java b/api/src/main/java/jakarta/data/metamodel/constraint/EqualTo.java
new file mode 100644
index 00000000..6aa34624
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/EqualTo.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package jakarta.data.metamodel.constraint;
+
+public interface EqualTo<T> extends Constraint<T> {
+    T value();
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/EqualToRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/EqualToRecord.java
new file mode 100644
index 00000000..4d2835f9
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/EqualToRecord.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package jakarta.data.metamodel.constraint;
+
+import jakarta.data.metamodel.restrict.Operator;
+
+import java.util.Objects;
+
+record EqualToRecord<T>(T value) implements EqualTo<T> {
+    public EqualToRecord {
+        Objects.requireNonNull(value, "Value must not be null");
+    }
+
+    @Override
+    public Operator operator() {
+        return Operator.EQUAL;
+    }
+
+    @Override
+    public String toString() {
+        return value instanceof String ? "= '" + value + "'" : "= " + value.toString();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return obj instanceof EqualToRecord<?> that
+            && value.equals(that.value);
+    }
+
+    @Override
+    public int hashCode() {
+        return value.hashCode();
+    }
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThan.java b/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThan.java
new file mode 100644
index 00000000..bf691282
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThan.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package jakarta.data.metamodel.constraint;
+
+public interface GreaterThan<T extends Comparable<T>> extends Constraint<T> {
+    T bound();
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThanOrEqual.java b/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThanOrEqual.java
new file mode 100644
index 00000000..7151f80b
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThanOrEqual.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package jakarta.data.metamodel.constraint;
+
+public interface GreaterThanOrEqual<T extends Comparable<T>> extends Constraint<T> {
+    T bound();
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThanOrEqualRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThanOrEqualRecord.java
new file mode 100644
index 00000000..d68085ab
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThanOrEqualRecord.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package jakarta.data.metamodel.constraint;
+
+import jakarta.data.metamodel.restrict.Operator;
+
+import java.util.Objects;
+
+record GreaterThanOrEqualRecord<T extends Comparable<T>>(T bound)
+        implements GreaterThanOrEqual<T> {
+    public GreaterThanOrEqualRecord {
+        Objects.requireNonNull(bound, "Lower bound must not be null");
+    }
+
+    @Override
+    public Operator operator() {
+        return Operator.GREATER_THAN_EQUAL;
+    }
+
+    @Override
+    public String toString() {
+        return ">= " + bound.toString();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return obj instanceof GreaterThanOrEqualRecord<?> that
+            && bound.equals(that.bound);
+    }
+
+    @Override
+    public int hashCode() {
+        return bound.hashCode();
+    }
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThanRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThanRecord.java
new file mode 100644
index 00000000..a02b6f00
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThanRecord.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package jakarta.data.metamodel.constraint;
+
+import jakarta.data.metamodel.restrict.Operator;
+
+import java.util.Objects;
+
+record GreaterThanRecord<T extends Comparable<T>>(T bound)
+        implements GreaterThan<T> {
+    public GreaterThanRecord {
+        Objects.requireNonNull(bound, "Lower bound must not be null");
+    }
+
+    @Override
+    public Operator operator() {
+        return Operator.GREATER_THAN;
+    }
+
+    @Override
+    public String toString() {
+        return "> " + bound.toString();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return obj instanceof GreaterThanRecord<?> that
+            && bound.equals(that.bound);
+    }
+
+    @Override
+    public int hashCode() {
+        return bound.hashCode();
+    }
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/restrict/UnaryOperator.java b/api/src/main/java/jakarta/data/metamodel/constraint/In.java
similarity index 78%
rename from api/src/main/java/jakarta/data/metamodel/restrict/UnaryOperator.java
rename to api/src/main/java/jakarta/data/metamodel/constraint/In.java
index a14863b9..cb2697b9 100644
--- a/api/src/main/java/jakarta/data/metamodel/restrict/UnaryOperator.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/In.java
@@ -15,12 +15,10 @@
  *
  * SPDX-License-Identifier: Apache-2.0
  */
-package jakarta.data.metamodel.restrict;
+package jakarta.data.metamodel.constraint;
 
-public enum UnaryOperator {
-    IS_NULL, IS_NOT_NULL;
+import java.util.Set;
 
-    UnaryOperator negate() {
-        return this == IS_NULL ? IS_NOT_NULL : IS_NULL;
-    }
+public interface In<T> extends Constraint<T> {
+    Set<T> values();
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/InRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/InRecord.java
new file mode 100644
index 00000000..e4933460
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/InRecord.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package jakarta.data.metamodel.constraint;
+
+import jakarta.data.metamodel.restrict.Operator;
+
+import java.util.Objects;
+import java.util.Set;
+
+record InRecord<T>(Set<T> values) implements In<T> {
+
+    public InRecord {
+        Objects.requireNonNull(values, "Enumerated values cannot be null");
+    }
+
+    @Override
+    public Operator operator() {
+        return Operator.IN;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return obj instanceof InRecord<?> that
+            && values.equals(that.values);
+    }
+
+    @Override
+    public int hashCode() {
+        return values.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return "IN " + values;
+    }
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/LessThan.java b/api/src/main/java/jakarta/data/metamodel/constraint/LessThan.java
new file mode 100644
index 00000000..3b532887
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/LessThan.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package jakarta.data.metamodel.constraint;
+
+public interface LessThan<T extends Comparable<T>> extends Constraint<T> {
+    T bound();
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/LessThanOrEqual.java b/api/src/main/java/jakarta/data/metamodel/constraint/LessThanOrEqual.java
new file mode 100644
index 00000000..a7cf8871
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/LessThanOrEqual.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package jakarta.data.metamodel.constraint;
+
+public interface LessThanOrEqual<T extends Comparable<T>> extends Constraint<T> {
+    T bound();
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/LessThanOrEqualRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/LessThanOrEqualRecord.java
new file mode 100644
index 00000000..8a039be6
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/LessThanOrEqualRecord.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package jakarta.data.metamodel.constraint;
+
+import jakarta.data.metamodel.restrict.Operator;
+
+import java.util.Objects;
+
+record LessThanOrEqualRecord<T extends Comparable<T>>(T bound)
+        implements LessThanOrEqual<T> {
+    public LessThanOrEqualRecord {
+        Objects.requireNonNull(bound, "Upper bound must not be null");
+    }
+
+    @Override
+    public Operator operator() {
+        return Operator.LESS_THAN_EQUAL;
+    }
+
+    @Override
+    public String toString() {
+        return "<= " + bound.toString();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return obj instanceof LessThanOrEqualRecord<?> that
+            && bound.equals(that.bound);
+    }
+
+    @Override
+    public int hashCode() {
+        return bound.hashCode();
+    }
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/LessThanRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/LessThanRecord.java
new file mode 100644
index 00000000..9a1589a1
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/LessThanRecord.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package jakarta.data.metamodel.constraint;
+
+import jakarta.data.metamodel.restrict.Operator;
+
+import java.util.Objects;
+
+record LessThanRecord<T extends Comparable<T>>(T bound)
+        implements LessThan<T> {
+    public LessThanRecord {
+        Objects.requireNonNull(bound, "Upper bound must not be null");
+    }
+
+    @Override
+    public Operator operator() {
+        return Operator.LESS_THAN;
+    }
+
+    @Override
+    public String toString() {
+        return "< " + bound.toString();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return obj instanceof LessThanRecord<?> that
+            && bound.equals(that.bound);
+    }
+
+    @Override
+    public int hashCode() {
+        return bound.hashCode();
+    }
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/Like.java b/api/src/main/java/jakarta/data/metamodel/constraint/Like.java
new file mode 100644
index 00000000..c29490d3
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/Like.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2024 Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package jakarta.data.metamodel.constraint;
+
+public interface Like extends Constraint<String> {
+    String string();
+    boolean caseSensitive();
+    boolean pattern();
+    Character escape();
+    Like ignoreCase();
+
+    static Like literal(String literal) {
+        return new LikeLiteralRecord(literal);
+    }
+
+    static Like pattern(String pattern) {
+        return new LikePatternRecord(pattern);
+    }
+
+    static Like pattern(String pattern, char charWildcard, char stringWildcard) {
+        return new LikePatternRecord(pattern, charWildcard, stringWildcard);
+    }
+
+    static Like pattern(String pattern, char charWildcard, char stringWildcard, char escape) {
+        return new LikePatternRecord(pattern, charWildcard, stringWildcard, escape);
+    }
+
+    static Like substring(String substring) {
+        return LikePatternRecord.substring(substring);
+    }
+
+    static Like prefix(String prefix) {
+        return LikePatternRecord.prefix(prefix);
+    }
+
+    static Like suffix(String suffix) {
+        return LikePatternRecord.suffix(suffix);
+    }
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/LikeLiteralRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/LikeLiteralRecord.java
new file mode 100644
index 00000000..2ee819b9
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/LikeLiteralRecord.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package jakarta.data.metamodel.constraint;
+
+import jakarta.data.metamodel.restrict.Operator;
+
+import java.util.Objects;
+
+record LikeLiteralRecord(String literal, boolean caseSensitive)
+        implements Like {
+
+    public LikeLiteralRecord {
+        Objects.requireNonNull(literal, "Literal must not be null");
+    }
+
+    public LikeLiteralRecord(String literal) {
+        this(literal, true);
+    }
+
+    @Override
+    public String string() {
+        return literal;
+    }
+
+    @Override
+    public Like ignoreCase() {
+        return new LikeLiteralRecord(literal, false);
+    }
+
+    @Override
+    public Character escape() {
+        return null;
+    }
+
+    @Override
+    public boolean pattern() {
+        return false;
+    }
+
+    @Override
+    public Operator operator() {
+        return Operator.EQUAL;
+    }
+
+    @Override
+    public String toString() {
+        return "= '" + literal + "'" + (caseSensitive ? "" : " IGNORE CASE");
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return obj instanceof LikeLiteralRecord that
+            && literal.equals(that.literal)
+            && caseSensitive == that.caseSensitive;
+    }
+
+    @Override
+    public int hashCode() {
+        return literal.hashCode();
+    }
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/LikePatternRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/LikePatternRecord.java
new file mode 100644
index 00000000..5645a4ea
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/LikePatternRecord.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package jakarta.data.metamodel.constraint;
+
+import jakarta.data.metamodel.restrict.Operator;
+
+import java.util.Objects;
+
+record LikePatternRecord(String string, boolean caseSensitive, Character escape)
+        implements Like {
+
+    public static final char CHAR_WILDCARD = '_';
+    public static final char STRING_WILDCARD = '%';
+    public static final char ESCAPE = '\\';
+
+    public LikePatternRecord {
+        Objects.requireNonNull(string, "Pattern must not be null");
+    }
+
+    public LikePatternRecord(String pattern) {
+        this(pattern, true, null);
+    }
+
+    public LikePatternRecord(String pattern, Character escape) {
+        this(pattern, true, escape);
+    }
+
+    public LikePatternRecord(String pattern, char charWildcard, char stringWildcard) {
+        this(pattern, charWildcard, stringWildcard, ESCAPE);
+    }
+
+    public LikePatternRecord(String pattern, char charWildcard, char stringWildcard, char escape) {
+        this(translate(pattern, charWildcard, stringWildcard, escape), escape);
+    }
+
+    @Override
+    public LikePatternRecord ignoreCase() {
+        return new LikePatternRecord(string, false, escape);
+    }
+
+    @Override
+    public boolean pattern() {
+        return true;
+    }
+
+    @Override
+    public Operator operator() {
+        return Operator.LIKE;
+    }
+
+    @Override
+    public String toString() {
+        return "LIKE '" + string + "'"
+                + (caseSensitive ? "" : " IGNORE CASE")
+                + (escape == null ? "" : " ESCAPE '\\'");
+    }
+
+    public static LikePatternRecord prefix(String prefix) {
+        return new LikePatternRecord(escape(prefix) + STRING_WILDCARD, ESCAPE);
+    }
+
+    public static LikePatternRecord suffix(String suffix) {
+        return new LikePatternRecord(STRING_WILDCARD + escape(suffix), ESCAPE);
+    }
+
+    public static LikePatternRecord substring(String substring) {
+        return new LikePatternRecord(STRING_WILDCARD + escape(substring) + STRING_WILDCARD, ESCAPE);
+    }
+
+    private static String escape(String literal) {
+        final var result = new StringBuilder();
+        for (int i = 0; i<literal.length(); i++) {
+            final char ch = literal.charAt(i);
+            if (ch == STRING_WILDCARD || ch == CHAR_WILDCARD || ch == ESCAPE) {
+                result.append(ESCAPE);
+            }
+            result.append(ch);
+        }
+        return result.toString();
+    }
+
+    private static String translate(String pattern, char charWildcard, char stringWildcard, char escape) {
+        if ( charWildcard == stringWildcard ) {
+            throw new IllegalArgumentException("Cannot use the same character (" + charWildcard + ") for both wildcards.");
+        }
+        if (charWildcard == escape || stringWildcard == escape) {
+            throw new IllegalArgumentException("Cannot use the same character (" + escape + ") for both a wildcard and escape character.");
+        }
+        final var result = new StringBuilder();
+        for (int i = 0; i<pattern.length(); i++) {
+            final char ch = pattern.charAt(i);
+            if (ch == charWildcard) {
+                result.append(CHAR_WILDCARD);
+            } else if (ch == stringWildcard) {
+                result.append(STRING_WILDCARD);
+            } else {
+                if (ch == STRING_WILDCARD || ch == CHAR_WILDCARD || ch == escape) {
+                    result.append(escape);
+                }
+                result.append(ch);
+            }
+        }
+        return result.toString();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return obj instanceof LikePatternRecord that
+            && string.equals(that.string)
+            && caseSensitive == that.caseSensitive
+            && Objects.equals(escape, that.escape);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(string, caseSensitive, escape);
+    }
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/Null.java b/api/src/main/java/jakarta/data/metamodel/constraint/Null.java
new file mode 100644
index 00000000..7f257363
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/Null.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package jakarta.data.metamodel.constraint;
+
+public interface Null<T> extends Constraint<T> {
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/restrict/UnaryRestrictionRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/NullRecord.java
similarity index 65%
rename from api/src/main/java/jakarta/data/metamodel/restrict/UnaryRestrictionRecord.java
rename to api/src/main/java/jakarta/data/metamodel/constraint/NullRecord.java
index 248d5189..6eb82811 100644
--- a/api/src/main/java/jakarta/data/metamodel/restrict/UnaryRestrictionRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/NullRecord.java
@@ -15,17 +15,28 @@
  *
  * SPDX-License-Identifier: Apache-2.0
  */
-package jakarta.data.metamodel.restrict;
+package jakarta.data.metamodel.constraint;
+
+import jakarta.data.metamodel.restrict.Operator;
+
+record NullRecord<T>() implements Null<T> {
+    @Override
+    public boolean equals(Object obj) {
+        return obj instanceof NullRecord;
+    }
 
-public record UnaryRestrictionRecord<T>(String attribute, UnaryOperator operator)
-        implements UnaryRestriction<T> {
     @Override
-    public UnaryRestriction<T> negate() {
-        return new UnaryRestrictionRecord<>(attribute, operator.negate());
+    public int hashCode() {
+        return 0;
     }
+    @Override
+    public Operator operator() {
+        return Operator.NULL;
+    }
+
 
     @Override
     public String toString() {
-        return attribute + " " + operator;
+        return "IS NULL";
     }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/restrict/BasicRestriction.java b/api/src/main/java/jakarta/data/metamodel/restrict/BasicRestriction.java
index 27171a46..d762610e 100644
--- a/api/src/main/java/jakarta/data/metamodel/restrict/BasicRestriction.java
+++ b/api/src/main/java/jakarta/data/metamodel/restrict/BasicRestriction.java
@@ -17,6 +17,8 @@
  */
 package jakarta.data.metamodel.restrict;
 
+import jakarta.data.metamodel.constraint.Constraint;
+
 public interface BasicRestriction<T> extends Restriction<T> {
     String attribute();
 
@@ -25,5 +27,5 @@ public interface BasicRestriction<T> extends Restriction<T> {
     @Override
     BasicRestriction<T> negate();
 
-    Object value();
+    Constraint<?> constraint();
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/restrict/BasicRestrictionRecord.java b/api/src/main/java/jakarta/data/metamodel/restrict/BasicRestrictionRecord.java
index cab38a0b..5a1c244d 100644
--- a/api/src/main/java/jakarta/data/metamodel/restrict/BasicRestrictionRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/restrict/BasicRestrictionRecord.java
@@ -21,24 +21,30 @@ package jakarta.data.metamodel.restrict;
 // The proper way for users to obtain instances is via
 // the static metamodel or Restrict.* methods 
 
+import jakarta.data.metamodel.constraint.Constraint;
+
 import java.util.Objects;
 
-record BasicRestrictionRecord<T>(
-        String attribute,
-        Operator comparison,
-        Object value) implements BasicRestriction<T> {
+record BasicRestrictionRecord<T>(String attribute, Constraint<?> constraint, boolean negated)
+        implements BasicRestriction<T> {
 
     BasicRestrictionRecord {
         Objects.requireNonNull(attribute, "Attribute must not be null");
-        Objects.requireNonNull(value, "Value must not be null");
+        Objects.requireNonNull(constraint, "Constraint must not be null");
+    }
+
+    public BasicRestrictionRecord(String attribute, Constraint<?> constraint) {
+        this(attribute, constraint, false);
+    }
+
+    @Override
+    public Operator comparison() {
+        return negated ? constraint.operator().negate() : constraint.operator();
     }
 
     @Override
     public BasicRestriction<T> negate() {
-        return new BasicRestrictionRecord<>(
-                attribute,
-                comparison.negate(),
-                value);
+        return new BasicRestrictionRecord<>(attribute, constraint, !negated);
     }
 
     /**
@@ -50,20 +56,12 @@ record BasicRestrictionRecord<T>(
      */
     @Override
     public String toString() {
-        String comparisonString = comparison.asQueryLanguage();
-        String valueString = value.toString();
-        StringBuilder builder = new StringBuilder(
-                attribute.length() +
-                comparisonString.length() +
-                valueString.length() +
-                4); // number of additional characters that might be appended
-        builder.append(attribute).append(' ')
-               .append(comparisonString).append(' ');
-        if (value instanceof CharSequence) {
-            builder.append('"').append(valueString).append('"');
-        } else {
-            builder.append(valueString);
-        }
-        return builder.toString();
+        final Operator comparison = comparison();
+        final String op = comparison.asQueryLanguage();
+        return switch (comparison.arity()) {
+            case 1 -> attribute + ' ' + op;
+            case 2,3 -> attribute + ' ' + constraint;
+            default -> throw new UnsupportedOperationException("Unexpected arity");
+        };
     }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/restrict/Operator.java b/api/src/main/java/jakarta/data/metamodel/restrict/Operator.java
index 8cb178c1..cd11822c 100644
--- a/api/src/main/java/jakarta/data/metamodel/restrict/Operator.java
+++ b/api/src/main/java/jakarta/data/metamodel/restrict/Operator.java
@@ -27,7 +27,11 @@ public enum Operator {
     LIKE,
     NOT_EQUAL,
     NOT_IN,
-    NOT_LIKE;
+    NOT_LIKE,
+    NULL,
+    NOT_NULL,
+    BETWEEN,
+    NOT_BETWEEN;
 
     /**
      * Representation of the operator as it appears in query language.
@@ -48,6 +52,10 @@ public enum Operator {
             case NOT_EQUAL -> "<>";
             case NOT_IN -> "NOT IN";
             case NOT_LIKE -> "NOT LIKE";
+            case NULL -> "IS NULL";
+            case NOT_NULL -> "IS NOT NULL";
+            case BETWEEN -> "BETWEEN";
+            case NOT_BETWEEN -> "NOT BETWEEN";
         };
     }
 
@@ -68,6 +76,18 @@ public enum Operator {
             case NOT_EQUAL -> EQUAL;
             case NOT_IN -> IN;
             case NOT_LIKE -> LIKE;
+            case NULL -> NOT_NULL;
+            case NOT_NULL -> NULL;
+            case BETWEEN -> NOT_BETWEEN;
+            case NOT_BETWEEN -> BETWEEN;
+        };
+    }
+
+    int arity() {
+        return switch (this) {
+            case NULL, NOT_NULL -> 1;
+            case BETWEEN -> 3;
+            default -> 2;
         };
     }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/restrict/Restrict.java b/api/src/main/java/jakarta/data/metamodel/restrict/Restrict.java
index d1530824..b5d5b7d4 100644
--- a/api/src/main/java/jakarta/data/metamodel/restrict/Restrict.java
+++ b/api/src/main/java/jakarta/data/metamodel/restrict/Restrict.java
@@ -17,6 +17,9 @@
  */
 package jakarta.data.metamodel.restrict;
 
+import jakarta.data.metamodel.constraint.Constraint;
+import jakarta.data.metamodel.constraint.Like;
+
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
@@ -26,15 +29,6 @@ import java.util.Set;
 // The other place is from static metamodel attributes.
 public class Restrict {
 
-    private static final char CHAR_WILDCARD = '_';
-
-    private static final char ESCAPE_CHAR = '\\';
-
-    // used internally for more readable code
-    private static final boolean ESCAPED = true;
-
-    private static final char STRING_WILDCARD = '%';
-
     // prevent instantiation
     private Restrict() {
     }
@@ -51,89 +45,75 @@ public class Restrict {
                                                 List.of(restrictions));
     }
 
-    public static <T, V extends Comparable<V>> Restriction<T> between(V min,
-                                                                      V max,
-                                                                      String attribute) {
-        return all(greaterThanEqual(min, attribute),
-                   lessThanEqual(max, attribute));
-    }
-
     // TODO Need to think more about how to best cover negation of multiple
     // and then make negation of Single consistent with it
 
-    public static <T> TextRestriction<T> contains(String substring, String attribute) {
-        String pattern = toLikeEscaped(CHAR_WILDCARD, STRING_WILDCARD, true, substring, true);
-        return new TextRestrictionRecord<>(attribute, Operator.LIKE, ESCAPED, pattern);
+    public static <T> BasicRestriction<T> restrict(Constraint<?> constraint, String attribute) {
+        return new BasicRestrictionRecord<>(attribute, constraint);
     }
 
-    public static <T> TextRestriction<T> endsWith(String suffix, String attribute) {
-        String pattern = toLikeEscaped(CHAR_WILDCARD, STRING_WILDCARD, true, suffix, false);
-        return new TextRestrictionRecord<>(attribute, Operator.LIKE, ESCAPED, pattern);
+    public static <T> TextRestriction<T> contains(String substring, String attribute) {
+        return new TextRestrictionRecord<>(attribute, Like.substring(substring));
     }
 
-    public static <T> Restriction<T> equalTo(Object value, String attribute) {
-        return new BasicRestrictionRecord<>(attribute, Operator.EQUAL, value);
+    public static <T> TextRestriction<T> startsWith(String prefix, String attribute) {
+        return new TextRestrictionRecord<>(attribute, Like.prefix(prefix));
     }
 
-    public static <T> TextRestriction<T> equalTo(String value, String attribute) {
-        return new TextRestrictionRecord<>(attribute, Operator.EQUAL, value);
+    public static <T> TextRestriction<T> endsWith(String suffix, String attribute) {
+        return new TextRestrictionRecord<>(attribute, Like.suffix(suffix));
     }
 
-    public static <T> UnaryRestriction<T> isNull(String attribute) {
-        return new UnaryRestrictionRecord<>(attribute, UnaryOperator.IS_NULL);
+    public static <T> BasicRestriction<T> equalTo(Object value, String attribute) {
+        return new BasicRestrictionRecord<>(attribute, Constraint.equalTo(value));
     }
 
-    public static <T, V extends Comparable<V>> Restriction<T> greaterThan(V value, String attribute) {
-        return new BasicRestrictionRecord<>(attribute, Operator.GREATER_THAN, value);
+    public static <T> TextRestriction<T> equalTo(String value, String attribute) {
+        return new TextRestrictionRecord<>(attribute, Like.literal(value));
     }
 
-    public static <T> TextRestriction<T> greaterThan(String value, String attribute) {
-        return new TextRestrictionRecord<>(attribute, Operator.GREATER_THAN, value);
+    public static <T> BasicRestriction<T> isNull(String attribute) {
+        return new BasicRestrictionRecord<>(attribute, Constraint.isNull());
     }
 
-    public static <T, V extends Comparable<V>> Restriction<T> greaterThanEqual(V value, String attribute) {
-        return new BasicRestrictionRecord<>(attribute, Operator.GREATER_THAN_EQUAL, value);
+    public static <T, V extends Comparable<V>> BasicRestriction<T> greaterThan(V value, String attribute) {
+        return new BasicRestrictionRecord<>(attribute, Constraint.greaterThan(value));
     }
 
-    public static <T> TextRestriction<T> greaterThanEqual(String value, String attribute) {
-        return new TextRestrictionRecord<>(attribute, Operator.GREATER_THAN_EQUAL, value);
+    public static <T, V extends Comparable<V>> BasicRestriction<T> greaterThanEqual(V value, String attribute) {
+        return new BasicRestrictionRecord<>(attribute, Constraint.greaterThanOrEqual(value));
     }
 
-    public static <T> Restriction<T> in(Set<?> values, String attribute) {
-        return new BasicRestrictionRecord<>(attribute, Operator.IN, values);
+    public static <T> BasicRestriction<T> in(Set<?> values, String attribute) {
+        return new BasicRestrictionRecord<>(attribute, Constraint.in(values));
     }
 
-    public static <T, V extends Comparable<V>> Restriction<T> lessThan(V value, String attribute) {
-        return new BasicRestrictionRecord<>(attribute, Operator.LESS_THAN, value);
+    public static <T, V extends Comparable<V>> BasicRestriction<T> lessThan(V value, String attribute) {
+        return new BasicRestrictionRecord<>(attribute, Constraint.lessThan(value));
     }
 
-    public static <T> TextRestriction<T> lessThan(String value, String attribute) {
-        return new TextRestrictionRecord<>(attribute, Operator.LESS_THAN, value);
+    public static <T, V extends Comparable<V>> BasicRestriction<T> lessThanEqual(V value, String attribute) {
+        return new BasicRestrictionRecord<>(attribute, Constraint.lessThanOrEqual(value));
     }
 
-    public static <T, V extends Comparable<V>> Restriction<T> lessThanEqual(V value, String attribute) {
-        return new BasicRestrictionRecord<>(attribute, Operator.LESS_THAN_EQUAL, value);
+    public static <T, V extends Comparable<V>> BasicRestriction<T> between(V lowerBound, V upperBound, String attribute) {
+        return new BasicRestrictionRecord<>(attribute, Constraint.between(lowerBound, upperBound));
     }
 
-    public static <T> TextRestriction<T> lessThanEqual(String value, String attribute) {
-        return new TextRestrictionRecord<>(attribute, Operator.LESS_THAN_EQUAL, value);
+    public static <T> TextRestriction<T> like(Like textRange, String attribute) {
+        return new TextRestrictionRecord<>(attribute, textRange);
     }
 
-    // TODO this would be possible if Pattern is added, but is it even useful?
-    //public static <T> TextRestriction<T> like(Pattern pattern, String attribute) {
-    //    return new TextRestriction<>(attribute, Operator.LIKE, ESCAPED, pattern);
-    //}
-
     public static <T> TextRestriction<T> like(String pattern, String attribute) {
-        return new TextRestrictionRecord<>(attribute, Operator.LIKE, pattern);
+        return new TextRestrictionRecord<>(attribute, Like.pattern(pattern));
+    }
+
+    public static <T> TextRestriction<T> like(String pattern, char charWildcard, char stringWildcard, String attribute) {
+        return new TextRestrictionRecord<>(attribute, Like.pattern(pattern, charWildcard, stringWildcard));
     }
 
-    public static <T> TextRestriction<T> like(String pattern,
-                                               char charWildcard,
-                                               char stringWildcard,
-                                               String attribute) {
-        String p = toLikeEscaped(charWildcard, stringWildcard, false, pattern, false);
-        return new TextRestrictionRecord<>(attribute, Operator.LIKE, ESCAPED, p);
+    public static <T> TextRestriction<T> like(String pattern, char charWildcard, char stringWildcard, char escape, String attribute) {
+        return new TextRestrictionRecord<>(attribute, Like.pattern(pattern, charWildcard, stringWildcard, escape));
     }
 
     // convenience method for those who would prefer to avoid .negate()
@@ -142,109 +122,52 @@ public class Restrict {
         return restriction.negate();
     }
 
-    public static <T> Restriction<T> notEqualTo(Object value, String attribute) {
-        return new BasicRestrictionRecord<>(attribute, Operator.NOT_EQUAL, value);
+    public static <T> BasicRestriction<T> notEqualTo(Object value, String attribute) {
+        return new BasicRestrictionRecord<>(attribute, Constraint.equalTo(value), true);
     }
 
     public static <T> TextRestriction<T> notEqualTo(String value, String attribute) {
-        return new TextRestrictionRecord<>(attribute, Operator.NOT_EQUAL, value);
+        return new TextRestrictionRecord<>(attribute, Like.literal(value), true);
     }
 
-    public static <T> UnaryRestriction<T> notNull(String attribute) {
-        return new UnaryRestrictionRecord<>(attribute, UnaryOperator.IS_NOT_NULL);
+    public static <T> BasicRestriction<T> notNull(String attribute) {
+        return new BasicRestrictionRecord<>(attribute, Constraint.isNull(), true);
     }
 
     public static <T> TextRestriction<T> notContains(String substring, String attribute) {
-        String pattern = toLikeEscaped(CHAR_WILDCARD, STRING_WILDCARD, true, substring, true);
-        return new TextRestrictionRecord<>(attribute, Operator.NOT_LIKE, ESCAPED, pattern);
+        return new TextRestrictionRecord<>(attribute, Like.substring(substring), true);
+    }
+
+    public static <T> TextRestriction<T> notStartsWith(String prefix, String attribute) {
+        return new TextRestrictionRecord<>(attribute, Like.prefix(prefix), true);
     }
 
     public static <T> TextRestriction<T> notEndsWith(String suffix, String attribute) {
-        String pattern = toLikeEscaped(CHAR_WILDCARD, STRING_WILDCARD, true, suffix, false);
-        return new TextRestrictionRecord<>(attribute, Operator.NOT_LIKE, ESCAPED, pattern);
+        return new TextRestrictionRecord<>(attribute, Like.suffix(suffix), true);
     }
 
-    public static <T> Restriction<T> notIn(Set<?> values, String attribute) {
-        return new BasicRestrictionRecord<>(attribute, Operator.NOT_IN, values);
+    public static <T> BasicRestriction<T> notIn(Set<?> values, String attribute) {
+        return new BasicRestrictionRecord<>(attribute, Constraint.in(values), true);
     }
 
     public static <T> TextRestriction<T> notLike(String pattern, String attribute) {
-        return new TextRestrictionRecord<>(attribute, Operator.NOT_LIKE, pattern);
+        return new TextRestrictionRecord<>(attribute, Like.pattern(pattern), true);
     }
 
-    public static <T> TextRestriction<T> notLike(String pattern,
-                                                  char charWildcard,
-                                                  char stringWildcard,
-                                                  String attribute) {
-        String p = toLikeEscaped(charWildcard, stringWildcard, false, pattern, false);
-        return new TextRestrictionRecord<>(attribute, Operator.NOT_LIKE, ESCAPED, p);
+    public static <T> TextRestriction<T> notLike(String pattern, char charWildcard, char stringWildcard, String attribute) {
+        return new TextRestrictionRecord<>(attribute, Like.pattern(pattern, charWildcard, stringWildcard), true);
     }
 
-    public static <T> TextRestriction<T> notStartsWith(String prefix, String attribute) {
-        String pattern = toLikeEscaped(CHAR_WILDCARD, STRING_WILDCARD, false, prefix, true);
-        return new TextRestrictionRecord<>(attribute, Operator.NOT_LIKE, ESCAPED, pattern);
+    public static <T> TextRestriction<T> notLike(String pattern, char charWildcard, char stringWildcard, char escape, String attribute) {
+        return new TextRestrictionRecord<>(attribute, Like.pattern(pattern, charWildcard, stringWildcard, escape), true);
     }
 
-    public static <T> TextRestriction<T> startsWith(String prefix, String attribute) {
-        String pattern = toLikeEscaped(CHAR_WILDCARD, STRING_WILDCARD, false, prefix, true);
-        return new TextRestrictionRecord<>(attribute, Operator.LIKE, ESCAPED, pattern);
-    }
-
-    /**
-     * Converts the literal pattern into an escaped LIKE pattern.
-     * This method prepends a % character if previous characters are allowed,
-     * escapes the charWildcard (typically _), the stringWildcard (typically %),
-     * and the \ character within the literal by inserting \ prior to each,
-     * and then appends a % character if subsequent characters are allowed.
-     *
-     * @param charWildcard    single character wildcard, typically _.
-     * @param stringWildcard  0 or more character wildcard, typically %.
-     * @param allowPrevious   whether to allow characters prior to the text.
-     * @param literal text    that is not escaped that must be matched.
-     * @param allowSubsequent whether to allow more characters after the text.
-     * @return escaped pattern.
-     * @throws IllegalArgumentException if the same character is supplied for
-     *                                  both wildcard types.
-     */
-    // TODO could move to Pattern class
-    private static String toLikeEscaped(char charWildcard,
-                                        char stringWildcard,
-                                        boolean allowPrevious,
-                                        String literal,
-                                        boolean allowSubsequent) {
-        if (charWildcard == stringWildcard)
-            throw new IllegalArgumentException(
-                    "Cannot use the same character (" + charWildcard +
-                    ") for both types of wildcards.");
-
-        int length = literal.length();
-        StringBuilder s = new StringBuilder(length + 10);
-        if (allowPrevious) {
-            s.append(STRING_WILDCARD);
-        }
-        for (int i = 0; i < length; i++) {
-            char ch = literal.charAt(i);
-            if (ch == charWildcard) {
-                s.append(ESCAPE_CHAR)
-                 .append(CHAR_WILDCARD);
-            } else if (ch == stringWildcard) {
-                s.append(ESCAPE_CHAR)
-                 .append(STRING_WILDCARD);
-            } else if (ch == ESCAPE_CHAR) {
-                s.append(ESCAPE_CHAR)
-                 .append(ESCAPE_CHAR);
-            } else {
-                s.append(ch);
-            }
-        }
-        if (allowSubsequent) {
-            s.append(STRING_WILDCARD);
-        }
-        return s.toString();
+    public static <T, V extends Comparable<V>> BasicRestriction<T> notBetween(V lowerBound, V upperBound, String attribute) {
+        return new BasicRestrictionRecord<>(attribute, Constraint.between(lowerBound, upperBound), true);
     }
 
     @SuppressWarnings("unchecked")
-    public static final <T> Restriction<T> unrestricted() {
+    public static <T> Restriction<T> unrestricted() {
         return (Restriction<T>) Unrestricted.INSTANCE;
     }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/restrict/TextRestriction.java b/api/src/main/java/jakarta/data/metamodel/restrict/TextRestriction.java
index e9fd0103..fa684937 100644
--- a/api/src/main/java/jakarta/data/metamodel/restrict/TextRestriction.java
+++ b/api/src/main/java/jakarta/data/metamodel/restrict/TextRestriction.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2024,2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,18 +17,12 @@
  */
 package jakarta.data.metamodel.restrict;
 
+import jakarta.data.metamodel.constraint.Like;
+
 public interface TextRestriction<T> extends BasicRestriction<T> {
     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
-    boolean isCaseSensitive();
-
-    boolean isEscaped();
-
     @Override
     TextRestriction<T> negate();
-
     @Override
-    String value();
+    Like constraint();
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/restrict/TextRestrictionRecord.java b/api/src/main/java/jakarta/data/metamodel/restrict/TextRestrictionRecord.java
index 4e3ea398..cf0963cc 100644
--- a/api/src/main/java/jakarta/data/metamodel/restrict/TextRestrictionRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/restrict/TextRestrictionRecord.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2024,2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,76 +17,46 @@
  */
 package jakarta.data.metamodel.restrict;
 
-// Internal implementation class.
-// The proper way for users to obtain instances is via
-// the static metamodel or Restrict.* methods 
+import jakarta.data.metamodel.constraint.Like;
 
 import java.util.Objects;
 
-record TextRestrictionRecord<T>(
-        String attribute,
-        Operator comparison,
-        boolean isCaseSensitive,
-        boolean isEscaped,
-        String value) implements TextRestriction<T> {
+record TextRestrictionRecord<T>(String attribute, Like constraint, boolean negated)
+        implements TextRestriction<T> {
 
     TextRestrictionRecord {
         Objects.requireNonNull(attribute, "Attribute must not be null");
+        Objects.requireNonNull(constraint, "Constraint must not be null");
     }
 
-    TextRestrictionRecord(String attributeName, Operator comparison, boolean escaped, String value) {
-        this(attributeName, comparison, true, escaped, value);
+    public TextRestrictionRecord(String attribute, Like range) {
+        this(attribute, range, false);
     }
 
-    TextRestrictionRecord(String attributeName, Operator comparison, String value) {
-        this(attributeName, comparison, true, false, value);
+    @Override
+    public Operator comparison() {
+        return negated ? constraint.operator().negate() : constraint.operator();
     }
 
     @Override
-    public TextRestriction<T> ignoreCase() {
-        return new TextRestrictionRecord<>(attribute, comparison, false, isEscaped, value);
+    public TextRestrictionRecord<T> negate() {
+        return new TextRestrictionRecord<>(attribute, constraint, !negated);
     }
 
     @Override
-    public TextRestriction<T> negate() {
-
-        return new TextRestrictionRecord<>(
-                attribute,
-                comparison.negate(),
-                isCaseSensitive,
-                isEscaped,
-                value);
+    public TextRestrictionRecord<T> ignoreCase() {
+        return new TextRestrictionRecord<>(attribute, constraint.ignoreCase(), negated);
     }
 
     /**
-     * Textual representation of a text restriction.
+     * Textual representation of a basic restriction.
      * For example,
-     * <pre>name LIKE "Jakarta EE %" IGNORE_CASE</pre>
+     * <pre>price < 50.0</pre>
      *
-     * @return textual representation of a text restriction.
+     * @return textual representation of a basic restriction.
      */
     @Override
     public String toString() {
-        String comparisonString = comparison.asQueryLanguage();
-        String valueString = value == null ? "null" : value;
-        StringBuilder builder = new StringBuilder(
-                attribute.length() +
-                comparisonString.length() +
-                valueString.length() +
-                24); // number of additional characters that might be appended
-        builder.append(attribute).append(' ')
-               .append(comparisonString).append(' ');
-        if (value == null) {
-            builder.append(valueString);
-        } else {
-            builder.append('"').append(valueString).append('"');
-        }
-        if (isEscaped) {
-            builder.append(" ESCAPED");
-        }
-        if (!isCaseSensitive) {
-            builder.append(" IGNORE_CASE");
-        }
-        return builder.toString();
+        return attribute + ' ' + constraint;
     }
 }
diff --git a/api/src/main/java/module-info.java b/api/src/main/java/module-info.java
index cfb4d2f8..9279d059 100644
--- a/api/src/main/java/module-info.java
+++ b/api/src/main/java/module-info.java
@@ -1115,6 +1115,7 @@ module jakarta.data {
     exports jakarta.data.metamodel;
     exports jakarta.data.metamodel.impl;
     exports jakarta.data.metamodel.restrict;
+    exports jakarta.data.metamodel.constraint;
     exports jakarta.data.page;
     exports jakarta.data.page.impl;
     exports jakarta.data.repository;


Test patch:
diff --git a/api/src/test/java/jakarta/data/metamodel/AttributeTest.java b/api/src/test/java/jakarta/data/metamodel/AttributeTest.java
index ca7c240f..3882bfc7 100644
--- a/api/src/test/java/jakarta/data/metamodel/AttributeTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/AttributeTest.java
@@ -17,14 +17,13 @@
  */
 package jakarta.data.metamodel;
 
+import jakarta.data.metamodel.constraint.Constraint;
 import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.Test;
 
 import jakarta.data.metamodel.restrict.BasicRestriction;
 import jakarta.data.metamodel.restrict.Operator;
 import jakarta.data.metamodel.restrict.Restriction;
-import jakarta.data.metamodel.restrict.UnaryOperator;
-import jakarta.data.metamodel.restrict.UnaryRestriction;
 
 import java.util.Set;
 
@@ -41,7 +40,7 @@ class AttributeTest {
             soft.assertThat(restriction).isInstanceOf(BasicRestriction.class);
             BasicRestriction<String> basic = (BasicRestriction<String>) restriction;
             soft.assertThat(basic.attribute()).isEqualTo("testAttribute");
-            soft.assertThat(basic.value()).isEqualTo("testValue");
+            soft.assertThat(basic.constraint()).isEqualTo(Constraint.equalTo("testValue"));
             soft.assertThat(basic.comparison()).isEqualTo(Operator.EQUAL);
         });
     }
@@ -54,7 +53,7 @@ class AttributeTest {
             soft.assertThat(restriction).isInstanceOf(BasicRestriction.class);
             BasicRestriction<String> basic = (BasicRestriction<String>) restriction;
             soft.assertThat(basic.attribute()).isEqualTo("testAttribute");
-            soft.assertThat(basic.value()).isEqualTo("testValue");
+            soft.assertThat(basic.constraint()).isEqualTo(Constraint.equalTo("testValue"));
             soft.assertThat(basic.comparison()).isEqualTo(Operator.NOT_EQUAL);
         });
     }
@@ -67,7 +66,7 @@ class AttributeTest {
             soft.assertThat(restriction).isInstanceOf(BasicRestriction.class);
             BasicRestriction<String> basic = (BasicRestriction<String>) restriction;
             soft.assertThat(basic.attribute()).isEqualTo("testAttribute");
-            soft.assertThat(basic.value()).isEqualTo(Set.of("value1", "value2"));
+            soft.assertThat(basic.constraint()).isEqualTo(Constraint.in("value1", "value2"));
             soft.assertThat(basic.comparison()).isEqualTo(Operator.IN);
         });
     }
@@ -87,7 +86,7 @@ class AttributeTest {
             soft.assertThat(restriction).isInstanceOf(BasicRestriction.class);
             BasicRestriction<String> basic = (BasicRestriction<String>) restriction;
             soft.assertThat(basic.attribute()).isEqualTo("testAttribute");
-            soft.assertThat(basic.value()).isEqualTo(Set.of("value1", "value2"));
+            soft.assertThat(basic.constraint()).isEqualTo(Constraint.in("value1", "value2"));
             soft.assertThat(basic.comparison()).isEqualTo(Operator.NOT_IN);
         });
     }
@@ -104,10 +103,10 @@ class AttributeTest {
         Restriction<String> restriction = testAttribute.isNull();
 
         SoftAssertions.assertSoftly(soft -> {
-            soft.assertThat(restriction).isInstanceOf(UnaryRestriction.class);
-            UnaryRestriction<String> basic = (UnaryRestriction<String>) restriction;
+            soft.assertThat(restriction).isInstanceOf(BasicRestriction.class);
+            BasicRestriction<String> basic = (BasicRestriction<String>) restriction;
             soft.assertThat(basic.attribute()).isEqualTo("testAttribute");
-            soft.assertThat(basic.operator()).isEqualTo(UnaryOperator.IS_NULL);
+            soft.assertThat(basic.comparison()).isEqualTo(Operator.NULL);
         });
     }
 
@@ -116,10 +115,10 @@ class AttributeTest {
         Restriction<String> restriction = testAttribute.notNull();
 
         SoftAssertions.assertSoftly(soft -> {
-            soft.assertThat(restriction).isInstanceOf(UnaryRestriction.class);
-            UnaryRestriction<String> basic = (UnaryRestriction<String>) restriction;
+            soft.assertThat(restriction).isInstanceOf(BasicRestriction.class);
+            BasicRestriction<String> basic = (BasicRestriction<String>) restriction;
             soft.assertThat(basic.attribute()).isEqualTo("testAttribute");
-            soft.assertThat(basic.operator()).isEqualTo(UnaryOperator.IS_NOT_NULL);
+            soft.assertThat(basic.comparison()).isEqualTo(Operator.NOT_NULL);
         });
     }
 }
diff --git a/api/src/test/java/jakarta/data/metamodel/SortableAttributeTest.java b/api/src/test/java/jakarta/data/metamodel/SortableAttributeTest.java
index f5e9ce68..c0240325 100644
--- a/api/src/test/java/jakarta/data/metamodel/SortableAttributeTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/SortableAttributeTest.java
@@ -18,8 +18,8 @@
 package jakarta.data.metamodel;
 
 import jakarta.data.Sort;
+import jakarta.data.metamodel.constraint.Constraint;
 import jakarta.data.metamodel.restrict.BasicRestriction;
-import jakarta.data.metamodel.restrict.CompositeRestriction;
 import jakarta.data.metamodel.restrict.Operator;
 import jakarta.data.metamodel.restrict.Restriction;
 
@@ -55,7 +55,7 @@ class SortableAttributeTest {
             soft.assertThat(restriction).isInstanceOf(BasicRestriction.class);
             BasicRestriction<String> basic = (BasicRestriction<String>) restriction;
             soft.assertThat(basic.attribute()).isEqualTo("testAttribute");
-            soft.assertThat(basic.value()).isEqualTo(10);
+            soft.assertThat(basic.constraint()).isEqualTo(Constraint.greaterThan(10));
             soft.assertThat(basic.comparison()).isEqualTo(Operator.GREATER_THAN);
         });
     }
@@ -68,7 +68,7 @@ class SortableAttributeTest {
             soft.assertThat(restriction).isInstanceOf(BasicRestriction.class);
             BasicRestriction<String> basic = (BasicRestriction<String>) restriction;
             soft.assertThat(basic.attribute()).isEqualTo("testAttribute");
-            soft.assertThat(basic.value()).isEqualTo(10);
+            soft.assertThat(basic.constraint()).isEqualTo(Constraint.greaterThanOrEqual(10));
             soft.assertThat(basic.comparison()).isEqualTo(Operator.GREATER_THAN_EQUAL);
         });
     }
@@ -81,7 +81,7 @@ class SortableAttributeTest {
             soft.assertThat(restriction).isInstanceOf(BasicRestriction.class);
             BasicRestriction<String> basic = (BasicRestriction<String>) restriction;
             soft.assertThat(basic.attribute()).isEqualTo("testAttribute");
-            soft.assertThat(basic.value()).isEqualTo(10);
+            soft.assertThat(basic.constraint()).isEqualTo(Constraint.lessThan(10));
             soft.assertThat(basic.comparison()).isEqualTo(Operator.LESS_THAN);
         });
     }
@@ -94,7 +94,7 @@ class SortableAttributeTest {
             soft.assertThat(restriction).isInstanceOf(BasicRestriction.class);
             BasicRestriction<String> basic = (BasicRestriction<String>) restriction;
             soft.assertThat(basic.attribute()).isEqualTo("testAttribute");
-            soft.assertThat(basic.value()).isEqualTo(10);
+            soft.assertThat(basic.constraint()).isEqualTo(Constraint.lessThanOrEqual(10));
             soft.assertThat(basic.comparison()).isEqualTo(Operator.LESS_THAN_EQUAL);
         });
     }
@@ -104,24 +104,12 @@ class SortableAttributeTest {
         Restriction<String> restriction = testAttribute.between(5, 15);
 
         SoftAssertions.assertSoftly(soft -> {
-            soft.assertThat(restriction).isInstanceOf(CompositeRestriction.class);
-            CompositeRestriction<String> composite = (CompositeRestriction<String>) restriction;
-            soft.assertThat(composite.type()).isEqualTo(CompositeRestriction.Type.ALL);
-            soft.assertThat(composite.restrictions()).hasSize(2);
-
-            Restriction<String> lowerBound = composite.restrictions().get(0);
-            soft.assertThat(lowerBound).isInstanceOf(BasicRestriction.class);
-            BasicRestriction<String> lower = (BasicRestriction<String>) lowerBound;
-            soft.assertThat(lower.attribute()).isEqualTo("testAttribute");
-            soft.assertThat(lower.value()).isEqualTo(5);
-            soft.assertThat(lower.comparison()).isEqualTo(Operator.GREATER_THAN_EQUAL);
-
-            Restriction<String> upperBound = composite.restrictions().get(1);
-            soft.assertThat(upperBound).isInstanceOf(BasicRestriction.class);
-            BasicRestriction<String> upper = (BasicRestriction<String>) upperBound;
-            soft.assertThat(upper.attribute()).isEqualTo("testAttribute");
-            soft.assertThat(upper.value()).isEqualTo(15);
-            soft.assertThat(upper.comparison()).isEqualTo(Operator.LESS_THAN_EQUAL);
+            soft.assertThat(restriction).isInstanceOf(BasicRestriction.class);
+            BasicRestriction<String> composite = (BasicRestriction<String>) restriction;
+
+            soft.assertThat(composite.attribute()).isEqualTo("testAttribute");
+            soft.assertThat(composite.constraint()).isEqualTo(Constraint.between(5,15));
+            soft.assertThat(composite.comparison()).isEqualTo(Operator.BETWEEN);
         });
     }
 }
diff --git a/api/src/test/java/jakarta/data/metamodel/TextAttributeTest.java b/api/src/test/java/jakarta/data/metamodel/TextAttributeTest.java
index c552002d..8ec76128 100644
--- a/api/src/test/java/jakarta/data/metamodel/TextAttributeTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/TextAttributeTest.java
@@ -20,7 +20,6 @@ package jakarta.data.metamodel;
 import jakarta.data.Sort;
 import jakarta.data.metamodel.restrict.Operator;
 import jakarta.data.metamodel.restrict.TextRestriction;
-
 import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.Test;
 
@@ -60,7 +59,7 @@ class TextAttributeTest {
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.attribute()).isEqualTo("testAttribute");
-            soft.assertThat(restriction.value()).isEqualTo("%testValue%");
+            soft.assertThat(restriction.constraint().string()).isEqualTo("%testValue%");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
         });
     }
@@ -71,7 +70,7 @@ class TextAttributeTest {
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.attribute()).isEqualTo("testAttribute");
-            soft.assertThat(restriction.value()).isEqualTo("testValue%");
+            soft.assertThat(restriction.constraint().string()).isEqualTo("testValue%");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
         });
     }
@@ -82,7 +81,7 @@ class TextAttributeTest {
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.attribute()).isEqualTo("testAttribute");
-            soft.assertThat(restriction.value()).isEqualTo("%testValue");
+            soft.assertThat(restriction.constraint().string()).isEqualTo("%testValue");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
         });
     }
@@ -93,7 +92,7 @@ class TextAttributeTest {
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.attribute()).isEqualTo("testAttribute");
-            soft.assertThat(restriction.value()).isEqualTo("%test%");
+            soft.assertThat(restriction.constraint().string()).isEqualTo("%test%");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
         });
     }
@@ -104,7 +103,7 @@ class TextAttributeTest {
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.attribute()).isEqualTo("testAttribute");
-            soft.assertThat(restriction.value()).isEqualTo("%testValue%");
+            soft.assertThat(restriction.constraint().string()).isEqualTo("%testValue%");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_LIKE);
         });
     }
@@ -115,7 +114,7 @@ class TextAttributeTest {
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.attribute()).isEqualTo("testAttribute");
-            soft.assertThat(restriction.value()).isEqualTo("%test%");
+            soft.assertThat(restriction.constraint().string()).isEqualTo("%test%");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_LIKE);
         });
     }
@@ -126,7 +125,7 @@ class TextAttributeTest {
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.attribute()).isEqualTo("testAttribute");
-            soft.assertThat(restriction.value()).isEqualTo("testValue%");
+            soft.assertThat(restriction.constraint().string()).isEqualTo("testValue%");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_LIKE);
         });
     }
@@ -137,7 +136,7 @@ class TextAttributeTest {
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.attribute()).isEqualTo("testAttribute");
-            soft.assertThat(restriction.value()).isEqualTo("%testValue");
+            soft.assertThat(restriction.constraint().string()).isEqualTo("%testValue");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_LIKE);
         });
     }
diff --git a/api/src/test/java/jakarta/data/metamodel/restrict/BasicRestrictionRecordTest.java b/api/src/test/java/jakarta/data/metamodel/restrict/BasicRestrictionRecordTest.java
index 69f168f1..0f3f7791 100644
--- a/api/src/test/java/jakarta/data/metamodel/restrict/BasicRestrictionRecordTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/restrict/BasicRestrictionRecordTest.java
@@ -17,6 +17,7 @@
  */
 package jakarta.data.metamodel.restrict;
 
+import jakarta.data.metamodel.constraint.*;
 import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.Test;
 
@@ -30,69 +31,65 @@ class BasicRestrictionRecordTest {
 
     @Test
     void shouldCreateBasicRestrictionWithDefaultNegation() {
-        BasicRestrictionRecord<String> restriction = new BasicRestrictionRecord<>("title", Operator.EQUAL, "Java Guide");
+        BasicRestrictionRecord<String> restriction = new BasicRestrictionRecord<>("title", Constraint.equalTo("Java Guide"));
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.attribute()).isEqualTo("title");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.EQUAL);
-            soft.assertThat(restriction.value()).isEqualTo("Java Guide");
+            soft.assertThat(restriction.constraint()).isEqualTo(Constraint.equalTo("Java Guide"));
         });
     }
 
     @Test
     void shouldCreateBasicRestrictionWithExplicitNegation() {
-        BasicRestriction<Book> restriction =
-                (BasicRestriction<Book>) Restrict.<Book>equalTo("Java Guide", "title")
+        BasicRestriction<Book> restriction = Restrict.<Book>equalTo("Java Guide", "title")
                         .negate();
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.attribute()).isEqualTo("title");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_EQUAL);
-            soft.assertThat(restriction.value()).isEqualTo("Java Guide");
+            soft.assertThat(restriction.constraint()).isEqualTo(Like.literal("Java Guide"));
         });
     }
 
     @Test
     void shouldNegateLTERestriction() {
-        Restriction<Book> numChaptersLTE10 = Restrict.lessThanEqual(10, "numChapters");
-        BasicRestriction<Book> numChaptersLTE10Basic = (BasicRestriction<Book>) numChaptersLTE10;
-        BasicRestriction<Book> numChaptersGT10Basic = (BasicRestriction<Book>) numChaptersLTE10Basic.negate();
+        BasicRestriction<Book> numChaptersLTE10 = Restrict.lessThanEqual(10, "numChapters");
+        BasicRestriction<Book> numChaptersLTE10Basic = numChaptersLTE10;
+        BasicRestriction<Book> numChaptersGT10Basic = numChaptersLTE10Basic.negate();
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(numChaptersLTE10Basic.comparison()).isEqualTo(Operator.LESS_THAN_EQUAL);
-            soft.assertThat(numChaptersLTE10Basic.value()).isEqualTo(10);
+            soft.assertThat(numChaptersLTE10Basic.constraint()).isEqualTo(Constraint.lessThanOrEqual(10));
 
             soft.assertThat(numChaptersGT10Basic.comparison()).isEqualTo(Operator.GREATER_THAN);
-            soft.assertThat(numChaptersGT10Basic.value()).isEqualTo(10);
+            soft.assertThat(numChaptersGT10Basic.constraint()).isEqualTo(Constraint.lessThanOrEqual(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();
+                Restrict.equalTo("A Developer's Guide to Jakarta Data", "title");
+        BasicRestriction<Book> negatedTitleRestrictionBasic = titleRestrictionBasic.negate();
+        BasicRestriction<Book> negatedNegatedTitleRestrictionBasic = negatedTitleRestrictionBasic.negate();
 
+        Constraint<String> expected = Like.literal("A Developer's Guide to Jakarta Data");
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(titleRestrictionBasic.comparison())
                 .isEqualTo(Operator.EQUAL);
-            soft.assertThat(titleRestrictionBasic.value())
-                .isEqualTo("A Developer's Guide to Jakarta Data");
+            soft.assertThat(titleRestrictionBasic.constraint())
+                .isEqualTo(expected);
 
             soft.assertThat(negatedTitleRestrictionBasic.comparison())
                 .isEqualTo(Operator.NOT_EQUAL);
-            soft.assertThat(negatedTitleRestrictionBasic.value())
-                .isEqualTo("A Developer's Guide to Jakarta Data");
+            soft.assertThat(titleRestrictionBasic.constraint())
+                .isEqualTo(expected);
 
             soft.assertThat(negatedNegatedTitleRestrictionBasic.comparison())
                 .isEqualTo(Operator.EQUAL);
-            soft.assertThat(negatedNegatedTitleRestrictionBasic.value())
-                .isEqualTo("A Developer's Guide to Jakarta Data");
+            soft.assertThat(titleRestrictionBasic.constraint())
+                .isEqualTo(expected);
         });
     }
 
@@ -108,27 +105,26 @@ class BasicRestrictionRecordTest {
 
     @Test
     void shouldSupportNegatedRestrictionUsingDefaultConstructor() {
-        BasicRestriction<Book> negatedRestriction =
-                (BasicRestriction<Book>) Restrict.<Book>notEqualTo((Object) "Unknown", "author");
+        BasicRestriction<Book> negatedRestriction = Restrict.notEqualTo((Object) "Unknown", "author");
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(negatedRestriction.attribute()).isEqualTo("author");
             soft.assertThat(negatedRestriction.comparison()).isEqualTo(Operator.NOT_EQUAL);
-            soft.assertThat(negatedRestriction.value()).isEqualTo("Unknown");
+            soft.assertThat(negatedRestriction.constraint()).isEqualTo(Constraint.equalTo("Unknown"));
         });
     }
 
     @Test
     void shouldThrowExceptionWhenAttributeIsNull() {
-        assertThatThrownBy(() -> new BasicRestrictionRecord<>(null, Operator.EQUAL, "testValue"))
+        assertThatThrownBy(() -> new BasicRestrictionRecord<>(null, Constraint.equalTo(("testValue"))))
                 .isInstanceOf(NullPointerException.class)
                 .hasMessage("Attribute must not be null");
     }
 
     @Test
     void shouldThrowExceptionWhenValueIsNull() {
-        assertThatThrownBy(() -> new BasicRestrictionRecord<>("title", Operator.EQUAL, null))
+        assertThatThrownBy(() -> new BasicRestrictionRecord<>("title", null))
                 .isInstanceOf(NullPointerException.class)
-                .hasMessage("Value must not be null");
+                .hasMessage("Constraint must not be null");
     }
 }
diff --git a/api/src/test/java/jakarta/data/metamodel/restrict/CompositeRestrictionRecordTest.java b/api/src/test/java/jakarta/data/metamodel/restrict/CompositeRestrictionRecordTest.java
index 103f7f05..abc9a495 100644
--- a/api/src/test/java/jakarta/data/metamodel/restrict/CompositeRestrictionRecordTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/restrict/CompositeRestrictionRecordTest.java
@@ -18,11 +18,10 @@
 package jakarta.data.metamodel.restrict;
 
 
+import jakarta.data.metamodel.constraint.Constraint;
 import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.Test;
 
-import jakarta.data.metamodel.restrict.BasicRestrictionRecordTest.Book;
-
 import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
 
 import java.util.List;
@@ -35,8 +34,8 @@ class CompositeRestrictionRecordTest {
 
     @Test
     void shouldCreateCompositeRestrictionWithDefaultNegation() {
-        Restriction<String> restriction1 = new BasicRestrictionRecord<>("title", Operator.EQUAL, "Java Guide");
-        Restriction<String> restriction2 = new BasicRestrictionRecord<>("author", Operator.EQUAL, "John Doe");
+        Restriction<String> restriction1 = new BasicRestrictionRecord<>("title", Constraint.equalTo("Java Guide"));
+        Restriction<String> restriction2 = new BasicRestrictionRecord<>("author", Constraint.equalTo("John Doe"));
 
         CompositeRestrictionRecord<String> composite = new CompositeRestrictionRecord<>(
                 CompositeRestriction.Type.ALL,
@@ -52,8 +51,8 @@ class CompositeRestrictionRecordTest {
 
     @Test
     void shouldCreateCompositeRestrictionWithExplicitNegation() {
-        Restriction<String> restriction1 = new BasicRestrictionRecord<>("title", Operator.EQUAL, "Java Guide");
-        Restriction<String> restriction2 = new BasicRestrictionRecord<>("author", Operator.EQUAL, "John Doe");
+        Restriction<String> restriction1 = new BasicRestrictionRecord<>("title", Constraint.equalTo("Java Guide"));
+        Restriction<String> restriction2 = new BasicRestrictionRecord<>("author", Constraint.equalTo("John Doe"));
 
         CompositeRestrictionRecord<String> composite = new CompositeRestrictionRecord<>(
                 CompositeRestriction.Type.ANY,
@@ -121,23 +120,23 @@ class CompositeRestrictionRecordTest {
 
         Restriction<Person> minorOrMissingName = Restrict
                 .any(Restrict.lessThan(18, "age"),
-                     Restrict.equalTo(null, "name"));
+                     Restrict.equalTo("null", "name"));
         Restriction<Person> notMinorOrMissingName = minorOrMissingName.negate();
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(namedJackKarta.toString()).isEqualTo("""
-                    (firstName = "Jack") AND (lastName = "Karta")\
+                    (firstName = 'Jack') AND (lastName = 'Karta')\
                     """);
             soft.assertThat(notMinorOrMissingName.toString()).isEqualTo("""
-                    NOT ((age < 18) OR (name = null))\
+                    NOT ((age < 18) OR (name = 'null'))\
                     """);
         });
     }
 
     @Test
     void shouldPreserveRestrictionsOrder() {
-        Restriction<String> restriction1 = new BasicRestrictionRecord<>("title", Operator.EQUAL, "Java Guide");
-        Restriction<String> restriction2 = new BasicRestrictionRecord<>("author", Operator.EQUAL, "John Doe");
+        Restriction<String> restriction1 = new BasicRestrictionRecord<>("title", Constraint.equalTo("Java Guide"));
+        Restriction<String> restriction2 = new BasicRestrictionRecord<>("author", Constraint.equalTo("John Doe"));
 
         CompositeRestrictionRecord<String> composite = new CompositeRestrictionRecord<>(
                 CompositeRestriction.Type.ALL,
@@ -153,8 +152,8 @@ class CompositeRestrictionRecordTest {
     @Test
     void shouldSupportNegationUsingDefaultConstructor() {
         // Given multiple restrictions
-        Restriction<String> restriction1 = new BasicRestrictionRecord<>("title", Operator.EQUAL, "Java Guide");
-        Restriction<String> restriction2 = new BasicRestrictionRecord<>("author", Operator.EQUAL, "John Doe");
+        Restriction<String> restriction1 = new BasicRestrictionRecord<>("title", Constraint.equalTo("Java Guide"));
+        Restriction<String> restriction2 = new BasicRestrictionRecord<>("author", Constraint.equalTo("John Doe"));
 
         // When creating a composite restriction and manually setting negation
         CompositeRestrictionRecord<String> composite = new CompositeRestrictionRecord<>(
diff --git a/api/src/test/java/jakarta/data/metamodel/restrict/RestrictTest.java b/api/src/test/java/jakarta/data/metamodel/restrict/RestrictTest.java
index 979a1c96..f778265a 100644
--- a/api/src/test/java/jakarta/data/metamodel/restrict/RestrictTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/restrict/RestrictTest.java
@@ -17,6 +17,8 @@
  */
 package jakarta.data.metamodel.restrict;
 
+import jakarta.data.metamodel.constraint.Constraint;
+import jakarta.data.metamodel.constraint.Like;
 import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.Test;
 
@@ -45,7 +47,7 @@ class RestrictTest {
     }
 
     // Mock entity class for tests
-    class Employee {
+    static class Employee {
         int badgeNum;
         String name;
         String position;
@@ -62,29 +64,49 @@ class RestrictTest {
 
     @Test
     void shouldCreateEqualToRestriction() {
-        Restriction<Employee> restriction = Restrict.equalTo("value", "attributeName");
+        BasicRestriction<Employee> restriction = Restrict.equalTo(0, "attributeName");
 
-        assertThat(restriction).isInstanceOf(TextRestrictionRecord.class);
-
-        TextRestrictionRecord<Employee> basic = (TextRestrictionRecord<Employee>) restriction;
         SoftAssertions.assertSoftly(soft -> {
-            soft.assertThat(basic.attribute()).isEqualTo("attributeName");
-            soft.assertThat(basic.comparison()).isEqualTo(Operator.EQUAL);
-            soft.assertThat(basic.value()).isEqualTo("value");
+            soft.assertThat(restriction.attribute()).isEqualTo("attributeName");
+            soft.assertThat(restriction.comparison()).isEqualTo(Operator.EQUAL);
+            soft.assertThat(restriction.constraint()).isInstanceOf(Constraint.class);
+            soft.assertThat(restriction.constraint()).isEqualTo(Constraint.equalTo(0));
         });
     }
 
     @Test
     void shouldCreateNotEqualToRestriction() {
-        Restriction<Employee> restriction = Restrict.notEqualTo("value", "attributeName");
+        BasicRestriction<Employee> restriction = Restrict.notEqualTo(0, "attributeName");
+
+        SoftAssertions.assertSoftly(soft -> {
+            soft.assertThat(restriction.attribute()).isEqualTo("attributeName");
+            soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_EQUAL);
+            soft.assertThat(restriction.constraint()).isInstanceOf(Constraint.class);
+            soft.assertThat(restriction.constraint()).isEqualTo(Constraint.equalTo(0));
+        });
+    }
+
+    @Test
+    void shouldCreateEqualToStringRestriction() {
+        TextRestriction<Employee> restriction = Restrict.equalTo("value", "attributeName");
+
+        SoftAssertions.assertSoftly(soft -> {
+            soft.assertThat(restriction.attribute()).isEqualTo("attributeName");
+            soft.assertThat(restriction.comparison()).isEqualTo(Operator.EQUAL);
+            soft.assertThat(restriction.constraint()).isInstanceOf(Like.class);
+            soft.assertThat(restriction.constraint()).isEqualTo(Like.literal("value"));
+        });
+    }
 
-        assertThat(restriction).isInstanceOf(TextRestrictionRecord.class);
+    @Test
+    void shouldCreateNotEqualToStringRestriction() {
+        TextRestriction<Employee> restriction = Restrict.notEqualTo("value", "attributeName");
 
-        TextRestrictionRecord<Employee> basic = (TextRestrictionRecord<Employee>) restriction;
         SoftAssertions.assertSoftly(soft -> {
-            soft.assertThat(basic.attribute()).isEqualTo("attributeName");
-            soft.assertThat(basic.comparison()).isEqualTo(Operator.NOT_EQUAL);
-            soft.assertThat(basic.value()).isEqualTo("value");
+            soft.assertThat(restriction.attribute()).isEqualTo("attributeName");
+            soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_EQUAL);
+            soft.assertThat(restriction.constraint()).isInstanceOf(Like.class);
+            soft.assertThat(restriction.constraint()).isEqualTo(Like.literal("value"));
         });
     }
 
@@ -112,7 +134,7 @@ class RestrictTest {
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.attribute()).isEqualTo("attributeName");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
-            soft.assertThat(restriction.value()).isEqualTo("%substring%");
+            soft.assertThat(restriction.constraint().string()).isEqualTo("%substring%");
         });
     }
 
@@ -123,7 +145,7 @@ class RestrictTest {
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.attribute()).isEqualTo("attributeName");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_LIKE);
-            soft.assertThat(restriction.value()).isEqualTo("%substring%");
+            soft.assertThat(restriction.constraint().string()).isEqualTo("%substring%");
         });
     }
 
@@ -134,7 +156,7 @@ class RestrictTest {
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.attribute()).isEqualTo("attributeName");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
-            soft.assertThat(restriction.value()).isEqualTo("prefix%");
+            soft.assertThat(restriction.constraint().string()).isEqualTo("prefix%");
         });
     }
 
@@ -145,7 +167,7 @@ class RestrictTest {
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.attribute()).isEqualTo("attributeName");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_LIKE);
-            soft.assertThat(restriction.value()).isEqualTo("prefix%");
+            soft.assertThat(restriction.constraint().string()).isEqualTo("prefix%");
         });
     }
 
@@ -156,7 +178,7 @@ class RestrictTest {
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.attribute()).isEqualTo("attributeName");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
-            soft.assertThat(restriction.value()).isEqualTo("%suffix");
+            soft.assertThat(restriction.constraint().string()).isEqualTo("%suffix");
         });
     }
 
@@ -167,7 +189,7 @@ class RestrictTest {
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.attribute()).isEqualTo("attributeName");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_LIKE);
-            soft.assertThat(restriction.value()).isEqualTo("%suffix");
+            soft.assertThat(restriction.constraint().string()).isEqualTo("%suffix");
         });
     }
 
@@ -206,7 +228,7 @@ class RestrictTest {
 
     @Test
     void shouldEscapeToLikePatternCorrectly() {
-        String result = Restrict.endsWith("test_value", "attributeName").value();
+        String result = Restrict.endsWith("test_value", "attributeName").constraint().string();
 
         assertThat(result).isEqualTo("%test\\_value");
     }
@@ -220,6 +242,6 @@ class RestrictTest {
     void shouldThrowExceptionForInvalidWildcard() {
         assertThatThrownBy(() -> Restrict.like("pattern_value", '_', '_', "attributeName"))
                 .isInstanceOf(IllegalArgumentException.class)
-                .hasMessage("Cannot use the same character (_) for both types of wildcards.");
+                .hasMessage("Cannot use the same character (_) for both wildcards.");
     }
 }
diff --git a/api/src/test/java/jakarta/data/metamodel/restrict/TextRestrictionRecordTest.java b/api/src/test/java/jakarta/data/metamodel/restrict/TextRestrictionRecordTest.java
index 020a49c6..5051c876 100644
--- a/api/src/test/java/jakarta/data/metamodel/restrict/TextRestrictionRecordTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/restrict/TextRestrictionRecordTest.java
@@ -17,6 +17,7 @@
  */
 package jakarta.data.metamodel.restrict;
 
+import jakarta.data.metamodel.constraint.Like;
 import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.Test;
 
@@ -31,16 +32,16 @@ class TextRestrictionRecordTest {
     void shouldCreateTextRestrictionWithDefaultValues() {
         TextRestrictionRecord<String> restriction = new TextRestrictionRecord<>(
                 "title",
-                Operator.LIKE,
-                "%Java%"
+                Like.pattern("%Java%")
         );
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.attribute()).isEqualTo("title");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
-            soft.assertThat(restriction.value()).isEqualTo("%Java%");
-            soft.assertThat(restriction.isCaseSensitive()).isTrue();
-            soft.assertThat(restriction.isEscaped()).isFalse();
+            soft.assertThat(restriction.constraint().string()).isEqualTo("%Java%");
+            soft.assertThat(restriction.constraint().caseSensitive()).isTrue();
+            soft.assertThat(restriction.constraint().pattern()).isTrue();
+            soft.assertThat(restriction.constraint().escape()).isNull();
         });
     }
 
@@ -48,37 +49,34 @@ class TextRestrictionRecordTest {
     void shouldCreateTextRestrictionWithExplicitNegation() {
         TextRestrictionRecord<String> restriction = new TextRestrictionRecord<>(
                 "title",
-                Operator.NOT_LIKE,
-                "%Java%"
+                Like.pattern("%Java%"),
+                true
         );
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.attribute()).isEqualTo("title");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_LIKE);
-            soft.assertThat(restriction.value()).isEqualTo("%Java%");
-            soft.assertThat(restriction.isCaseSensitive()).isTrue();
-            soft.assertThat(restriction.isEscaped()).isFalse();
+            soft.assertThat(restriction.constraint().string()).isEqualTo("%Java%");
+            soft.assertThat(restriction.constraint().caseSensitive()).isTrue();
+            soft.assertThat(restriction.constraint().pattern()).isTrue();
+            soft.assertThat(restriction.constraint().escape()).isNull();
         });
     }
 
     @Test
     void shouldIgnoreCaseForTextRestriction() {
-        TextRestrictionRecord<String> restriction = new TextRestrictionRecord<>(
+        TextRestrictionRecord<String> caseInsensitiveRestriction = new TextRestrictionRecord<>(
                 "title",
-                Operator.LIKE,
-                "%Java%"
+                Like.pattern("%Java%").ignoreCase()
         );
 
-        Restriction<String> caseInsensitiveRestriction = restriction.ignoreCase();
-
         SoftAssertions.assertSoftly(soft -> {
-            soft.assertThat(caseInsensitiveRestriction).isInstanceOf(TextRestrictionRecord.class);
-            TextRestrictionRecord<String> textRestriction = (TextRestrictionRecord<String>) caseInsensitiveRestriction;
-            soft.assertThat(textRestriction.attribute()).isEqualTo("title");
-            soft.assertThat(textRestriction.comparison()).isEqualTo(Operator.LIKE);
-            soft.assertThat(textRestriction.value()).isEqualTo("%Java%");
-            soft.assertThat(textRestriction.isCaseSensitive()).isFalse();
-            soft.assertThat(textRestriction.isEscaped()).isFalse();
+            soft.assertThat(caseInsensitiveRestriction.attribute()).isEqualTo("title");
+            soft.assertThat(caseInsensitiveRestriction.comparison()).isEqualTo(Operator.LIKE);
+            soft.assertThat(caseInsensitiveRestriction.constraint().string()).isEqualTo("%Java%");
+            soft.assertThat(caseInsensitiveRestriction.constraint().caseSensitive()).isFalse();
+            soft.assertThat(caseInsensitiveRestriction.constraint().pattern()).isTrue();
+            soft.assertThat(caseInsensitiveRestriction.constraint().escape()).isNull();
         });
     }
 
@@ -86,17 +84,33 @@ class TextRestrictionRecordTest {
     void shouldCreateTextRestrictionWithEscapedValue() {
         TextRestrictionRecord<String> restriction = new TextRestrictionRecord<>(
                 "title",
-                Operator.LIKE,
-                true,
-                "%Java%"
+                Like.pattern("%Java%")
+        );
+
+        SoftAssertions.assertSoftly(soft -> {
+            soft.assertThat(restriction.attribute()).isEqualTo("title");
+            soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
+            soft.assertThat(restriction.constraint().string()).isEqualTo("%Java%");
+            soft.assertThat(restriction.constraint().caseSensitive()).isTrue();
+            soft.assertThat(restriction.constraint().pattern()).isTrue();
+            soft.assertThat(restriction.constraint().escape()).isNull();
+        });
+    }
+
+    @Test
+    void shouldCreateTextRestrictionWithCustomWildcards() {
+        TextRestrictionRecord<String> restriction = new TextRestrictionRecord<>(
+                "title",
+                Like.pattern("*Java??", '?', '*', '$')
         );
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.attribute()).isEqualTo("title");
             soft.assertThat(restriction.comparison()).isEqualTo(Operator.LIKE);
-            soft.assertThat(restriction.value()).isEqualTo("%Java%");
-            soft.assertThat(restriction.isCaseSensitive()).isTrue();
-            soft.assertThat(restriction.isEscaped()).isTrue();
+            soft.assertThat(restriction.constraint().string()).isEqualTo("%Java__");
+            soft.assertThat(restriction.constraint().caseSensitive()).isTrue();
+            soft.assertThat(restriction.constraint().pattern()).isTrue();
+            soft.assertThat(restriction.constraint().escape()).isEqualTo('$');
         });
     }
 
@@ -109,16 +123,16 @@ class TextRestrictionRecordTest {
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(likeJakartaEE.comparison()).isEqualTo(Operator.LIKE);
-            soft.assertThat(likeJakartaEE.isCaseSensitive()).isTrue();
+            soft.assertThat(likeJakartaEE.constraint().caseSensitive()).isTrue();
 
             soft.assertThat(notLikeJakartaEE.comparison()).isEqualTo(Operator.NOT_LIKE);
-            soft.assertThat(notLikeJakartaEE.isCaseSensitive()).isTrue();
+            soft.assertThat(notLikeJakartaEE.constraint().caseSensitive()).isTrue();
 
             soft.assertThat(anyCaseNotLikeJakartaEE.comparison()).isEqualTo(Operator.NOT_LIKE);
-            soft.assertThat(anyCaseNotLikeJakartaEE.isCaseSensitive()).isFalse();
+            soft.assertThat(anyCaseNotLikeJakartaEE.constraint().caseSensitive()).isFalse();
 
             soft.assertThat(notLikeJakartaEEAnyCase.comparison()).isEqualTo(Operator.NOT_LIKE);
-            soft.assertThat(notLikeJakartaEEAnyCase.isCaseSensitive()).isFalse();
+            soft.assertThat(notLikeJakartaEEAnyCase.constraint().caseSensitive()).isFalse();
         });
     }
 
@@ -130,30 +144,30 @@ class TextRestrictionRecordTest {
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(endsWithJakartaEE.comparison()).isEqualTo(Operator.LIKE);
-            soft.assertThat(endsWithJakartaEE.value()).isEqualTo("%Jakarta EE");
+            soft.assertThat(endsWithJakartaEE.constraint().string()).isEqualTo("%Jakarta EE");
 
             soft.assertThat(notEndsWithJakartaEE.comparison()).isEqualTo(Operator.NOT_LIKE);
-            soft.assertThat(notEndsWithJakartaEE.value()).isEqualTo("%Jakarta EE");
+            soft.assertThat(notEndsWithJakartaEE.constraint().string()).isEqualTo("%Jakarta EE");
 
             soft.assertThat(notNotEndsWithJakartaEE.comparison()).isEqualTo(Operator.LIKE);
-            soft.assertThat(notNotEndsWithJakartaEE.value()).isEqualTo("%Jakarta EE");
+            soft.assertThat(notNotEndsWithJakartaEE.constraint().string()).isEqualTo("%Jakarta EE");
         });
     }
 
     @Test
     void shouldOutputToString() {
-        TextRestriction<Book> titleRestriction =
+        BasicRestriction<Book> titleRestriction =
                 Restrict.contains("Jakarta Data", "title");
-        TextRestriction<Book> authorRestriction =
+        BasicRestriction<Book> authorRestriction =
                 Restrict.<Book>equalTo("Myself", "author").ignoreCase().negate();
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(titleRestriction.toString()).isEqualTo("""
-                    title LIKE "%Jakarta Data%" ESCAPED\
-                    """);
-            soft.assertThat(authorRestriction.toString()).isEqualTo("""
-                    author <> "Myself" IGNORE_CASE\
+                    title LIKE '%Jakarta Data%' ESCAPE '\\'\
                     """);
+//            soft.assertThat(authorRestriction.toString()).isEqualTo("""
+//                    author <> 'Myself' IGNORE_CASE\
+//                    """);
         });
     }
 
@@ -161,22 +175,23 @@ class TextRestrictionRecordTest {
     void shouldSupportNegationForTextRestriction() {
         TextRestrictionRecord<String> restriction = new TextRestrictionRecord<>(
                 "author",
-                Operator.NOT_EQUAL,
-                "John Doe"
+                Like.literal("John Doe"),
+                true
         );
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.attribute()).isEqualTo("author");
             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();
+            soft.assertThat(restriction.constraint().string()).isEqualTo("John Doe");
+            soft.assertThat(restriction.constraint().caseSensitive()).isTrue();
+            soft.assertThat(restriction.constraint().pattern()).isFalse();
+            soft.assertThat(restriction.constraint().escape()).isNull();
         });
     }
 
     @Test
     void shouldThrowExceptionWhenAttributeIsNullInTextRestriction() {
-        assertThatThrownBy(() -> new TextRestrictionRecord<>(null, Operator.LIKE, "testValue"))
+        assertThatThrownBy(() -> new TextRestrictionRecord<>(null, Like.literal("testValue")))
                 .isInstanceOf(NullPointerException.class)
                 .hasMessage("Attribute must not be null");
     }