Repository: jakartaee/data
Pull request #1061 (2025-04-05 20:25:17)

Restrictions that compare against Expressions
This PR tries out splitting BasicRestriction into one that compares against values (as it currently does) named ValueRestriction and another that compares against Expressions, named ExpressionRestriction.  To get this to work, I had to get around the requirement for the constraint values of LessThan, GreaterThan, and so on to be Comparable.  ComparableExpression is not currently Comparable, but to get this to work, I made it be, and that mostly worked. This might not be where we want to end up long term, but it could a temporary step on the way there. With this, I'm now able to write several unit tests that compare against Expressions and successfully preserve all of the information.

njr-11: I finished off the updates that are needed to make `ComparableExpression` be `Comparable` so that these expressions can be supplied as the constraint value to LessThan/GreaterThan, and so forth.  This didn't turn out as awkward as I thought and could be a valid solution for being able to have Restrictions that compare against other Attributes and Expressions, for example,
```
_Book.copyrightYear.greaterThan(_Book.firstPublishedYear);

_Book.currentPrice.lessThanEqual(_Book.discountPrice.times(1.1));
```

The alternatives that I thought of so far are:

- Remove the `extends Comparable<?>` from LessThan, GreaterThan, and other constraint subtypes
- Duplicate all of the LessThan/GreaterThan, ... constraint subtypes to allow a variant that builds around Expression instead of a Comparable value.  I really don't like this option.
- Get rid of Constraint and go back to separate Operator and value.

Maybe there are other solutions that I haven't thought of yet.
njr-11: Rebased and pushed updates to resolve various soft merge conflicts with #1060
This is ready for review again.

Patch:
diff --git a/api/src/main/java/jakarta/data/metamodel/ComparableExpression.java b/api/src/main/java/jakarta/data/metamodel/ComparableExpression.java
index e87be5aa..e920d951 100644
--- a/api/src/main/java/jakarta/data/metamodel/ComparableExpression.java
+++ b/api/src/main/java/jakarta/data/metamodel/ComparableExpression.java
@@ -33,41 +33,55 @@ public interface ComparableExpression<T,V extends Comparable<?>>
         return BasicRestriction.of(this, Between.bounds(min, max));
     }
 
-    default Restriction<T> notBetween(V min, V max) {
-        return BasicRestriction.of(this, NotBetween.bounds(min, max));
+    default <U extends ComparableExpression<? super T, V>> Restriction<T> between(
+        U minExpression,
+        U maxExpression) {
+        return BasicRestriction.of(
+                this,
+                Between.bounds(minExpression, maxExpression));
     }
 
     default Restriction<T> greaterThan(V value) {
         return BasicRestriction.of(this, GreaterThan.bound(value));
     }
 
+    default Restriction<T> greaterThan(ComparableExpression<? super T, V> expression) {
+        return BasicRestriction.of(this, GreaterThan.bound(expression));
+    }
+
     default Restriction<T> greaterThanEqual(V value) {
         return BasicRestriction.of(this, GreaterThanOrEqual.min(value));
     }
 
+    default Restriction<T> greaterThanEqual(ComparableExpression<? super T, V> expression) {
+        return BasicRestriction.of(this, GreaterThanOrEqual.min(expression));
+    }
+
     default Restriction<T> lessThan(V value) {
         return BasicRestriction.of(this, LessThan.bound(value));
     }
 
+    default Restriction<T> lessThan(ComparableExpression<? super T, V> expression) {
+        return BasicRestriction.of(this, LessThan.bound(expression));
+    }
+
     default Restriction<T> lessThanEqual(V value) {
         return BasicRestriction.of(this, LessThanOrEqual.max(value));
     }
 
-    // Leave for later, since we need a new kind of Restriction for these
+    default Restriction<T> lessThanEqual(ComparableExpression<? super T, V> expression) {
+        return BasicRestriction.of(this, LessThanOrEqual.max(expression));
+    }
 
-//    default Restriction<T> greaterThan(Expression<? super T,V> value) {
-//        throw new UnsupportedOperationException("not yet implemented");
-//    }
-//
-//    default Restriction<T> greaterThanEqual(Expression<? super T,V> value) {
-//        throw new UnsupportedOperationException("not yet implemented");
-//    }
-//
-//    default Restriction<T> lessThan(Expression<? super T,V> value) {
-//        throw new UnsupportedOperationException("not yet implemented");
-//    }
-//
-//    default Restriction<T> lessThanEqual(Expression<? super T,V> value) {
-//        throw new UnsupportedOperationException("not yet implemented");
-//    }
+    default Restriction<T> notBetween(V min, V max) {
+        return BasicRestriction.of(this, NotBetween.bounds(min, max));
+    }
+
+    default <U extends ComparableExpression<? super T, V>> Restriction<T> notBetween(
+            U minExpression,
+            U maxExpression) {
+        return BasicRestriction.of(
+                this,
+                NotBetween.bounds(minExpression, maxExpression));
+    }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/Expression.java b/api/src/main/java/jakarta/data/metamodel/Expression.java
index ab2c3b5e..345cf3d7 100644
--- a/api/src/main/java/jakarta/data/metamodel/Expression.java
+++ b/api/src/main/java/jakarta/data/metamodel/Expression.java
@@ -18,6 +18,7 @@
 package jakarta.data.metamodel;
 
 import jakarta.data.metamodel.constraint.Constraint;
+import jakarta.data.metamodel.constraint.EqualTo;
 import jakarta.data.metamodel.constraint.In;
 import jakarta.data.metamodel.constraint.NotEqualTo;
 import jakarta.data.metamodel.constraint.NotIn;
@@ -31,7 +32,11 @@ import java.util.Set;
 public interface Expression<T,V> {
 
     default Restriction<T> equalTo(V value) {
-        return BasicRestriction.of(this, Constraint.equalTo(value));
+        return BasicRestriction.of(this, EqualTo.value(value));
+    }
+
+    default Restriction<T> equalTo(Expression<? super T,V> expression) {
+        return BasicRestriction.of(this, EqualTo.expression(expression));
     }
 
     default Restriction<T> in(Set<V> values) {
@@ -41,6 +46,16 @@ public interface Expression<T,V> {
         return BasicRestriction.of(this, In.values(values));
     }
 
+    default Restriction<T> in(
+            @SuppressWarnings("unchecked") V... values) {
+        return BasicRestriction.of(this, In.values(values));
+    }
+
+    default Restriction<T> in(
+            @SuppressWarnings("unchecked") Expression<? super T,V>... expressions) {
+        return BasicRestriction.of(this, In.expressions(expressions));
+    }
+
     default Restriction<T> isNull() {
         return BasicRestriction.of(this, Null.instance());
     }
@@ -49,6 +64,10 @@ public interface Expression<T,V> {
         return BasicRestriction.of(this, NotEqualTo.value(value));
     }
 
+    default Restriction<T> notEqualTo(Expression<? super T,V> expression) {
+        return BasicRestriction.of(this, NotEqualTo.expression(expression));
+    }
+
     default Restriction<T> notIn(Set<V> values) {
         if (values == null || values.isEmpty())
             throw new IllegalArgumentException("values are required");
@@ -56,6 +75,16 @@ public interface Expression<T,V> {
         return BasicRestriction.of(this, NotIn.values(values));
     }
 
+    default Restriction<T> notIn(
+            @SuppressWarnings("unchecked") V... values) {
+        return BasicRestriction.of(this, NotIn.values(values));
+    }
+
+    default Restriction<T> notIn(
+            @SuppressWarnings("unchecked") Expression<? super T,V>... expressions) {
+        return BasicRestriction.of(this, NotIn.expressions(expressions));
+    }
+
     default Restriction<T> notNull() {
         return BasicRestriction.of(this, NotNull.instance());
     }
@@ -64,14 +93,4 @@ public interface Expression<T,V> {
     default Restriction<T> satisfies(Constraint<V> constraint) {
         return BasicRestriction.of(this, constraint);
     }
-
-    // Leave for later, since we need a new kind of Restriction for these
-
-//    default Restriction<T> equalTo(Expression<? super T,V> expression) {
-//        throw new UnsupportedOperationException("not yet implemented");
-//    }
-//
-//    default Restriction<T> notEqualTo(Expression<? super T,V> expression) {
-//        throw new UnsupportedOperationException("not yet implemented");
-//    }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/Between.java b/api/src/main/java/jakarta/data/metamodel/constraint/Between.java
index cff706c1..e26a5b50 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/Between.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/Between.java
@@ -17,12 +17,40 @@
  */
 package jakarta.data.metamodel.constraint;
 
-public interface Between<T extends Comparable<?>> extends Constraint<T> {
+import jakarta.data.metamodel.ComparableExpression;
+import jakarta.data.metamodel.expression.ComparableLiteral;
 
-    static <T extends Comparable<?>> Between<T> bounds(T lower, T upper) {
-        return new BetweenRecord<>(lower, upper);
+public interface Between<V extends Comparable<?>> extends Constraint<V> {
+
+    static <V extends Comparable<?>> Between<V> bounds(
+            V lower,
+            V upper) {
+        return new BetweenRecord<>(ComparableLiteral.of(lower),
+                                   ComparableLiteral.of(upper));
+    }
+
+    static <V extends Comparable<?>> Between<V> bounds(
+            V lower,
+            ComparableExpression<?, V> upper) {
+        return new BetweenRecord<>(ComparableLiteral.of(lower),
+                                   upper);
+    }
+
+    static <V extends Comparable<?>> Between<V> bounds(
+            ComparableExpression<?, V> lower,
+            V upper) {
+        return new BetweenRecord<>(lower,
+                                   ComparableLiteral.of(upper));
+    }
+
+    static <V extends Comparable<?>> Between<V> bounds(
+            ComparableExpression<?, V> lower,
+            ComparableExpression<?, V> upper) {
+        return new BetweenRecord<>(lower,
+                                   upper);
     }
 
-    T lowerBound();
-    T upperBound();
+    ComparableExpression<?, V> lowerBound();
+
+    ComparableExpression<?, V> 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
index 3a19e871..e81c7013 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/BetweenRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/BetweenRecord.java
@@ -17,10 +17,14 @@
  */
 package jakarta.data.metamodel.constraint;
 
+import jakarta.data.metamodel.ComparableExpression;
+
 import java.util.Objects;
 
-record BetweenRecord<T extends Comparable<?>>(T lowerBound, T upperBound)
-        implements Between<T> {
+record BetweenRecord<V extends Comparable<?>>(
+        ComparableExpression<?, V> lowerBound,
+        ComparableExpression<?, V> upperBound)
+        implements Between<V> {
     public BetweenRecord {
         Objects.requireNonNull(lowerBound);
         Objects.requireNonNull(upperBound);
@@ -32,7 +36,7 @@ record BetweenRecord<T extends Comparable<?>>(T lowerBound, T upperBound)
     }
 
     @Override
-    public NotBetween<T> negate() {
+    public NotBetween<V> negate() {
         return NotBetween.bounds(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
index c9d23b35..27418c01 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/Constraint.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/Constraint.java
@@ -19,33 +19,33 @@ package jakarta.data.metamodel.constraint;
 
 import java.util.Set;
 
-public interface Constraint<T> {
+public interface Constraint<V> {
 
-    Constraint<T> negate();
+    Constraint<V> negate();
 
-    static <T> EqualTo<T> equalTo(T value) {
+    static <V> EqualTo<V> equalTo(V value) {
         return EqualTo.value(value);
     }
 
-    static <T> NotEqualTo<T> notEqualTo(T value) {
+    static <V> NotEqualTo<V> notEqualTo(V value) {
         return NotEqualTo.value(value);
     }
 
     @SafeVarargs
-    static <T> In<T> in(T... values) {
+    static <V> In<V> in(V... values) {
         return In.values(values);
     }
 
-    static <T> In<T> in(Set<T> values) {
+    static <V> In<V> in(Set<V> values) {
         return In.values(values);
     }
 
     @SafeVarargs
-    static <T> NotIn<T> notIn(T... values) {
+    static <V> NotIn<V> notIn(V... values) {
         return NotIn.values(values);
     }
 
-    static <T> NotIn<T> notIn(Set<T> values) {
+    static <V> NotIn<V> notIn(Set<V> values) {
         return NotIn.values(values);
     }
 
@@ -73,35 +73,35 @@ public interface Constraint<T> {
         return NotLike.pattern(pattern, charWildcard, stringWildcard, escape);
     }
 
-    static <T> Null<T> isNull() {
+    static <V> Null<V> isNull() {
         return Null.instance();
     }
 
-    static <T> NotNull<T> notNull() {
+    static <V> NotNull<V> notNull() {
         return NotNull.instance();
     }
 
-    static <T extends Comparable<?>> GreaterThan<T> greaterThan(T bound) {
+    static <V extends Comparable<?>> GreaterThan<V> greaterThan(V bound) {
         return GreaterThan.bound(bound);
     }
 
-    static <T extends Comparable<?>> LessThan<T> lessThan(T bound) {
+    static <V extends Comparable<?>> LessThan<V> lessThan(V bound) {
         return LessThan.bound(bound);
     }
 
-    static <T extends Comparable<?>> GreaterThanOrEqual<T> greaterThanOrEqual(T bound) {
+    static <V extends Comparable<?>> GreaterThanOrEqual<V> greaterThanOrEqual(V bound) {
         return GreaterThanOrEqual.min(bound);
     }
 
-    static <T extends Comparable<?>> LessThanOrEqual<T> lessThanOrEqual(T bound) {
+    static <V extends Comparable<?>> LessThanOrEqual<V> lessThanOrEqual(V bound) {
         return LessThanOrEqual.max(bound);
     }
 
-    static <T extends Comparable<?>> Between<T> between(T lowerBound, T upperBound) {
+    static <V extends Comparable<?>> Between<V> between(V lowerBound, V upperBound) {
         return Between.bounds(lowerBound, upperBound);
     }
 
-    static <T extends Comparable<?>> NotBetween<T> notBetween(T lowerBound, T upperBound) {
+    static <V extends Comparable<?>> NotBetween<V> notBetween(V lowerBound, V upperBound) {
         return NotBetween.bounds(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
index 7a9db74f..a6e869b7 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/EqualTo.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/EqualTo.java
@@ -17,11 +17,18 @@
  */
 package jakarta.data.metamodel.constraint;
 
-public interface EqualTo<T> extends Constraint<T> {
+import jakarta.data.metamodel.Expression;
+import jakarta.data.metamodel.expression.Literal;
 
-    static <T> EqualTo<T> value(T value) {
-        return new EqualToRecord<>(value);
+public interface EqualTo<V> extends Constraint<V> {
+
+    static <V> EqualTo<V> expression(Expression<?, V> expression) {
+        return new EqualToRecord<>(expression);
+    }
+
+    static <V> EqualTo<V> value(V value) {
+        return new EqualToRecord<>(Literal.of(value));
     }
 
-    T value();
+    Expression<?, V> expression();
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/EqualToRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/EqualToRecord.java
index 519920f0..cabeaba8 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/EqualToRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/EqualToRecord.java
@@ -19,18 +19,21 @@ package jakarta.data.metamodel.constraint;
 
 import java.util.Objects;
 
-record EqualToRecord<T>(T value) implements EqualTo<T> {
+import jakarta.data.metamodel.Expression;
+
+record EqualToRecord<V>(Expression<?, V> expression)
+        implements EqualTo<V> {
     public EqualToRecord {
-        Objects.requireNonNull(value, "Value must not be null");
+        Objects.requireNonNull(expression, "Value expression must not be null");
     }
 
     @Override
-    public NotEqualTo<T> negate() {
-        return NotEqualTo.value(value);
+    public NotEqualTo<V> negate() {
+        return NotEqualTo.expression(expression);
     }
 
     @Override
     public String toString() {
-        return value instanceof String ? "= '" + value + "'" : "= " + value.toString();
+        return "= " + expression.toString();
     }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThan.java b/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThan.java
index c30bc78b..99604236 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThan.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThan.java
@@ -17,11 +17,20 @@
  */
 package jakarta.data.metamodel.constraint;
 
-public interface GreaterThan<T extends Comparable<?>> extends Constraint<T> {
+import jakarta.data.metamodel.ComparableExpression;
+import jakarta.data.metamodel.expression.ComparableLiteral;
 
-    static <T extends Comparable<?>> GreaterThan<T> bound(T lowerBound) {
+public interface GreaterThan<V extends Comparable<?>> extends Constraint<V> {
+
+    static <V extends Comparable<?>> GreaterThan<V> bound(
+            V lowerBound) {
+        return new GreaterThanRecord<>(ComparableLiteral.of(lowerBound));
+    }
+
+    static <V extends Comparable<?>> GreaterThan<V> bound(
+            ComparableExpression<?, V> lowerBound) {
         return new GreaterThanRecord<>(lowerBound);
     }
 
-    T bound();
+    ComparableExpression<?, V> 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
index a3d0dc84..8e04960e 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThanOrEqual.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThanOrEqual.java
@@ -17,11 +17,20 @@
  */
 package jakarta.data.metamodel.constraint;
 
-public interface GreaterThanOrEqual<T extends Comparable<?>> extends Constraint<T> {
+import jakarta.data.metamodel.ComparableExpression;
+import jakarta.data.metamodel.expression.ComparableLiteral;
 
-    static <T extends Comparable<?>> GreaterThanOrEqual<T> min(T minimum) {
+public interface GreaterThanOrEqual<V extends Comparable<?>> extends Constraint<V> {
+
+    static <V extends Comparable<?>> GreaterThanOrEqual<V> min(
+            V minimum) {
+        return new GreaterThanOrEqualRecord<>(ComparableLiteral.of(minimum));
+    }
+
+    static <V extends Comparable<?>> GreaterThanOrEqual<V> min(
+            ComparableExpression<?, V> minimum) {
         return new GreaterThanOrEqualRecord<>(minimum);
     }
 
-    T bound();
+    ComparableExpression<?, V> 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
index 4497ab20..377df529 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThanOrEqualRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThanOrEqualRecord.java
@@ -19,14 +19,17 @@ package jakarta.data.metamodel.constraint;
 
 import java.util.Objects;
 
-record GreaterThanOrEqualRecord<T extends Comparable<?>>(T bound)
-        implements GreaterThanOrEqual<T> {
+import jakarta.data.metamodel.ComparableExpression;
+
+record GreaterThanOrEqualRecord<V extends Comparable<?>>(
+        ComparableExpression<?, V> bound)
+        implements GreaterThanOrEqual<V> {
     public GreaterThanOrEqualRecord {
         Objects.requireNonNull(bound, "Lower bound must not be null");
     }
 
     @Override
-    public LessThan<T> negate() {
+    public LessThan<V> negate() {
         return LessThan.bound(bound);
     }
 
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThanRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThanRecord.java
index 9958e0e0..7929e1e4 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThanRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/GreaterThanRecord.java
@@ -19,14 +19,17 @@ package jakarta.data.metamodel.constraint;
 
 import java.util.Objects;
 
-record GreaterThanRecord<T extends Comparable<?>>(T bound)
-        implements GreaterThan<T> {
+import jakarta.data.metamodel.ComparableExpression;
+
+record GreaterThanRecord<V extends Comparable<?>>(
+        ComparableExpression<?, V> bound)
+        implements GreaterThan<V> {
     public GreaterThanRecord {
         Objects.requireNonNull(bound, "Lower bound must not be null");
     }
 
     @Override
-    public LessThanOrEqual<T> negate() {
+    public LessThanOrEqual<V> negate() {
         return LessThanOrEqual.max(bound);
     }
 
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/In.java b/api/src/main/java/jakarta/data/metamodel/constraint/In.java
index 37700490..50779b76 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/In.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/In.java
@@ -17,18 +17,77 @@
  */
 package jakarta.data.metamodel.constraint;
 
+import java.util.LinkedHashSet;
+import java.util.Objects;
 import java.util.Set;
 
-public interface In<T> extends Constraint<T> {
+import jakarta.data.metamodel.Expression;
+import jakarta.data.metamodel.expression.Literal;
+public interface In<V> extends Constraint<V> {
 
-    static <T> In<T> values(Set<T> values) {
-        return new InRecord<>(values);
+    @SafeVarargs
+    static <V> In<V> values(V... values) {
+        Objects.requireNonNull(values, "Values are required.");
+
+        if (values.length == 0) {
+            throw new IllegalArgumentException("Array of values must not be empty.");
+        }
+
+        Set<Expression<?, V>> expressions = new LinkedHashSet<>(values.length);
+        for (V value : values) {
+            Objects.requireNonNull(value, "Value must not be null.");
+            expressions.add(Literal.of(value));
+        }
+
+        return new InRecord<>(expressions);
+    }
+
+    static <V> In<V> values(Set<V> values) {
+        Objects.requireNonNull(values, "Values are required.");
+
+        if (values.isEmpty()) {
+            throw new IllegalArgumentException("Values must not be empty.");
+        }
+
+        Set<Expression<?, V>> expressions = new LinkedHashSet<>(values.size());
+        for (V value : values) {
+            Objects.requireNonNull(value, "Value must not be null.");
+            expressions.add(Literal.of(value));
+        }
+
+        return new InRecord<>(expressions);
+    }
+
+    static <V> In<V> expressions(Set<Expression<?, V>> expressions) {
+        Objects.requireNonNull(expressions, "Value expressions are required.");
+
+        if (expressions.isEmpty()) {
+            throw new IllegalArgumentException(
+                    "Value expressions must not be empty.");
+        }
+
+        for (Expression<?, V> expression : expressions) {
+            Objects.requireNonNull(expression, "Value expression must not be null.");
+        }
+
+        return new InRecord<>(expressions);
     }
 
     @SafeVarargs
-    static <T> In<T> values(T... values) {
-        return new InRecord<>(Set.of(values));
+    static <V> In<V> expressions(Expression<?, V>... expressions) {
+        Objects.requireNonNull(expressions, "Value expressions are required.");
+
+        if (expressions.length == 0) {
+            throw new IllegalArgumentException(
+                    "Array of value expressions must not be empty.");
+        }
+
+        for (Expression<?, V> expression : expressions) {
+            Objects.requireNonNull(expression, "Value expression must not be null.");
+        }
+
+        return new InRecord<>(Set.of(expressions));
     }
 
-    Set<T> values();
+    Set<Expression<?, V>> expressions();
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/InRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/InRecord.java
index c81b91e9..33a792aa 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/InRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/InRecord.java
@@ -17,25 +17,19 @@
  */
 package jakarta.data.metamodel.constraint;
 
-import java.util.Objects;
 import java.util.Set;
 
-record InRecord<T>(Set<T> values) implements In<T> {
+import jakarta.data.metamodel.Expression;
 
-    public InRecord {
-        Objects.requireNonNull(values, "Enumerated values cannot be null");
-        for (T value : values) {
-            Objects.requireNonNull(value, "value must not be null");
-        }
-    }
+record InRecord<V>(Set<Expression<?, V>> expressions) implements In<V> {
 
     @Override
-    public NotIn<T> negate() {
-        return NotIn.values(values);
+    public NotIn<V> negate() {
+        return NotIn.expressions(expressions);
     }
 
     @Override
     public String toString() {
-        return "IN " + values;
+        return "IN " + expressions;
     }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/LessThan.java b/api/src/main/java/jakarta/data/metamodel/constraint/LessThan.java
index a08f3f70..21e05179 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/LessThan.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/LessThan.java
@@ -17,11 +17,20 @@
  */
 package jakarta.data.metamodel.constraint;
 
-public interface LessThan<T extends Comparable<?>> extends Constraint<T> {
+import jakarta.data.metamodel.ComparableExpression;
+import jakarta.data.metamodel.expression.ComparableLiteral;
 
-    static <T extends Comparable<?>> LessThan<T> bound(T upperBound) {
+public interface LessThan<V extends Comparable<?>> extends Constraint<V> {
+
+    static <V extends Comparable<?>> LessThan<V> bound(
+            V upperBound) {
+        return new LessThanRecord<>(ComparableLiteral.of(upperBound));
+    }
+
+    static <V extends Comparable<?>> LessThan<V> bound(
+            ComparableExpression<?, V> upperBound) {
         return new LessThanRecord<>(upperBound);
     }
 
-    T bound();
+    ComparableExpression<?, V> 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
index 8dccc0f5..0377a119 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/LessThanOrEqual.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/LessThanOrEqual.java
@@ -17,11 +17,20 @@
  */
 package jakarta.data.metamodel.constraint;
 
-public interface LessThanOrEqual<T extends Comparable<?>> extends Constraint<T> {
+import jakarta.data.metamodel.ComparableExpression;
+import jakarta.data.metamodel.expression.ComparableLiteral;
 
-    static <T extends Comparable<?>> LessThanOrEqual<T> max(T maximum) {
+public interface LessThanOrEqual<V extends Comparable<?>> extends Constraint<V> {
+
+    static <V extends Comparable<?>> LessThanOrEqual<V> max(
+            V maximum) {
+        return new LessThanOrEqualRecord<>(ComparableLiteral.of(maximum));
+    }
+
+    static <V extends Comparable<?>> LessThanOrEqual<V> max(
+            ComparableExpression<?, V> maximum) {
         return new LessThanOrEqualRecord<>(maximum);
     }
 
-    T bound();
+    ComparableExpression<?, V> 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
index 5e13218f..fe4702fe 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/LessThanOrEqualRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/LessThanOrEqualRecord.java
@@ -19,14 +19,17 @@ package jakarta.data.metamodel.constraint;
 
 import java.util.Objects;
 
-record LessThanOrEqualRecord<T extends Comparable<?>>(T bound)
-        implements LessThanOrEqual<T> {
+import jakarta.data.metamodel.ComparableExpression;
+
+record LessThanOrEqualRecord<V extends Comparable<?>>(
+        ComparableExpression<?, V> bound)
+        implements LessThanOrEqual<V> {
     public LessThanOrEqualRecord {
         Objects.requireNonNull(bound, "Upper bound must not be null");
     }
 
     @Override
-    public GreaterThan<T> negate() {
+    public GreaterThan<V> negate() {
         return GreaterThan.bound(bound);
     }
 
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/LessThanRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/LessThanRecord.java
index 5bc692cf..6d91bbf9 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/LessThanRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/LessThanRecord.java
@@ -17,16 +17,18 @@
  */
 package jakarta.data.metamodel.constraint;
 
+import jakarta.data.metamodel.ComparableExpression;
+
 import java.util.Objects;
 
-record LessThanRecord<T extends Comparable<?>>(T bound)
-        implements LessThan<T> {
+record LessThanRecord<V extends Comparable<?>>(ComparableExpression<?, V> bound)
+        implements LessThan<V> {
     public LessThanRecord {
         Objects.requireNonNull(bound, "Upper bound must not be null");
     }
 
     @Override
-    public GreaterThanOrEqual<T> negate() {
+    public GreaterThanOrEqual<V> negate() {
         return GreaterThanOrEqual.min(bound);
     }
 
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/Like.java b/api/src/main/java/jakarta/data/metamodel/constraint/Like.java
index 03b4822b..6cbc9fbb 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/Like.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/Like.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2024 Contributors to the Eclipse Foundation
+ * Copyright (c) 2024,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.
@@ -19,36 +19,66 @@ package jakarta.data.metamodel.constraint;
 
 import static jakarta.data.metamodel.constraint.LikeRecord.ESCAPE;
 import static jakarta.data.metamodel.constraint.LikeRecord.STRING_WILDCARD;
+import static jakarta.data.metamodel.constraint.LikeRecord.translate;
+
+import java.util.Objects;
+
+import jakarta.data.metamodel.TextExpression;
+import jakarta.data.metamodel.expression.StringLiteral;
 
 public interface Like extends Constraint<String> {
-    String pattern();
-    Character escape();
 
     static Like pattern(String pattern) {
-        return new LikeRecord(pattern);
+        Objects.requireNonNull(pattern, "Pattern is required.");
+        StringLiteral<Object> expression = StringLiteral.of(pattern);
+        return new LikeRecord(expression, null);
     }
 
     static Like pattern(String pattern, char charWildcard, char stringWildcard) {
-        return new LikeRecord(pattern, charWildcard, stringWildcard);
+        return Like.pattern(pattern, charWildcard, stringWildcard, ESCAPE);
     }
 
     static Like pattern(String pattern, char charWildcard, char stringWildcard, char escape) {
-        return new LikeRecord(pattern, charWildcard, stringWildcard, escape);
+        Objects.requireNonNull(pattern, "Pattern is required.");
+        StringLiteral<Object> expression = StringLiteral.of(
+                translate(pattern, charWildcard, stringWildcard, escape));
+        return new LikeRecord(expression, escape);
     }
 
-    static Like substring(String substring) {
-        return new LikeRecord(STRING_WILDCARD + LikeRecord.escape(substring) + STRING_WILDCARD, ESCAPE);
+    static Like pattern(TextExpression<?> pattern, char escape) {
+        Objects.requireNonNull(pattern, "Pattern expression is required.");
+        return new LikeRecord(pattern, escape);
     }
 
     static Like prefix(String prefix) {
-        return new LikeRecord(LikeRecord.escape(prefix) + STRING_WILDCARD, ESCAPE);
+        Objects.requireNonNull(prefix, "Prefix is required.");
+        StringLiteral<Object> expression = StringLiteral.of(
+                LikeRecord.escape(prefix) + STRING_WILDCARD);
+        return new LikeRecord(expression, ESCAPE);
+    }
+
+    static Like substring(String substring) {
+        Objects.requireNonNull(substring, "Substring is required.");
+        StringLiteral<Object> expression = StringLiteral.of(
+                STRING_WILDCARD + LikeRecord.escape(substring) + STRING_WILDCARD);
+        return new LikeRecord(expression, ESCAPE);
     }
 
     static Like suffix(String suffix) {
-        return new LikeRecord(STRING_WILDCARD + LikeRecord.escape(suffix), ESCAPE);
+        Objects.requireNonNull(suffix, "Suffix is required.");
+        StringLiteral<Object> expression = StringLiteral.of(
+                STRING_WILDCARD + LikeRecord.escape(suffix));
+        return new LikeRecord(expression, ESCAPE);
     }
 
-    static Like literal(String suffix) {
-        return new LikeRecord(LikeRecord.escape(suffix), ESCAPE);
+    static Like literal(String value) {
+        Objects.requireNonNull(value, "Value is required.");
+        StringLiteral<Object> expression = StringLiteral.of(
+                LikeRecord.escape(value));
+        return new LikeRecord(expression, ESCAPE);
     }
+
+    Character escape();
+
+    TextExpression<?> pattern();
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/LikeRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/LikeRecord.java
index 82378b99..8ff77dcd 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/LikeRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/LikeRecord.java
@@ -17,30 +17,14 @@
  */
 package jakarta.data.metamodel.constraint;
 
-import java.util.Objects;
+import jakarta.data.metamodel.TextExpression;
 
-record LikeRecord(String pattern, Character escape)
+record LikeRecord(TextExpression<?> pattern, Character escape)
         implements Like {
 
-    public static final char CHAR_WILDCARD = '_';
-    public static final char STRING_WILDCARD = '%';
-    public static final char ESCAPE = '\\';
-
-    LikeRecord {
-        Objects.requireNonNull(pattern, "Pattern must not be null");
-    }
-
-    LikeRecord(String pattern) {
-        this(pattern, null);
-    }
-
-    LikeRecord(String pattern, char charWildcard, char stringWildcard) {
-        this(pattern, charWildcard, stringWildcard, ESCAPE);
-    }
-
-    LikeRecord(String pattern, char charWildcard, char stringWildcard, char escape) {
-        this(translate(pattern, charWildcard, stringWildcard, escape), escape);
-    }
+    static final char CHAR_WILDCARD = '_';
+    static final char STRING_WILDCARD = '%';
+    static final char ESCAPE = '\\';
 
     @Override
     public NotLike negate() {
@@ -49,8 +33,8 @@ record LikeRecord(String pattern, Character escape)
 
     @Override
     public String toString() {
-        return "LIKE '" + pattern + "'"
-                + (escape == null ? "" : " ESCAPE '\\'");
+        return "LIKE " + pattern +
+                (escape == null ? "" : " ESCAPE '" + escape + "'");
     }
 
     static String escape(String literal) {
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/NotBetween.java b/api/src/main/java/jakarta/data/metamodel/constraint/NotBetween.java
index e1780403..bad6325b 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/NotBetween.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/NotBetween.java
@@ -17,13 +17,38 @@
  */
 package jakarta.data.metamodel.constraint;
 
-public interface NotBetween<T extends Comparable<?>> extends Constraint<T> {
+import jakarta.data.metamodel.ComparableExpression;
+import jakarta.data.metamodel.expression.ComparableLiteral;
 
-    static <T extends Comparable<?>> NotBetween<T> bounds(T lower, T upper) {
-        return new NotBetweenRecord<>(lower, upper);
+public interface NotBetween<V extends Comparable<?>> extends Constraint<V> {
+
+    static <V extends Comparable<?>> NotBetween<V> bounds(V lower, V upper) {
+        return new NotBetweenRecord<>(ComparableLiteral.of(lower),
+                                      ComparableLiteral.of(upper));
+    }
+
+    static <V extends Comparable<?>> NotBetween<V> bounds(
+            V lower,
+            ComparableExpression<?, V> upper) {
+        return new NotBetweenRecord<>(ComparableLiteral.of(lower),
+                                      upper);
+    }
+
+    static <V extends Comparable<?>> NotBetween<V> bounds(
+            ComparableExpression<?, V> lower,
+            V upper) {
+        return new NotBetweenRecord<>(lower,
+                                      ComparableLiteral.of(upper));
+    }
+
+    static <V extends Comparable<?>> NotBetween<V> bounds(
+            ComparableExpression<?, V> lower,
+            ComparableExpression<?, V> upper) {
+        return new NotBetweenRecord<>(lower,
+                                      upper);
     }
 
-    T lowerBound();
+    ComparableExpression<?, V> lowerBound();
 
-    T upperBound();
+    ComparableExpression<?, V> upperBound();
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/NotBetweenRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/NotBetweenRecord.java
index 0b0b24b0..98eb3a5b 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/NotBetweenRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/NotBetweenRecord.java
@@ -19,8 +19,12 @@ package jakarta.data.metamodel.constraint;
 
 import java.util.Objects;
 
-record NotBetweenRecord<T extends Comparable<?>>(T lowerBound, T upperBound)
-    implements NotBetween<T> {
+import jakarta.data.metamodel.ComparableExpression;
+
+record NotBetweenRecord<V extends Comparable<?>>(
+        ComparableExpression<?, V> lowerBound, 
+        ComparableExpression<?, V> upperBound)
+    implements NotBetween<V> {
 
     NotBetweenRecord {
         Objects.requireNonNull(lowerBound, "lowerBound must not be null");
@@ -28,14 +32,12 @@ record NotBetweenRecord<T extends Comparable<?>>(T lowerBound, T upperBound)
     }
 
     @Override
-    public Between<T> negate() {
+    public Between<V> negate() {
         return Between.bounds(lowerBound, upperBound);
     }
 
     @Override
     public String toString() {
-        return lowerBound instanceof String
-                ? "NOT BETWEEN '" + lowerBound + "' AND '" + upperBound + "'"
-                : "NOT BETWEEN " + lowerBound + " AND " + upperBound;
+        return "NOT BETWEEN " + lowerBound + " AND " + upperBound;
     }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/NotEqualTo.java b/api/src/main/java/jakarta/data/metamodel/constraint/NotEqualTo.java
index 37b48435..449c136d 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/NotEqualTo.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/NotEqualTo.java
@@ -17,11 +17,18 @@
  */
 package jakarta.data.metamodel.constraint;
 
-public interface NotEqualTo<T> extends Constraint<T> {
+import jakarta.data.metamodel.Expression;
+import jakarta.data.metamodel.expression.Literal;
 
-    static <T> NotEqualTo<T> value(T value) {
-        return new NotEqualToRecord<>(value);
+public interface NotEqualTo<V> extends Constraint<V> {
+
+    static <V> NotEqualTo<V> expression(Expression<?, V> expression) {
+        return new NotEqualToRecord<>(expression);
+    }
+
+    static <V> NotEqualTo<V> value(V value) {
+        return new NotEqualToRecord<>(Literal.of(value));
     }
 
-    T value();
+    Expression<?, V> expression();
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/NotEqualToRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/NotEqualToRecord.java
index 74e827e0..a75006a6 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/NotEqualToRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/NotEqualToRecord.java
@@ -19,19 +19,23 @@ package jakarta.data.metamodel.constraint;
 
 import java.util.Objects;
 
-record NotEqualToRecord<T>(T value) implements NotEqualTo<T> {
+import jakarta.data.metamodel.Expression;
+
+record NotEqualToRecord<V>(
+        Expression<?, V> expression)
+        implements NotEqualTo<V> {
 
     NotEqualToRecord {
-        Objects.requireNonNull(value, "value must not be null");
+        Objects.requireNonNull(expression, "Value expression must not be null");
     }
 
     @Override
-    public EqualTo<T> negate() {
-        return EqualTo.value(value);
+    public EqualTo<V> negate() {
+        return EqualTo.expression(expression);
     }
 
     @Override
     public String toString() {
-        return value instanceof String ? "<> '" + value + "'" : "<> " + value;
+        return "<> " + expression;
     }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/NotIn.java b/api/src/main/java/jakarta/data/metamodel/constraint/NotIn.java
index b774a145..e043f1a9 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/NotIn.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/NotIn.java
@@ -17,18 +17,77 @@
  */
 package jakarta.data.metamodel.constraint;
 
+import java.util.LinkedHashSet;
+import java.util.Objects;
 import java.util.Set;
 
-public interface NotIn<T> extends Constraint<T> {
+import jakarta.data.metamodel.Expression;
+import jakarta.data.metamodel.expression.Literal;
 
-    static <T> NotIn<T> values(Set<T> values) {
-        return new NotInRecord<>(values);
+public interface NotIn<V> extends Constraint<V> {
+
+    @SafeVarargs
+    static <V> NotIn<V> values(V... values) {
+        Objects.requireNonNull(values, "Values are required.");
+
+        if (values.length == 0) {
+            throw new IllegalArgumentException("Array of values must not be empty.");
+        }
+
+        Set<Expression<?, V>> expressions = new LinkedHashSet<>(values.length);
+        for (V value : values) {
+            Objects.requireNonNull(value, "Value must not be null.");
+            expressions.add(Literal.of(value));
+        }
+
+        return new NotInRecord<>(expressions);
+    }
+
+    static <V> NotIn<V> values(Set<V> values) {
+        Objects.requireNonNull(values, "Values are required.");
+
+        if (values.isEmpty()) {
+            throw new IllegalArgumentException("Values must not be empty.");
+        }
+
+        Set<Expression<?, V>> expressions = new LinkedHashSet<>(values.size());
+        for (V value : values) {
+            Objects.requireNonNull(value, "Value must not be null.");
+            expressions.add(Literal.of(value));
+        }
+
+        return new NotInRecord<>(expressions);
+    }
+
+    static <V> NotIn<V> expressions(Set<Expression<?, V>> expressions) {
+        Objects.requireNonNull(expressions, "Value expressions are required.");
+
+        if (expressions.isEmpty()) {
+            throw new IllegalArgumentException("Value expressions must not be empty.");
+        }
+
+        for (Expression<?, V> expression : expressions) {
+            Objects.requireNonNull(expression, "Value expression must not be null.");
+        }
+
+        return new NotInRecord<>(expressions);
     }
 
     @SafeVarargs
-    static <T> NotIn<T> values(T... values) {
-        return new NotInRecord<>(Set.of(values));
+    static <V> NotIn<V> expressions(Expression<?, V>... expressions) {
+        Objects.requireNonNull(expressions, "Value expressions are required.");
+
+        if (expressions.length == 0) {
+            throw new IllegalArgumentException(
+                    "Array of value expressions must not be empty.");
+        }
+
+        for (Expression<?, V> expression : expressions) {
+            Objects.requireNonNull(expression, "Value expression must not be null.");
+        }
+
+        return new NotInRecord<>(Set.of(expressions));
     }
 
-    Set<T> values();
+    Set<Expression<?, V>> expressions();
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/NotInRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/NotInRecord.java
index 27b9b1d2..9022f923 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/NotInRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/NotInRecord.java
@@ -17,25 +17,19 @@
  */
 package jakarta.data.metamodel.constraint;
 
-import java.util.Objects;
 import java.util.Set;
 
-record NotInRecord<T>(Set<T> values) implements NotIn<T> {
+import jakarta.data.metamodel.Expression;
 
-    NotInRecord {
-        Objects.requireNonNull(values, "values must not be null");
-        for (T value : values) {
-            Objects.requireNonNull(value, "value must not be null");
-        }
-    }
+record NotInRecord<V>(Set<Expression<?, V>> expressions) implements NotIn<V> {
 
     @Override
-    public In<T> negate() {
-        return In.values(values);
+    public In<V> negate() {
+        return In.expressions(expressions);
     }
 
     @Override
     public String toString() {
-        return "NOT IN " + values;
+        return "NOT IN " + expressions;
     }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/NotLike.java b/api/src/main/java/jakarta/data/metamodel/constraint/NotLike.java
index 272a992b..34fb4dbb 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/NotLike.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/NotLike.java
@@ -19,38 +19,66 @@ package jakarta.data.metamodel.constraint;
 
 import static jakarta.data.metamodel.constraint.LikeRecord.ESCAPE;
 import static jakarta.data.metamodel.constraint.LikeRecord.STRING_WILDCARD;
+import static jakarta.data.metamodel.constraint.LikeRecord.translate;
+
+import java.util.Objects;
+
+import jakarta.data.metamodel.TextExpression;
+import jakarta.data.metamodel.expression.StringLiteral;
 
 public interface NotLike extends Constraint<String> {
 
     static NotLike pattern(String pattern) {
-        return new NotLikeRecord(pattern);
+        Objects.requireNonNull(pattern, "Pattern is required.");
+        StringLiteral<Object> expression = StringLiteral.of(pattern);
+        return new NotLikeRecord(expression, null);
     }
 
     static NotLike pattern(String pattern, char charWildcard, char stringWildcard) {
-        return new NotLikeRecord(pattern, charWildcard, stringWildcard);
+        return NotLike.pattern(pattern, charWildcard, stringWildcard, ESCAPE);
     }
 
     static NotLike pattern(String pattern, char charWildcard, char stringWildcard, char escape) {
-        return new NotLikeRecord(pattern, charWildcard, stringWildcard, escape);
+        Objects.requireNonNull(pattern, "Pattern is required.");
+        StringLiteral<Object> expression = StringLiteral.of(
+                translate(pattern, charWildcard, stringWildcard, escape));
+        return new NotLikeRecord(expression, escape);
+    }
+
+    static NotLike pattern(TextExpression<?> pattern, char escape) {
+        Objects.requireNonNull(pattern, "Pattern expression is required.");
+        return new NotLikeRecord(pattern, escape);
     }
 
     static NotLike prefix(String prefix) {
-        return new NotLikeRecord(LikeRecord.escape(prefix) + STRING_WILDCARD, ESCAPE);
+        Objects.requireNonNull(prefix, "Prefix is required.");
+        StringLiteral<Object> expression = StringLiteral.of(
+                LikeRecord.escape(prefix) + STRING_WILDCARD);
+        return new NotLikeRecord(expression, ESCAPE);
     }
 
     static NotLike substring(String substring) {
-        return new NotLikeRecord(STRING_WILDCARD + LikeRecord.escape(substring) + STRING_WILDCARD, ESCAPE);
+        Objects.requireNonNull(substring, "Substring is required.");
+        StringLiteral<Object> expression = StringLiteral.of(
+                STRING_WILDCARD + LikeRecord.escape(substring) + STRING_WILDCARD);
+        return new NotLikeRecord(expression, ESCAPE);
     }
 
     static NotLike suffix(String suffix) {
-        return new NotLikeRecord(STRING_WILDCARD + LikeRecord.escape(suffix), ESCAPE);
+        Objects.requireNonNull(suffix, "Suffix is required.");
+        StringLiteral<Object> expression = StringLiteral.of(
+                STRING_WILDCARD + LikeRecord.escape(suffix));
+        return new NotLikeRecord(expression, ESCAPE);
     }
 
-    static NotLike literal(String suffix) {
-        return new NotLikeRecord(LikeRecord.escape(suffix), ESCAPE);
+    static NotLike literal(String value) {
+        Objects.requireNonNull(value, "Value is required.");
+        StringLiteral<Object> expression = StringLiteral.of(
+                LikeRecord.escape(value));
+        return new NotLikeRecord(expression, ESCAPE);
     }
 
     Character escape();
 
-    String pattern();
+    TextExpression<?> pattern();
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/NotLikeRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/NotLikeRecord.java
index 9dcaf2fd..a6d31605 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/NotLikeRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/NotLikeRecord.java
@@ -17,28 +17,10 @@
  */
 package jakarta.data.metamodel.constraint;
 
-import java.util.Objects;
+import jakarta.data.metamodel.TextExpression;
 
-import static jakarta.data.metamodel.constraint.LikeRecord.ESCAPE;
-import static jakarta.data.metamodel.constraint.LikeRecord.translate;
-
-record NotLikeRecord(String pattern, Character escape) implements NotLike {
-
-    NotLikeRecord {
-        Objects.requireNonNull(pattern, "pattern must not be null");
-    }
-
-    NotLikeRecord(String pattern) {
-        this(pattern, null);
-    }
-
-    NotLikeRecord(String pattern, char charWildcard, char stringWildcard) {
-        this(pattern, charWildcard, stringWildcard, ESCAPE);
-    }
-
-    NotLikeRecord(String pattern, char charWildcard, char stringWildcard, char escape) {
-        this(translate(pattern, charWildcard, stringWildcard, escape), escape);
-    }
+record NotLikeRecord(TextExpression<?> pattern, Character escape)
+        implements NotLike {
 
     @Override
     public Like negate() {
@@ -47,7 +29,7 @@ record NotLikeRecord(String pattern, Character escape) implements NotLike {
 
     @Override
     public String toString() {
-        return "NOT LIKE '" + pattern + "'" +
-               (escape == null ? "" : " ESCAPE '\\'");
+        return "NOT LIKE " + pattern +
+                (escape == null ? "" : " ESCAPE '" + escape + "'");
     }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/NotNull.java b/api/src/main/java/jakarta/data/metamodel/constraint/NotNull.java
index 5675c8e3..1ce7b9b1 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/NotNull.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/NotNull.java
@@ -17,10 +17,10 @@
  */
 package jakarta.data.metamodel.constraint;
 
-public interface NotNull<T> extends Constraint<T> {
+public interface NotNull<V> extends Constraint<V> {
 
     @SuppressWarnings("unchecked")
-    static <T> NotNull<T> instance() {
-        return (NotNull<T>) NotNullRecord.INSTANCE;
+    static <V> NotNull<V> instance() {
+        return (NotNull<V>) NotNullRecord.INSTANCE;
     }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/NotNullRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/NotNullRecord.java
index bd9693ee..bdc3e885 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/NotNullRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/NotNullRecord.java
@@ -17,11 +17,11 @@
  */
 package jakarta.data.metamodel.constraint;
 
-record NotNullRecord<T>() implements NotNull<T> {
+record NotNullRecord<V>() implements NotNull<V> {
     static final NotNullRecord<?> INSTANCE = new NotNullRecord<>();
 
     @Override
-    public Null<T> negate() {
+    public Null<V> negate() {
         return Null.instance();
     }
 
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/Null.java b/api/src/main/java/jakarta/data/metamodel/constraint/Null.java
index aa2303e8..73d4375d 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/Null.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/Null.java
@@ -17,10 +17,10 @@
  */
 package jakarta.data.metamodel.constraint;
 
-public interface Null<T> extends Constraint<T> {
+public interface Null<V> extends Constraint<V> {
 
     @SuppressWarnings("unchecked")
-    static <T> Null<T> instance() {
-        return (Null<T>) NullRecord.INSTANCE;
+    static <V> Null<V> instance() {
+        return (Null<V>) NullRecord.INSTANCE;
     }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/constraint/NullRecord.java b/api/src/main/java/jakarta/data/metamodel/constraint/NullRecord.java
index 7b157b78..0a417a7a 100644
--- a/api/src/main/java/jakarta/data/metamodel/constraint/NullRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/constraint/NullRecord.java
@@ -17,11 +17,11 @@
  */
 package jakarta.data.metamodel.constraint;
 
-record NullRecord<T>() implements Null<T> {
+record NullRecord<V>() implements Null<V> {
     static final NullRecord<?> INSTANCE = new NullRecord<>();
 
     @Override
-    public NotNull<T> negate() {
+    public NotNull<V> negate() {
         return NotNull.instance();
     }
 
diff --git a/api/src/main/java/jakarta/data/metamodel/expression/ComparableLiteral.java b/api/src/main/java/jakarta/data/metamodel/expression/ComparableLiteral.java
new file mode 100644
index 00000000..f136090e
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/expression/ComparableLiteral.java
@@ -0,0 +1,68 @@
+/*
+ * 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.expression;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+
+import jakarta.data.metamodel.ComparableExpression;
+
+public interface ComparableLiteral<T, V extends Comparable<?>>
+        extends ComparableExpression<T,V>, Literal<T, V> {
+
+    @SuppressWarnings("unchecked")
+    static <T, V extends Comparable<?>> ComparableLiteral<T, V> of(V value) {
+        // Subtypes of Number and Temporal are needed here because
+        // NumericExperssion has N extends Number & Comparable<N>
+        // and
+        // TemporalExpression has V extends Temporal & Comparable<? extends Temporal>
+        if (value instanceof String s) {
+            return (ComparableLiteral<T, V>) StringLiteral.of(s);
+        } else if (value instanceof Integer i) {
+            return (ComparableLiteral<T, V>) NumericLiteral.of(i);
+        } else if (value instanceof Long l) {
+            return (ComparableLiteral<T, V>) NumericLiteral.of(l);
+        } else if (value instanceof Float f) {
+            return (ComparableLiteral<T, V>) NumericLiteral.of(f);
+        } else if (value instanceof Double d) {
+            return (ComparableLiteral<T, V>) NumericLiteral.of(d);
+        } else if (value instanceof Byte b) {
+            return (ComparableLiteral<T, V>) NumericLiteral.of(b);
+        } else if (value instanceof Short s) {
+            return (ComparableLiteral<T, V>) NumericLiteral.of(s);
+        } else if (value instanceof BigInteger i) {
+            return (ComparableLiteral<T, V>) NumericLiteral.of(i);
+        } else if (value instanceof BigDecimal d) {
+            return (ComparableLiteral<T, V>) NumericLiteral.of(d);
+        } else if (value instanceof Instant i) {
+            return (ComparableLiteral<T, V>) TemporalLiteral.of(i);
+        } else if (value instanceof LocalDate d) {
+            return (ComparableLiteral<T, V>) TemporalLiteral.of(d);
+        } else if (value instanceof LocalDateTime d) {
+            return (ComparableLiteral<T, V>) TemporalLiteral.of(d);
+        } else if (value instanceof LocalTime t) {
+            return (ComparableLiteral<T, V>) TemporalLiteral.of(t);
+        } else {
+            return new ComparableLiteralRecord<>(value);
+        }
+    }
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/expression/ComparableLiteralRecord.java b/api/src/main/java/jakarta/data/metamodel/expression/ComparableLiteralRecord.java
new file mode 100644
index 00000000..e95483c0
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/expression/ComparableLiteralRecord.java
@@ -0,0 +1,34 @@
+/*
+ * 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.expression;
+
+import java.util.Objects;
+
+record ComparableLiteralRecord<T, V extends Comparable<?>>(V value)
+        implements ComparableLiteral<T,V> {
+
+    ComparableLiteralRecord {
+        Objects.requireNonNull(value, "Value is required.");
+    }
+
+    @Override
+    public String toString() {
+        return value.toString();
+    }
+
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/expression/Literal.java b/api/src/main/java/jakarta/data/metamodel/expression/Literal.java
new file mode 100644
index 00000000..1c74c88b
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/expression/Literal.java
@@ -0,0 +1,34 @@
+/*
+ * 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.expression;
+
+import jakarta.data.metamodel.Expression;
+
+public interface Literal<T, V> extends Expression<T, V> {
+    V value();
+
+    @SuppressWarnings("unchecked")
+    static <T, V> Literal<T, V> of(V value) {
+        if (value instanceof Comparable c) {
+            return (Literal<T, V>) ComparableLiteral.of(c);
+        } else {
+            return new LiteralRecord<>(value);
+        }
+    }
+
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/expression/LiteralRecord.java b/api/src/main/java/jakarta/data/metamodel/expression/LiteralRecord.java
new file mode 100644
index 00000000..b5b4d728
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/expression/LiteralRecord.java
@@ -0,0 +1,33 @@
+/*
+ * 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.expression;
+
+import java.util.Objects;
+
+record LiteralRecord<T, V>(V value) implements Literal<T,V> {
+
+    LiteralRecord {
+        Objects.requireNonNull(value, "Value is required.");
+    }
+
+    @Override
+    public String toString() {
+        return value.toString();
+    }
+
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/expression/NumericCastRecord.java b/api/src/main/java/jakarta/data/metamodel/expression/NumericCastRecord.java
index fb602c55..c09f8054 100644
--- a/api/src/main/java/jakarta/data/metamodel/expression/NumericCastRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/expression/NumericCastRecord.java
@@ -17,9 +17,17 @@
  */
 package jakarta.data.metamodel.expression;
 
+import java.util.Objects;
+
 import jakarta.data.metamodel.NumericExpression;
 
 record NumericCastRecord<T, N extends Number & Comparable<N>>
         (NumericExpression<T,?> expression, Class<N> type)
         implements NumericCast<T,N> {
+
+    NumericCastRecord {
+        Objects.requireNonNull(expression, "Expression is required.");
+        Objects.requireNonNull(type, "Attribute type is required.");
+    }
+
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/expression/NumericFunctionExpression.java b/api/src/main/java/jakarta/data/metamodel/expression/NumericFunctionExpression.java
index c83e8609..92ccccf8 100644
--- a/api/src/main/java/jakarta/data/metamodel/expression/NumericFunctionExpression.java
+++ b/api/src/main/java/jakarta/data/metamodel/expression/NumericFunctionExpression.java
@@ -17,6 +17,7 @@
  */
 package jakarta.data.metamodel.expression;
 
+import jakarta.data.metamodel.ComparableExpression;
 import jakarta.data.metamodel.NumericExpression;
 import jakarta.data.metamodel.TextExpression;
 
@@ -37,4 +38,7 @@ public interface NumericFunctionExpression<T, N extends Number & Comparable<N>>
     of(String name, NumericExpression<? super T,N> argument) {
         return new NumericFunctionExpressionRecord<>(name, List.of(argument));
     }
+
+    @Override
+    List<? extends ComparableExpression<? super T,?>> arguments();
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/expression/NumericFunctionExpressionRecord.java b/api/src/main/java/jakarta/data/metamodel/expression/NumericFunctionExpressionRecord.java
index 4266e8a2..9a5fb22d 100644
--- a/api/src/main/java/jakarta/data/metamodel/expression/NumericFunctionExpressionRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/expression/NumericFunctionExpressionRecord.java
@@ -17,11 +17,19 @@
  */
 package jakarta.data.metamodel.expression;
 
-import jakarta.data.metamodel.Expression;
+import jakarta.data.metamodel.ComparableExpression;
 
 import java.util.List;
+import java.util.Objects;
 
-record NumericFunctionExpressionRecord<T, N extends Number & Comparable<N>>
-        (String name, List<Expression<? super T,?>> arguments)
+record NumericFunctionExpressionRecord<T, N extends Number & Comparable<N>>(
+        String name,
+        List<ComparableExpression<? super T,?>> arguments)
         implements NumericFunctionExpression<T,N> {
+
+    NumericFunctionExpressionRecord {
+        Objects.requireNonNull(name, "Function name is required.");
+        Objects.requireNonNull(arguments, "Function arguments is required.");
+    }
+
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/expression/NumericLiteral.java b/api/src/main/java/jakarta/data/metamodel/expression/NumericLiteral.java
index ec3ca65b..0b4e600c 100644
--- a/api/src/main/java/jakarta/data/metamodel/expression/NumericLiteral.java
+++ b/api/src/main/java/jakarta/data/metamodel/expression/NumericLiteral.java
@@ -20,8 +20,7 @@ package jakarta.data.metamodel.expression;
 import jakarta.data.metamodel.NumericExpression;
 
 public interface NumericLiteral<T, N extends Number & Comparable<N>>
-        extends NumericExpression<T,N> {
-    N value();
+        extends ComparableLiteral<T, N>, NumericExpression<T,N> {
 
     static <T, N extends Number & Comparable<N>> NumericLiteral<T, N> of(N value) {
         return new NumericLiteralRecord<>(value);
diff --git a/api/src/main/java/jakarta/data/metamodel/expression/NumericLiteralRecord.java b/api/src/main/java/jakarta/data/metamodel/expression/NumericLiteralRecord.java
index b498f64d..a2ed8929 100644
--- a/api/src/main/java/jakarta/data/metamodel/expression/NumericLiteralRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/expression/NumericLiteralRecord.java
@@ -17,6 +17,34 @@
  */
 package jakarta.data.metamodel.expression;
 
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Objects;
+
 record NumericLiteralRecord<T, N extends Number & Comparable<N>>
         (N value) implements NumericLiteral<T,N> {
+
+    NumericLiteralRecord {
+        Objects.requireNonNull(value, "Value is required.");
+    }
+
+    @Override
+    public String toString() {
+        // TODO can use switch after compilation is switched to Java 21
+        String suffix;
+        if (value instanceof Long) {
+            suffix = "L";
+        } else if (value instanceof Float) {
+            suffix = "F";
+        } else if (value instanceof Double) {
+            suffix = "D";
+        } else if (value instanceof BigInteger) {
+            suffix = "BI";
+        } else if (value instanceof BigDecimal) {
+            suffix = "BD";
+        } else {
+            suffix = "";
+        }
+        return value.toString() + suffix;
+    }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/expression/NumericOperatorExpressionRecord.java b/api/src/main/java/jakarta/data/metamodel/expression/NumericOperatorExpressionRecord.java
index b2fe9487..119fbbe3 100644
--- a/api/src/main/java/jakarta/data/metamodel/expression/NumericOperatorExpressionRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/expression/NumericOperatorExpressionRecord.java
@@ -17,9 +17,18 @@
  */
 package jakarta.data.metamodel.expression;
 
+import java.util.Objects;
+
 import jakarta.data.metamodel.NumericExpression;
 
 record NumericOperatorExpressionRecord<T, N extends Number & Comparable<N>>
         (Operator operator, NumericExpression<T,N> left, NumericExpression<T,N> right)
         implements NumericOperatorExpression<T,N> {
+
+    NumericOperatorExpressionRecord {
+        Objects.requireNonNull(operator, "Operator is required.");
+        Objects.requireNonNull(left, "Left side expression is required.");
+        Objects.requireNonNull(left, "Right side expression is required.");
+    }
+
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/expression/StringLiteral.java b/api/src/main/java/jakarta/data/metamodel/expression/StringLiteral.java
index b116e129..19c4bd49 100644
--- a/api/src/main/java/jakarta/data/metamodel/expression/StringLiteral.java
+++ b/api/src/main/java/jakarta/data/metamodel/expression/StringLiteral.java
@@ -19,8 +19,8 @@ package jakarta.data.metamodel.expression;
 
 import jakarta.data.metamodel.TextExpression;
 
-public interface StringLiteral<T> extends TextExpression<T> {
-    String value();
+public interface StringLiteral<T>
+        extends ComparableLiteral<T, String>, TextExpression<T> {
 
     static <T> StringLiteral<T> of(String value) {
         return new StringLiteralRecord<T>(value);
diff --git a/api/src/main/java/jakarta/data/metamodel/expression/StringLiteralRecord.java b/api/src/main/java/jakarta/data/metamodel/expression/StringLiteralRecord.java
index 83a8067e..257d554e 100644
--- a/api/src/main/java/jakarta/data/metamodel/expression/StringLiteralRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/expression/StringLiteralRecord.java
@@ -17,7 +17,17 @@
  */
 package jakarta.data.metamodel.expression;
 
+import java.util.Objects;
+
 record StringLiteralRecord<T>(String value)
         implements StringLiteral<T> {
 
+    StringLiteralRecord {
+        Objects.requireNonNull(value, "Value is required.");
+    }
+
+    @Override
+    public String toString() {
+        return '\'' + value + '\'';
+    }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/expression/TemporalLiteral.java b/api/src/main/java/jakarta/data/metamodel/expression/TemporalLiteral.java
new file mode 100644
index 00000000..d30582ec
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/expression/TemporalLiteral.java
@@ -0,0 +1,32 @@
+/*
+ * 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.expression;
+
+import java.time.temporal.Temporal;
+
+import jakarta.data.metamodel.TemporalExpression;
+
+public interface TemporalLiteral<T, V extends Temporal & Comparable<? extends Temporal>>
+        extends ComparableLiteral<T, V>, TemporalExpression<T, V> {
+
+    static <T, V extends Temporal & Comparable<? extends Temporal>> TemporalLiteral<T, V> of(
+            V value) {
+        return new TemporalLiteralRecord<>(value);
+    }
+
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/expression/TemporalLiteralRecord.java b/api/src/main/java/jakarta/data/metamodel/expression/TemporalLiteralRecord.java
new file mode 100644
index 00000000..1b833feb
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/expression/TemporalLiteralRecord.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.expression;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.temporal.Temporal;
+import java.util.Objects;
+
+record TemporalLiteralRecord<T, V extends Temporal & Comparable<? extends Temporal>>(
+        V value)
+        implements TemporalLiteral<T, V> {
+
+    TemporalLiteralRecord {
+        Objects.requireNonNull(value, "Value is required.");
+    }
+
+    @Override
+    public String toString() {
+        // TODO can use a switch statement after updating to Java 21
+        if (value instanceof Instant ||
+            value instanceof LocalDateTime) {
+            return "TIMESTAMP('" + value + "')";
+        } else if (value instanceof LocalDate) {
+            return "DATE('" + value + "')";
+        } else if (value instanceof LocalTime) {
+            return "TIME('" + value + "')";
+        } else {
+            return value.toString();
+        }
+    }
+
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/expression/TextFunctionExpression.java b/api/src/main/java/jakarta/data/metamodel/expression/TextFunctionExpression.java
index facef105..ce8d3a69 100644
--- a/api/src/main/java/jakarta/data/metamodel/expression/TextFunctionExpression.java
+++ b/api/src/main/java/jakarta/data/metamodel/expression/TextFunctionExpression.java
@@ -17,6 +17,7 @@
  */
 package jakarta.data.metamodel.expression;
 
+import jakarta.data.metamodel.ComparableExpression;
 import jakarta.data.metamodel.TextExpression;
 
 import java.util.List;
@@ -45,4 +46,7 @@ public interface TextFunctionExpression<T>
     static <T> TextFunctionExpression<T> of(String name, TextExpression<? super T> left, int literal) {
         return new TextFunctionExpressionRecord<>(name, List.of(left, NumericLiteral.of(literal)));
     }
+
+    @Override
+    List<? extends ComparableExpression<? super T,?>> arguments();
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/expression/TextFunctionExpressionRecord.java b/api/src/main/java/jakarta/data/metamodel/expression/TextFunctionExpressionRecord.java
index c114b0ce..061dfecf 100644
--- a/api/src/main/java/jakarta/data/metamodel/expression/TextFunctionExpressionRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/expression/TextFunctionExpressionRecord.java
@@ -17,10 +17,19 @@
  */
 package jakarta.data.metamodel.expression;
 
-import jakarta.data.metamodel.Expression;
+import jakarta.data.metamodel.ComparableExpression;
 
 import java.util.List;
+import java.util.Objects;
 
-record TextFunctionExpressionRecord<T>(String name, List<? extends Expression<? super T,?>> arguments)
+record TextFunctionExpressionRecord<T>(
+        String name,
+        List<ComparableExpression<? super T,?>> arguments)
         implements TextFunctionExpression<T> {
+
+    TextFunctionExpressionRecord {
+        Objects.requireNonNull(name, "Function name is required.");
+        Objects.requireNonNull(arguments, "Function arguments is required.");
+    }
+
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/path/ComparablePathRecord.java b/api/src/main/java/jakarta/data/metamodel/path/ComparablePathRecord.java
index a8dd5f23..035e3295 100644
--- a/api/src/main/java/jakarta/data/metamodel/path/ComparablePathRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/path/ComparablePathRecord.java
@@ -17,10 +17,18 @@
  */
 package jakarta.data.metamodel.path;
 
+import java.util.Objects;
+
 import jakarta.data.metamodel.ComparableAttribute;
 import jakarta.data.metamodel.NavigableExpression;
 
 record ComparablePathRecord<T,U,C extends Comparable<?>>
         (NavigableExpression<T,U> expression, ComparableAttribute<U, C> attribute)
         implements ComparablePath<T,U,C> {
+
+    ComparablePathRecord {
+        Objects.requireNonNull(expression, "Expression is required.");
+        Objects.requireNonNull(attribute, "Entity attribute is required.");
+    }
+
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/path/NavigablePathRecord.java b/api/src/main/java/jakarta/data/metamodel/path/NavigablePathRecord.java
index ea930c65..783f9c26 100644
--- a/api/src/main/java/jakarta/data/metamodel/path/NavigablePathRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/path/NavigablePathRecord.java
@@ -17,6 +17,8 @@
  */
 package jakarta.data.metamodel.path;
 
+import java.util.Objects;
+
 import jakarta.data.metamodel.NavigableAttribute;
 import jakarta.data.metamodel.NavigableExpression;
 
@@ -24,4 +26,9 @@ record NavigablePathRecord<T,U,V>
         (NavigableExpression<T,U> expression, NavigableAttribute<U,V> attribute)
         implements NavigablePath<T,U,V> {
 
+    NavigablePathRecord {
+        Objects.requireNonNull(expression, "Expression is required.");
+        Objects.requireNonNull(attribute, "Entity attribute is required.");
+    }
+
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/path/NumericPathRecord.java b/api/src/main/java/jakarta/data/metamodel/path/NumericPathRecord.java
index b1606576..9ea31b47 100644
--- a/api/src/main/java/jakarta/data/metamodel/path/NumericPathRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/path/NumericPathRecord.java
@@ -17,10 +17,18 @@
  */
 package jakarta.data.metamodel.path;
 
+import java.util.Objects;
+
 import jakarta.data.metamodel.NavigableExpression;
 import jakarta.data.metamodel.NumericAttribute;
 
 record NumericPathRecord<T,U,N extends Number & Comparable<N>>
         (NavigableExpression<T,U> expression, NumericAttribute<U,N> attribute)
         implements NumericPath<T,U,N> {
+
+    NumericPathRecord {
+        Objects.requireNonNull(expression, "Expression is required.");
+        Objects.requireNonNull(attribute, "Entity attribute is required.");
+    }
+
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/path/TextPathRecord.java b/api/src/main/java/jakarta/data/metamodel/path/TextPathRecord.java
index ed380e3e..5f793a14 100644
--- a/api/src/main/java/jakarta/data/metamodel/path/TextPathRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/path/TextPathRecord.java
@@ -17,10 +17,18 @@
  */
 package jakarta.data.metamodel.path;
 
+import java.util.Objects;
+
 import jakarta.data.metamodel.NavigableExpression;
 import jakarta.data.metamodel.TextAttribute;
 
 record TextPathRecord<T,U>
         (NavigableExpression<T,U> expression, TextAttribute<U> attribute)
         implements TextPath<T,U> {
+
+    TextPathRecord {
+        Objects.requireNonNull(expression, "Expression is required.");
+        Objects.requireNonNull(attribute, "Entity attribute is required.");
+    }
+
 }


Test patch:
diff --git a/api/src/test/java/jakarta/data/metamodel/TextAttributeTest.java b/api/src/test/java/jakarta/data/metamodel/TextAttributeTest.java
index 9e061532..cb7a9657 100644
--- a/api/src/test/java/jakarta/data/metamodel/TextAttributeTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/TextAttributeTest.java
@@ -19,6 +19,7 @@ package jakarta.data.metamodel;
 
 import jakarta.data.metamodel.constraint.Like;
 import jakarta.data.metamodel.constraint.NotLike;
+import jakarta.data.metamodel.expression.StringLiteral;
 import jakarta.data.metamodel.restrict.BasicRestriction;
 import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.Test;
@@ -39,10 +40,12 @@ class TextAttributeTest {
         BasicRestriction<Author,String> restriction =
                 (BasicRestriction<Author, String>) testAttribute.contains("testValue");
 
+        Like like = (Like) restriction.constraint();
+        StringLiteral<?> literal = ((StringLiteral<?>) like.pattern());
+
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.expression()).isEqualTo(testAttribute);
-            soft.assertThat(restriction.constraint()).isInstanceOf(Like.class);
-            soft.assertThat(((Like) restriction.constraint()).pattern()).isEqualTo("%testValue%");
+            soft.assertThat(literal.value()).isEqualTo("%testValue%");
         });
     }
 
@@ -52,10 +55,12 @@ class TextAttributeTest {
         BasicRestriction<Author,String> restriction =
                 (BasicRestriction<Author, String>) testAttribute.startsWith("testValue");
 
+        Like like = (Like) restriction.constraint();
+        StringLiteral<?> literal = ((StringLiteral<?>) like.pattern());
+
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.expression()).isEqualTo(testAttribute);
-            soft.assertThat(restriction.constraint()).isInstanceOf(Like.class);
-            soft.assertThat(((Like) restriction.constraint()).pattern()).isEqualTo("testValue%");
+            soft.assertThat(literal.value()).isEqualTo("testValue%");
         });
     }
 
@@ -65,10 +70,12 @@ class TextAttributeTest {
         BasicRestriction<Author,String> restriction =
                 (BasicRestriction<Author,String>) testAttribute.endsWith("testValue");
 
+        Like like = (Like) restriction.constraint();
+        StringLiteral<?> literal = ((StringLiteral<?>) like.pattern());
+
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.expression()).isEqualTo(testAttribute);
-            soft.assertThat(restriction.constraint()).isInstanceOf(Like.class);
-            soft.assertThat(((Like) restriction.constraint()).pattern()).isEqualTo("%testValue");
+            soft.assertThat(literal.value()).isEqualTo("%testValue");
         });
     }
 
@@ -78,10 +85,13 @@ class TextAttributeTest {
         BasicRestriction<Author,String> restriction =
                 (BasicRestriction<Author, String>) testAttribute.like("%test%");
 
+        Like like = (Like) restriction.constraint();
+        StringLiteral<?> literal = ((StringLiteral<?>) like.pattern());
+
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.expression()).isEqualTo(testAttribute);
             soft.assertThat(restriction.constraint()).isInstanceOf(Like.class);
-            soft.assertThat(((Like) restriction.constraint()).pattern()).isEqualTo("%test%");
+            soft.assertThat(literal.value()).isEqualTo("%test%");
         });
     }
 
@@ -91,10 +101,13 @@ class TextAttributeTest {
         BasicRestriction<Author,String> restriction =
                 (BasicRestriction<Author, String>) testAttribute.notContains("testValue");
 
+        NotLike notLike = (NotLike) restriction.constraint();
+        StringLiteral<?> literal = ((StringLiteral<?>) notLike.pattern());
+
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.expression()).isEqualTo(testAttribute);
             soft.assertThat(restriction.constraint()).isInstanceOf(NotLike.class);
-            soft.assertThat(((NotLike) restriction.constraint()).pattern()).isEqualTo("%testValue%");
+            soft.assertThat(literal.value()).isEqualTo("%testValue%");
         });
     }
 
@@ -104,10 +117,12 @@ class TextAttributeTest {
         BasicRestriction<Author,String> restriction =
                 (BasicRestriction<Author, String>) testAttribute.notLike("%test%");
 
+        NotLike notLike = (NotLike) restriction.constraint();
+        StringLiteral<?> literal = ((StringLiteral<?>) notLike.pattern());
+
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.expression()).isEqualTo(testAttribute);
-            soft.assertThat(restriction.constraint()).isInstanceOf(NotLike.class);
-            soft.assertThat(((NotLike) restriction.constraint()).pattern()).isEqualTo("%test%");
+            soft.assertThat(literal.value()).isEqualTo("%test%");
         });
     }
 
@@ -117,10 +132,12 @@ class TextAttributeTest {
         BasicRestriction<Author,String> restriction =
                 (BasicRestriction<Author, String>) testAttribute.notStartsWith("testValue");
 
+        NotLike notLike = (NotLike) restriction.constraint();
+        StringLiteral<?> literal = ((StringLiteral<?>) notLike.pattern());
+
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.expression()).isEqualTo(testAttribute);
-            soft.assertThat(restriction.constraint()).isInstanceOf(NotLike.class);
-            soft.assertThat(((NotLike) restriction.constraint()).pattern()).isEqualTo("testValue%");
+            soft.assertThat(literal.value()).isEqualTo("testValue%");
         });
     }
 
@@ -130,10 +147,12 @@ class TextAttributeTest {
         BasicRestriction<Author,String> restriction =
                 (BasicRestriction<Author, String>) testAttribute.notEndsWith("testValue");
 
+        NotLike notLike = (NotLike) restriction.constraint();
+        StringLiteral<?> literal = ((StringLiteral<?>) notLike.pattern());
+
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.expression()).isEqualTo(testAttribute);
-            soft.assertThat(restriction.constraint()).isInstanceOf(NotLike.class);
-            soft.assertThat(((NotLike) restriction.constraint()).pattern()).isEqualTo("%testValue");
+            soft.assertThat(literal.value()).isEqualTo("%testValue");
         });
     }
 }
diff --git a/api/src/test/java/jakarta/data/metamodel/expression/ExpressionTest.java b/api/src/test/java/jakarta/data/metamodel/expression/ExpressionTest.java
index 526ebfdb..b4ff0605 100644
--- a/api/src/test/java/jakarta/data/metamodel/expression/ExpressionTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/expression/ExpressionTest.java
@@ -33,16 +33,39 @@ import org.junit.jupiter.api.Test;
 
 class ExpressionTest {
 
+    @Test
+    void shouldCompareWithOtherEntityAttribute() {
+        Restriction<Book> autobiographies =
+                _Book.title.equalTo(_Book.author);
+
+        @SuppressWarnings("unchecked")
+        BasicRestriction<Book, String> restriction =
+                (BasicRestriction<Book, String>) autobiographies;
+
+        EqualTo<String> equalToConstraint =
+                (EqualTo<String>) restriction.constraint();
+
+        SoftAssertions.assertSoftly(soft -> {
+            soft.assertThat(restriction.expression())
+                .isEqualTo(_Book.title);
+
+            soft.assertThat(equalToConstraint.expression())
+                .isEqualTo(_Book.author);
+        });
+    }
+
     @Test
     void shouldRestrictLast2ofFirst10Chars() {
 
         Restriction<Book> titleWithEE =
             _Book.title.left(10).right(2).upper().equalTo("EE");
 
+        @SuppressWarnings("unchecked")
         BasicRestriction<Book, String> restriction =
             (BasicRestriction<Book, String>) titleWithEE;
 
-        Constraint<String> constraint = restriction.constraint();
+        EqualTo<String> constraint = (EqualTo<String>) restriction.constraint();
+        StringLiteral<?> literal = (StringLiteral<?>) constraint.expression();
 
         TextFunctionExpression<Book> upperExpression =
             (TextFunctionExpression<Book>) restriction.expression();
@@ -95,10 +118,7 @@ class ExpressionTest {
             soft.assertThat(upperExpression.name())
                 .isEqualTo(TextFunctionExpression.UPPER);
 
-            soft.assertThat(constraint)
-                .isInstanceOf(EqualTo.class);
-
-            soft.assertThat(((EqualTo<String>) constraint).value())
+            soft.assertThat(literal.value())
                 .isEqualTo("EE");
         });
     }
@@ -112,7 +132,11 @@ class ExpressionTest {
         BasicRestriction<Book, Integer> restriction =
             (BasicRestriction<Book, Integer>) titleUpTo50Chars;
 
-        Constraint<Integer> constraint = restriction.constraint();
+        LessThanOrEqual<Integer> constraint =
+            (LessThanOrEqual<Integer>) restriction.constraint();
+
+        NumericLiteral<?, Integer> literal =
+            (NumericLiteral<?, Integer>) constraint.bound();
 
         NumericFunctionExpression<Book, Integer> lengthExpression =
             (NumericFunctionExpression<Book, Integer>) restriction.expression();
@@ -127,10 +151,7 @@ class ExpressionTest {
             soft.assertThat(lengthExpression.arguments().get(0))
                 .isEqualTo(_Book.title);
 
-            soft.assertThat(constraint)
-                .isInstanceOf(LessThanOrEqual.class);
-
-            soft.assertThat(((LessThanOrEqual<Integer>) constraint).bound())
+            soft.assertThat(literal.value())
                 .isEqualTo(50);
         });
     }
diff --git a/api/src/test/java/jakarta/data/metamodel/expression/NumericExpressionTest.java b/api/src/test/java/jakarta/data/metamodel/expression/NumericExpressionTest.java
new file mode 100644
index 00000000..c7b7dc71
--- /dev/null
+++ b/api/src/test/java/jakarta/data/metamodel/expression/NumericExpressionTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.expression;
+
+import jakarta.data.metamodel.constraint.*;
+import jakarta.data.metamodel.restrict.BasicRestriction;
+import jakarta.data.metamodel.restrict.Restriction;
+import jakarta.data.mock.entity.Book;
+import jakarta.data.mock.entity._Book;
+
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.Test;
+
+class NumericExpressionTest {
+
+    @Test
+    void shouldCompareWithNumericAttribute() {
+        Restriction<Book> averageChapterAtLeastAsLongAsNumChapters =
+                _Book.numPages.divide(_Book.numChapters)
+                        .greaterThanEqual(_Book.numChapters);
+
+        @SuppressWarnings("unchecked")
+        BasicRestriction<Book, Integer> restriction =
+                (BasicRestriction<Book, Integer>)
+                        averageChapterAtLeastAsLongAsNumChapters;
+
+        GreaterThanOrEqual<Integer> gteNumChapters =
+                (GreaterThanOrEqual<Integer>) restriction.constraint();
+
+        NumericOperatorExpression<Book, Integer> divide =
+                (NumericOperatorExpression<Book, Integer>) restriction.expression();
+
+        SoftAssertions.assertSoftly(soft -> {
+            soft.assertThat(gteNumChapters.bound())
+                .isEqualTo(_Book.numChapters);
+
+            soft.assertThat(divide.left())
+                .isEqualTo(_Book.numPages);
+
+            soft.assertThat(divide.operator())
+                .isEqualTo(NumericOperatorExpression.Operator.DIVIDE);
+
+            soft.assertThat(divide.right())
+                .isEqualTo(_Book.numChapters);
+        });
+    }
+
+    @Test
+    void shouldCompareWithNumericFunctionExpression() {
+        Restriction<Book> fewerChaptersThanCharsInTitle =
+                _Book.numChapters.lessThan(
+                        _Book.title.length());
+
+        @SuppressWarnings("unchecked")
+        BasicRestriction<Book, Integer> restriction =
+                (BasicRestriction<Book, Integer>) fewerChaptersThanCharsInTitle;
+
+        LessThan<Integer> lessThanLength =
+                (LessThan<Integer>) restriction.constraint();
+
+        @SuppressWarnings("unchecked")
+        NumericFunctionExpression<Book, Integer> lengthExpression =
+                (NumericFunctionExpression<Book, Integer>) lessThanLength.bound();
+
+        SoftAssertions.assertSoftly(soft -> {
+            soft.assertThat(restriction.expression())
+                .isEqualTo(_Book.numChapters);
+
+            soft.assertThat(lengthExpression.name())
+                .isEqualTo(NumericFunctionExpression.LENGTH);
+
+            soft.assertThat(lengthExpression.arguments().get(0))
+                .isEqualTo(_Book.title);
+        });
+    }
+}
diff --git a/api/src/test/java/jakarta/data/metamodel/expression/TemporalExpressionTest.java b/api/src/test/java/jakarta/data/metamodel/expression/TemporalExpressionTest.java
index 1c844252..7eedb3ae 100644
--- a/api/src/test/java/jakarta/data/metamodel/expression/TemporalExpressionTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/expression/TemporalExpressionTest.java
@@ -37,10 +37,29 @@ class TemporalExpressionTest {
         TemporalExpression<Object, LocalDate> today =
             TemporalExpression.localDate();
 
-        // TODO write the test once comparisons with expressions are added
-        //Restriction<Book> beforeCurrentDate =
-        //    _Book.publicationDate.lessThan(today);
-        // ...
+        Restriction<Book> beforeCurrentDate =
+            _Book.publicationDate.lessThan(today);
+
+        @SuppressWarnings("unchecked")
+        BasicRestriction<Book, LocalDate> restriction =
+            (BasicRestriction<Book, LocalDate>) beforeCurrentDate;
+
+        TemporalExpression<Book, LocalDate> lessThanExpression =
+                (TemporalExpression<Book, LocalDate>) restriction.expression();
+
+        LessThan<LocalDate> lessThan =
+            (LessThan<LocalDate>) restriction.constraint();
+
+        SoftAssertions.assertSoftly(soft -> {
+            soft.assertThat(lessThanExpression)
+                .isEqualTo(_Book.publicationDate);
+
+            soft.assertThat(lessThan.bound())
+                .isInstanceOf(CurrentDate.class);
+
+            soft.assertThat(lessThan.bound())
+                .isEqualTo(today);
+        });
     }
 
     @Test
@@ -54,7 +73,14 @@ class TemporalExpressionTest {
         BasicRestriction<Book, LocalDate> restriction =
             (BasicRestriction<Book, LocalDate>) publishedInApril2025;
 
-        Constraint<LocalDate> constraint = restriction.constraint();
+        Between<LocalDate> constraint =
+            (Between<LocalDate>) restriction.constraint();
+
+        TemporalLiteral<?, LocalDate> lowerLiteral =
+            (TemporalLiteral<?, LocalDate>) constraint.lowerBound();
+
+        TemporalLiteral<?, LocalDate> upperLiteral =
+            (TemporalLiteral<?, LocalDate>) constraint.upperBound();
 
         TemporalExpression<Book, LocalDate> betweenExpression =
             (TemporalExpression<Book, LocalDate>) restriction.expression();
@@ -63,13 +89,10 @@ class TemporalExpressionTest {
             soft.assertThat(betweenExpression)
                 .isEqualTo(_Book.publicationDate);
 
-            soft.assertThat(constraint)
-                .isInstanceOf(Between.class);
-
-            soft.assertThat(((Between<LocalDate>) constraint).lowerBound())
+            soft.assertThat(lowerLiteral.value())
                 .isEqualTo(LocalDate.of(2025, Month.APRIL, 1));
 
-            soft.assertThat(((Between<LocalDate>) constraint).upperBound())
+            soft.assertThat(upperLiteral.value())
                 .isEqualTo(LocalDate.of(2025, Month.APRIL, 30));
         });
     }
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 416f3c5d..b1788385 100644
--- a/api/src/test/java/jakarta/data/metamodel/restrict/BasicRestrictionRecordTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/restrict/BasicRestrictionRecordTest.java
@@ -126,6 +126,6 @@ class BasicRestrictionRecordTest {
     void shouldThrowExceptionWhenValueIsNull() {
         assertThatThrownBy(() -> _Book.title.equalTo((String) null))
                 .isInstanceOf(NullPointerException.class)
-                .hasMessage("Value must not be null");
+                .hasMessage("Value is required.");
     }
 }
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 d00bbd83..8d51c589 100644
--- a/api/src/test/java/jakarta/data/metamodel/restrict/RestrictTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/restrict/RestrictTest.java
@@ -27,6 +27,7 @@ import jakarta.data.metamodel.constraint.EqualTo;
 import jakarta.data.metamodel.constraint.Like;
 import jakarta.data.metamodel.constraint.NotEqualTo;
 import jakarta.data.metamodel.constraint.NotLike;
+import jakarta.data.metamodel.expression.StringLiteral;
 
 import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
 import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
@@ -142,10 +143,12 @@ class RestrictTest {
         BasicRestriction<Employee,String> restriction =
                 (BasicRestriction<Employee, String>) _Employee.position.contains("Manager");
 
+        Like constraint = (Like) restriction.constraint();
+        StringLiteral<?> literal = (StringLiteral<?>) constraint.pattern();
+
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.expression()).isEqualTo(_Employee.position);
-            soft.assertThat(restriction.constraint()).isInstanceOf(Like.class);
-            soft.assertThat(((Like) restriction.constraint()).pattern()).isEqualTo("%Manager%");
+            soft.assertThat(literal.value()).isEqualTo("%Manager%");
         });
     }
 
@@ -155,10 +158,12 @@ class RestrictTest {
         BasicRestriction<Employee,String> restriction =
                 (BasicRestriction<Employee, String>) _Employee.position.notContains("Director");
 
+        NotLike constraint = (NotLike) restriction.constraint();
+        StringLiteral<?> literal = (StringLiteral<?>) constraint.pattern();
+
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.expression()).isEqualTo(_Employee.position);
-            soft.assertThat(restriction.constraint()).isInstanceOf(NotLike.class);
-            soft.assertThat(((NotLike) restriction.constraint()).pattern()).isEqualTo("%Director%");
+            soft.assertThat(literal.value()).isEqualTo("%Director%");
         });
     }
 
@@ -168,10 +173,12 @@ class RestrictTest {
         BasicRestriction<Employee,String> restriction =
                 (BasicRestriction<Employee, String>) _Employee.position.startsWith("Director");
 
+        Like constraint = (Like) restriction.constraint();
+        StringLiteral<?> literal = (StringLiteral<?>) constraint.pattern();
+
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.expression()).isEqualTo(_Employee.position);
-            soft.assertThat(restriction.constraint()).isInstanceOf(Like.class);
-            soft.assertThat(((Like) restriction.constraint()).pattern()).isEqualTo("Director%");
+            soft.assertThat(literal.value()).isEqualTo("Director%");
         });
     }
 
@@ -181,10 +188,12 @@ class RestrictTest {
         BasicRestriction<Employee,String> restriction =
                 (BasicRestriction<Employee, String>) _Employee.position.notStartsWith("Manager");
 
+        NotLike constraint = (NotLike) restriction.constraint();
+        StringLiteral<?> literal = (StringLiteral<?>) constraint.pattern();
+
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.expression()).isEqualTo(_Employee.position);
-            soft.assertThat(restriction.constraint()).isInstanceOf(NotLike.class);
-            soft.assertThat(((NotLike) restriction.constraint()).pattern()).isEqualTo("Manager%");
+            soft.assertThat(literal.value()).isEqualTo("Manager%");
         });
     }
 
@@ -194,10 +203,12 @@ class RestrictTest {
         BasicRestriction<Employee,String> restriction =
                 (BasicRestriction<Employee,String>) _Employee.position.endsWith("Manager");
 
+        Like constraint = (Like) restriction.constraint();
+        StringLiteral<?> literal = (StringLiteral<?>) constraint.pattern();
+
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.expression()).isEqualTo(_Employee.position);
-            soft.assertThat(restriction.constraint()).isInstanceOf(Like.class);
-            soft.assertThat(((Like) restriction.constraint()).pattern()).isEqualTo("%Manager");
+            soft.assertThat(literal.value()).isEqualTo("%Manager");
         });
     }
 
@@ -207,10 +218,12 @@ class RestrictTest {
         BasicRestriction<Employee,String> restriction =
                 (BasicRestriction<Employee, String>) _Employee.position.notEndsWith("Supervisor");
 
+        NotLike constraint = (NotLike) restriction.constraint();
+        StringLiteral<?> literal = (StringLiteral<?>) constraint.pattern();
+
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.expression()).isEqualTo(_Employee.position);
-            soft.assertThat(restriction.constraint()).isInstanceOf(NotLike.class);
-            soft.assertThat(((NotLike) restriction.constraint()).pattern()).isEqualTo("%Supervisor");
+            soft.assertThat(literal.value()).isEqualTo("%Supervisor");
         });
     }
 
@@ -252,9 +265,9 @@ class RestrictTest {
         @SuppressWarnings("unchecked")
         Like like = (Like) ((BasicRestriction<Employee,String>)
                 _Employee.position.endsWith("test_value")).constraint();
-        String result = like.pattern();
+        StringLiteral<?> literal = (StringLiteral<?>) like.pattern();
 
-        assertThat(result).isEqualTo("%test\\_value");
+        assertThat(literal.value()).isEqualTo("%test\\_value");
     }
 
     @Test
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 39644a8a..99a63c19 100644
--- a/api/src/test/java/jakarta/data/metamodel/restrict/TextRestrictionRecordTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/restrict/TextRestrictionRecordTest.java
@@ -21,6 +21,7 @@ import jakarta.data.metamodel.TextAttribute;
 import jakarta.data.metamodel.constraint.Like;
 import jakarta.data.metamodel.constraint.NotEqualTo;
 import jakarta.data.metamodel.constraint.NotLike;
+import jakarta.data.metamodel.expression.StringLiteral;
 import jakarta.data.mock.entity.Book;
 import jakarta.data.mock.entity._Book;
 
@@ -39,12 +40,13 @@ class TextRestrictionRecordTest {
                 (BasicRestriction<Book, String>) _Book.title.like("%Java%");
 
         Like constraint = (Like) restriction.constraint();
+        StringLiteral<?> literal = (StringLiteral<?>) constraint.pattern();
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.expression()).isEqualTo(_Book.title);
             // TODO TextRestriction.ignoreCase vs TextAttribute.upper/lowercased
             //soft.assertThat(restriction.isCaseSensitive()).isTrue();
-            soft.assertThat(constraint.pattern()).isEqualTo("%Java%");
+            soft.assertThat(literal.value()).isEqualTo("%Java%");
             soft.assertThat(constraint.escape()).isNull();
         });
     }
@@ -56,12 +58,13 @@ class TextRestrictionRecordTest {
                 (BasicRestriction<Book, String>) _Book.title.like("%Java%").negate();
 
         NotLike constraint = (NotLike) restriction.constraint();
+        StringLiteral<?> literal = (StringLiteral<?>) constraint.pattern();
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.expression()).isEqualTo(_Book.title);
             // TODO TextRestriction.ignoreCase vs TextAttribute.upper/lowercased
             //soft.assertThat(restriction.isCaseSensitive()).isTrue();
-            soft.assertThat(constraint.pattern()).isEqualTo("%Java%");
+            soft.assertThat(literal.value()).isEqualTo("%Java%");
             soft.assertThat(constraint.escape()).isNull();
         });
     }
@@ -75,11 +78,12 @@ class TextRestrictionRecordTest {
         // TODO TextRestriction.ignoreCase vs TextAttribute.upper/lowercased
         //BasicRestriction<Book,String> caseInsensitiveRestriction = restriction.ignoreCase();
         Like constraint = (Like) restriction.constraint();
+        StringLiteral<?> literal = (StringLiteral<?>) constraint.pattern();
 
         SoftAssertions.assertSoftly(soft -> {
         //    soft.assertThat(caseInsensitiveRestriction.expression()).isEqualTo(_Book.title);
         //    soft.assertThat(caseInsensitiveRestriction.isCaseSensitive()).isFalse();
-            soft.assertThat(constraint.pattern()).isEqualTo("%Java%");
+            soft.assertThat(literal.value()).isEqualTo("%Java%");
             soft.assertThat(constraint.escape()).isNull();
         });
     }
@@ -91,12 +95,13 @@ class TextRestrictionRecordTest {
                 (BasicRestriction<Book, String>) _Book.title.like("%Java%");
 
         Like constraint = (Like) restriction.constraint();
+        StringLiteral<?> literal = (StringLiteral<?>) constraint.pattern();
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.expression()).isEqualTo(_Book.title);
             // TODO TextRestriction.ignoreCase vs TextAttribute.upper/lowercased
             //soft.assertThat(restriction.isCaseSensitive()).isTrue();
-            soft.assertThat(constraint.pattern()).isEqualTo("%Java%");
+            soft.assertThat(literal.value()).isEqualTo("%Java%");
             soft.assertThat(constraint.escape()).isNull();
         });
     }
@@ -108,12 +113,13 @@ class TextRestrictionRecordTest {
                 (BasicRestriction<Book, String>) _Book.title.like("*Java??", '?', '*', '$');
 
         Like constraint = (Like) restriction.constraint();
+        StringLiteral<?> literal = (StringLiteral<?>) constraint.pattern();
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.expression()).isEqualTo(_Book.title);
             // TODO TextRestriction.ignoreCase vs TextAttribute.upper/lowercased
             //soft.assertThat(restriction.isCaseSensitive()).isTrue();
-            soft.assertThat(constraint.pattern()).isEqualTo("%Java__");
+            soft.assertThat(literal.value()).isEqualTo("%Java__");
             soft.assertThat(constraint.escape()).isEqualTo('$');
         });
     }
@@ -144,17 +150,32 @@ class TextRestrictionRecordTest {
         BasicRestriction<Book,String> notEndsWithJakartaEE = endsWithJakartaEE.negate();
         BasicRestriction<Book,String> notNotEndsWithJakartaEE = notEndsWithJakartaEE.negate();
 
+        Like endsWithJakartaEEConstraint =
+                (Like) endsWithJakartaEE.constraint();
+
+        NotLike notEndsWithJakartaEEConstraint =
+                (NotLike) notEndsWithJakartaEE.constraint();
+
+        Like notNotEndsWithJakartaEEConstraint =
+                (Like) notNotEndsWithJakartaEE.constraint();
+
+        StringLiteral<?> endsWithJakartaEELiteral =
+                (StringLiteral<?>) endsWithJakartaEEConstraint.pattern();
+
+        StringLiteral<?> notEndsWithJakartaEELiteral =
+                (StringLiteral<?>) notEndsWithJakartaEEConstraint.pattern();
+
+        StringLiteral<?> notNotEndsWithJakartaEELiteral =
+                (StringLiteral<?>) notNotEndsWithJakartaEEConstraint.pattern();
+
         SoftAssertions.assertSoftly(soft -> {
-            soft.assertThat(endsWithJakartaEE.constraint()).isInstanceOf(Like.class);
-            soft.assertThat(((Like) endsWithJakartaEE.constraint()).pattern())
+            soft.assertThat(endsWithJakartaEELiteral.value())
                 .isEqualTo("%Jakarta EE");
 
-            soft.assertThat(notEndsWithJakartaEE.constraint()).isInstanceOf(NotLike.class);
-            soft.assertThat(((NotLike) notEndsWithJakartaEE.constraint()).pattern())
+            soft.assertThat(notEndsWithJakartaEELiteral.value())
                 .isEqualTo("%Jakarta EE");
 
-            soft.assertThat(notNotEndsWithJakartaEE.constraint()).isInstanceOf(Like.class);
-            soft.assertThat(((Like) notNotEndsWithJakartaEE.constraint()).pattern())
+            soft.assertThat(notNotEndsWithJakartaEELiteral.value())
                 .isEqualTo("%Jakarta EE");
         });
     }
@@ -187,13 +208,15 @@ class TextRestrictionRecordTest {
         BasicRestriction<Book,String> restriction =
                 (BasicRestriction<Book, String>) _Book.author.equalTo("John Doe").negate();
 
-        NotEqualTo<String> constraint = (NotEqualTo<String>) restriction.constraint();
+        NotEqualTo<String> constraint =
+                (NotEqualTo<String>) restriction.constraint();
+        StringLiteral<?> literal = (StringLiteral<?>) constraint.expression();
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction.expression()).isEqualTo(_Book.author);
             // TODO TextRestriction.ignoreCase vs TextAttribute.upper/lowercased
             //soft.assertThat(restriction.isCaseSensitive()).isTrue();
-            soft.assertThat(constraint.value()).isEqualTo("John Doe");
+            soft.assertThat(literal.value()).isEqualTo("John Doe");
         });
     }
 
diff --git a/api/src/test/java/jakarta/data/mock/entity/_Book.java b/api/src/test/java/jakarta/data/mock/entity/_Book.java
index 595ee6ed..feff0411 100644
--- a/api/src/test/java/jakarta/data/mock/entity/_Book.java
+++ b/api/src/test/java/jakarta/data/mock/entity/_Book.java
@@ -21,7 +21,7 @@ import java.time.Instant;
 import java.time.LocalDate;
 
 import jakarta.data.metamodel.BasicAttribute;
-import jakarta.data.metamodel.ComparableAttribute;
+import jakarta.data.metamodel.NumericAttribute;
 import jakarta.data.metamodel.TemporalAttribute;
 import jakarta.data.metamodel.TextAttribute;
 
@@ -43,9 +43,9 @@ public interface _Book {
             Book.class, COPYRIGHTDATE, Instant.class);
     TextAttribute<Book> id = TextAttribute.of(
             Book.class, ID);
-    ComparableAttribute<Book, Integer> numChapters = ComparableAttribute.of(
+    NumericAttribute<Book, Integer> numChapters = NumericAttribute.of(
             Book.class, NUMCHAPTERS, int.class);
-    ComparableAttribute<Book, Integer> numPages = ComparableAttribute.of(
+    NumericAttribute<Book, Integer> numPages = NumericAttribute.of(
             Book.class, NUMPAGES, int.class);
     TemporalAttribute<Book, LocalDate> publicationDate = TemporalAttribute.of(
             Book.class, PUBLICATIONDATE, LocalDate.class);