From efe4f1ecd920ab4dc11317209b12df0f0f4e261b Mon Sep 17 00:00:00 2001
From: Nathan Rauh <nathan.rauh@us.ibm.com>
Date: Tue, 5 Nov 2024 08:21:38 -0600
Subject: [PATCH 1/4] Issue #857 more types of comparisons for parameter based
 automatic query

Signed-off-by: Nathan Rauh <nathan.rauh@us.ibm.com>
---
 api/src/main/java/jakarta/data/Limit.java     |   9 +-
 .../java/jakarta/data/page/CursoredPage.java  |  17 +--
 .../java/jakarta/data/page/PageRequest.java   |  11 +-
 .../main/java/jakarta/data/repository/By.java |   5 +-
 .../main/java/jakarta/data/repository/Is.java | 118 ++++++++++++++++++
 .../java/jakarta/data/repository/OrderBy.java |   8 +-
 .../jakarta/data/repository/Repository.java   |   5 +-
 api/src/main/java/module-info.java            |  90 ++++++++-----
 8 files changed, 216 insertions(+), 47 deletions(-)
 create mode 100644 api/src/main/java/jakarta/data/repository/Is.java

diff --git a/api/src/main/java/jakarta/data/Limit.java b/api/src/main/java/jakarta/data/Limit.java
index ca2948bcd..d06981a2e 100644
--- a/api/src/main/java/jakarta/data/Limit.java
+++ b/api/src/main/java/jakarta/data/Limit.java
@@ -33,12 +33,15 @@
  * For example,</p>
  *
  * <pre>
- * Product[] findByNameLike(String namePattern, Limit limit, Sort&lt;?&gt;... sorts);
+ * &#64;Find
+ * Product[] named(&#64;By(_Product.NAME) &#64;Is(LikeIgnoreCase) String namePattern,
+ *                 Limit limit,
+ *                 Sort&lt;Product&gt;... sorts);
  * 
  * ...
- * mostExpensive50 = products.findByNameLike(pattern, Limit.of(50), Sort.desc("price"));
+ * mostExpensive50 = products.named(pattern, Limit.of(50), Sort.desc("price"));
  * ...
- * secondMostExpensive50 = products.findByNameLike(pattern, Limit.range(51, 100), Sort.desc("price"));
+ * secondMostExpensive50 = products.named(pattern, Limit.range(51, 100), Sort.desc("price"));
  * </pre>
  *
  * <p>A repository method may not be declared with:
diff --git a/api/src/main/java/jakarta/data/page/CursoredPage.java b/api/src/main/java/jakarta/data/page/CursoredPage.java
index 2ad70d295..71770f8ce 100644
--- a/api/src/main/java/jakarta/data/page/CursoredPage.java
+++ b/api/src/main/java/jakarta/data/page/CursoredPage.java
@@ -55,23 +55,26 @@
  * query parameters) of type {@link PageRequest}, for example:</p>
  *
  * <pre>
- * &#64;OrderBy("lastName")
- * &#64;OrderBy("firstName")
- * &#64;OrderBy("id")
- * CursoredPage&lt;Employee&gt; findByHoursWorkedGreaterThan(int hours, PageRequest pageRequest);
+ * &#64;Find
+ * &#64;OrderBy(_Employee.LASTNAME)
+ * &#64;OrderBy(_Employee.FIRSTNAME)
+ * &#64;OrderBy(_Employee.ID)
+ * CursoredPage&lt;Employee&gt; withOvertime(
+ *         &#64;By(_Employee.HOURSWORKED) &#64;Is(GreaterThan) int fullTimeHours,
+ *         PageRequest pageRequest);
  * </pre>
  *
  * <p>In initial page may be requested using an offset-based page request:</p>
  *
  * <pre>
- * page = employees.findByHoursWorkedGreaterThan(1500, PageRequest.ofSize(50));
+ * page = employees.withOvertime(40, PageRequest.ofSize(50));
  * </pre>
  *
  * <p>The next page may be requested relative to the end of the current page,
  * as follows:</p>
  *
  * <pre>
- * page = employees.findByHoursWorkedGreaterThan(1500, page.nextPageRequest());
+ * page = employees.withOvertime(40, page.nextPageRequest());
  * </pre>
  *
  * <p>Here, the instance of {@link PageRequest} returned by
@@ -92,7 +95,7 @@
  *         PageRequest.ofPage(5)
  *                    .size(50)
  *                    .afterCursor(Cursor.forKey(emp.lastName, emp.firstName, emp.id));
- * page = employees.findByHoursWorkedGreaterThan(1500, pageRequest);
+ * page = employees.withOvertime(40, pageRequest);
  * </pre>
  *
  * <p>By making the query for the next page relative to observed values,
diff --git a/api/src/main/java/jakarta/data/page/PageRequest.java b/api/src/main/java/jakarta/data/page/PageRequest.java
index a73421d1d..f7d90a4b6 100644
--- a/api/src/main/java/jakarta/data/page/PageRequest.java
+++ b/api/src/main/java/jakarta/data/page/PageRequest.java
@@ -37,20 +37,25 @@
  * example:</p>
  *
  * <pre>
+ * &#64;Find
  * &#64;OrderBy("age")
  * &#64;OrderBy("ssn")
- * Person[] findByAgeBetween(int minAge, int maxAge, PageRequest pageRequest);
+ * Person[] agedBetween(&#64;By("age") &#64;Is(GreaterThanEqual) int minAge,
+ *                      &#64;By("age") &#64;Is(LessThanEqual) int maxAge,
+ *                      PageRequest pageRequest);
  * </pre>
  *
  * <p>This method might be called as follows:</p>
  *
  * <pre>
- * var page = people.findByAgeBetween(35, 59,
+ * var page = people.agedBetween(
+ *                35, 59,
  *                PageRequest.ofSize(100));
  * var results = page.content();
  * ...
  * while (page.hasNext()) {
- *     page = people.findByAgeBetween(35, 59,
+ *     page = people.agedBetween(
+ *                35, 59,
  *                page.nextPageRequest().withoutTotal());
  *     results = page.content();
  *   ...
diff --git a/api/src/main/java/jakarta/data/repository/By.java b/api/src/main/java/jakarta/data/repository/By.java
index 233dec891..cb01804a1 100644
--- a/api/src/main/java/jakarta/data/repository/By.java
+++ b/api/src/main/java/jakarta/data/repository/By.java
@@ -33,7 +33,10 @@
  *     to the unique identifier attribute.
  * </ul>
  * <p>Arguments to the annotated parameter are compared to values of the
- * mapped attribute.</p>
+ * mapped attribute. The equality comparison is used by default.<p>
+ *
+ * <p>For other types of basic comparisons, include the {@link Is} annotation.</p>
+ *
  * <p>The attribute name may be a compound name like {@code address.city}.</p>
  *
  * <p>For example, for a {@code Person} entity with attributes {@code ssn},
diff --git a/api/src/main/java/jakarta/data/repository/Is.java b/api/src/main/java/jakarta/data/repository/Is.java
new file mode 100644
index 000000000..20adb800c
--- /dev/null
+++ b/api/src/main/java/jakarta/data/repository/Is.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2024 Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package jakarta.data.repository;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <p>Annotates a parameter of a repository {@link Find} or {@link Delete} method,
+ * indicating how a persistent field is compared against the parameter's value.
+ * The {@link By} annotation is used on the same parameter to identify the
+ * persistent field.</p>
+ *
+ * <p>For example,</p>
+ *
+ * <pre>
+ * &#64;Repository
+ * public interface Products extends CrudRepository&lt;Product, Long&gt; {
+ *
+ *     // Find all Product entities where the price field is less than a maximum value.
+ *     &#64;Find
+ *     List&lt;Product&gt; pricedBelow(&#64;By(_Product.PRICE) &#64;Is(LessThan) float max);
+ *
+ *     // Find a page of Product entities where the name field matches a pattern, ignoring case.
+ *     &#64;Find
+ *     Page&lt;Product&gt; search(&#64;By(_Product.NAME) &#64;Is(LikeIgnoreCase) String pattern,
+ *                          PageRequest pagination,
+ *                          Order&lt;Product&gt; order);
+ *
+ *     // Remove Product entities with any of the unique identifiers listed.
+ *     &#64;Delete
+ *     void remove(&#64;By(ID) &#64;Is(In) List&lt;Long&gt; productIds);
+ * }
+ * </pre>
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface Is {
+    /**
+     * <p>The type of comparison operation to use when comparing a persistent
+     * field against a value that is supplied to a repository method.</p>
+     *
+     * <p>The following example compares the year a person was born against
+     * a minimum and maximum year that are supplied as parameters to a repository
+     * method:</p>
+     *
+     * <pre>
+     * &#64;Find
+     * &#64;OrderBy(_Person.YEAR_BORN)
+     * List&lt;Person&gt; bornWithin(&#64;By(_Person.YEAR_BORN) &#64;Is(GreaterThanEqual) float minYear,
+     *                         &#64;By(_Person.YEAR_BORN) &#64;Is(LessThanEqual) float maxYear);
+     * </pre>
+     *
+     * <p>The default comparison operation is the {@linkplain Op#Equal equality}
+     * comparison.</p> 
+     *
+     * @return the type of comparison operation.
+     */
+    Op value() default Op.Equal;
+
+    /**
+     * <p>Comparison operations for the {@link Is} annotation.</p>
+     *
+     * <p>For more concise code, it can be convenient to statically import one
+     * or more comparison operations. For example:</p>
+     *
+     * <pre>
+     * import static jakarta.data.repository.Is.Op.*;
+     * </pre>
+     */
+    public static enum Op {
+        // TODO add JavaDoc with examples to these
+        Equal,
+        GreaterThan,
+        GreaterThanEqual,
+        IgnoreCase,
+        In,
+        LessThan,
+        LessThanEqual,
+        Like,
+        LikeIgnoreCase,
+        // TODO might want to give more thought to the exact names,
+        Prefixed,
+        PrefixedIgnoreCase,
+        Substringed,
+        SubstringedIgnoreCase,
+        Suffixed,
+        SuffixedIgnoreCase,
+        Not,
+        NotIgnoreCase,
+        NotIn,
+        NotLike,
+        NotLikeIgnoreCase,
+        NotPrefixed,
+        NotPrefixedIgnoreCase,
+        NotSubstringed,
+        NotSubstringedIgnoreCase,
+        NotSuffixed,
+        NotSuffixedIgnoreCase;
+    }
+}
diff --git a/api/src/main/java/jakarta/data/repository/OrderBy.java b/api/src/main/java/jakarta/data/repository/OrderBy.java
index 207afd121..8c1822508 100644
--- a/api/src/main/java/jakarta/data/repository/OrderBy.java
+++ b/api/src/main/java/jakarta/data/repository/OrderBy.java
@@ -62,8 +62,9 @@
  * <p>The default sort order is ascending. The {@code descending} member can be
  * used to specify the sort direction.</p>
  * <pre>
- * &#64;OrderBy(value = "price", descending = true)
- * {@code Stream<Product>} findByPriceLessThanEqual(double maxPrice);
+ * &#64;Find
+ * &#64;OrderBy(value = _Product.PRICE, descending = true)
+ * {@code Stream<Product>} pricedBelow(&#64;By(_Product.PRICE) &#64;Is(LessThan) double maxPrice);
  * </pre>
  *
  * <p>A repository method with an {@code @OrderBy} annotation must not
@@ -115,8 +116,9 @@
      * <p>For example,</p>
      *
      * <pre>
+     * &#64;Find
      * &#64;OrderBy("age")
-     * Stream&lt;Person&gt; findByLastName(String lastName);
+     * Stream&lt;Person&gt; withLastName(&#64;By("lastName") &#64;Is(IgnoreCase) String surname);
      * </pre>
      *
      * @return entity attribute name.
diff --git a/api/src/main/java/jakarta/data/repository/Repository.java b/api/src/main/java/jakarta/data/repository/Repository.java
index 6812adcb2..bfc59513c 100644
--- a/api/src/main/java/jakarta/data/repository/Repository.java
+++ b/api/src/main/java/jakarta/data/repository/Repository.java
@@ -37,8 +37,9 @@
  * &#64;Repository
  * public interface Products extends DataRepository&lt;Product, Long&gt; {
  *
+ *     &#64;Find
  *     &#64;OrderBy("price")
- *     List&lt;Product&gt; findByNameLike(String namePattern);
+ *     List&lt;Product&gt; named(&#64;By("name") &#64;Is(LikeIgnoreCase) String namePattern);
  *
  *     &#64;Query("UPDATE Product SET price = price - (price * ?1) WHERE price * ?1 &lt;= ?2")
  *     int putOnSale(float rateOfDiscount, float maxDiscount);
@@ -52,7 +53,7 @@
  * Products products;
  *
  * ...
- * found = products.findByNameLike("%Printer%");
+ * found = products.named("%Printer%");
  * numUpdated = products.putOnSale(0.15f, 20.0f);
  * </pre>
  *
diff --git a/api/src/main/java/module-info.java b/api/src/main/java/module-info.java
index cfb4d2f88..1638d5dde 100644
--- a/api/src/main/java/module-info.java
+++ b/api/src/main/java/module-info.java
@@ -28,6 +28,7 @@
 import jakarta.data.repository.Delete;
 import jakarta.data.repository.Find;
 import jakarta.data.repository.Insert;
+import jakarta.data.repository.Is;
 import jakarta.data.repository.OrderBy;
 import jakarta.data.repository.Param;
 import jakarta.data.repository.Query;
@@ -70,8 +71,11 @@
  *     &#64;Insert
  *     void create(Product prod);
  *
+ *     &#64;Find
  *     &#64;OrderBy("price")
- *     List&lt;Product&gt; findByNameIgnoreCaseLikeAndPriceLessThan(String namePattern, float max);
+ *     List&lt;Product&gt; search(
+ *             &#64;By("name") &#64;Is(LikeIgnoreCase) String namePattern,
+ *             &#64;By("price") &#64;Is(LessThanEqual) float max);
  *
  *     &#64;Query("UPDATE Product SET price = price * (1.0 - ?1) WHERE yearProduced &lt;= ?2")
  *     int discountOldInventory(float rateOfDiscount, int maxYear);
@@ -91,7 +95,7 @@
  * ...
  * products.create(newProduct);
  *
- * found = products.findByNameIgnoreCaseLikeAndPriceLessThan("%cell%phone%", 900.0f);
+ * found = products.search("%cell%phone%", 900.0f);
  *
  * numDiscounted = products.discountOldInventory(0.15f, Year.now().getValue() - 1);
  * </pre>
@@ -150,8 +154,10 @@
  *
  * &#64;Repository
  * public interface Purchases {
+ *     &#64;Find
  *     &#64;OrderBy("address.zipCode")
- *     List&lt;Purchase&gt; findByAddressZipCodeIn(List&lt;Integer&gt; zipCodes);
+ *     List&lt;Purchase&gt; forZipCodes(
+ *             &#64;By("address.zipCode") &#64;Is(In) List&lt;Integer&gt; zipCodes);
  *
  *     &#64;Query("WHERE address.zipCode = ?1")
  *     List&lt;Purchase&gt; forZipCode(int zipCode);
@@ -711,15 +717,19 @@
  * with the {@code -parameters} compiler option so that parameter names are
  * available at runtime.</p>
  *
- * <p>Each parameter determines a query condition, and each such condition
- * is an equality condition. All conditions must match for a record to
+ * <p>Each parameter determines a query condition. By default, each such condition
+ * is an equality condition. The {@link Is} annotation can be combined with the
+ * {@link By} annotation to request other types of basic comparisons for a
+ * repository method parameter. All conditions must match for a record to
  * satisfy the query.</p>
  *
  * <pre>
  * &#64;Find
  * &#64;OrderBy("lastName")
  * &#64;OrderBy("firstName")
- * List&lt;Person&gt; peopleByAgeAndNationality(int age, Country nationality);
+ * List&lt;Person&gt; ofNationalityAndOlderThan(
+ *         Country nationality,
+ *         &#64;By("age") &#64;Is(GreaterThan) int minAge);
  * </pre>
  *
  * <pre>
@@ -727,6 +737,11 @@
  * Optional&lt;Person&gt; person(String ssn);
  * </pre>
  *
+ * <pre>
+ * &#64;Delete
+ * void remove(&#64;By("status") &#64;Is(In) List&lt;Status&gt; list);
+ * </pre>
+ *
  * <p>The {@code _} character may be used in a method parameter name to
  * reference an embedded attribute.</p>
  *
@@ -742,11 +757,19 @@
  *
  * <pre>
  * // Query by Method Name
- * Vehicle[] findByMakeAndModelAndYear(String makerName, String model, int year, Sort&lt;?&gt;... sorts);
+ * Vehicle[] findByMakeAndModelAndYearBetween(String makerName,
+ *                                            String model,
+ *                                            int minYear,
+ *                                            int maxYear,
+ *                                            Sort&lt;?&gt;... sorts);
  *
  * // parameter-based conditions
  * &#64;Find
- * Vehicle[] searchFor(String make, String model, int year, Sort&lt;?&gt;... sorts);
+ * Vehicle[] search(String make,
+ *                  String model,
+ *                  &#64;By(_Vehicle.YEAR) &#64;Is(GreaterThanEqual) int minYear,
+ *                  &#64;By(_Vehicle.YEAR) &#64;Is(LessThanEqual) int maxYear,
+ *                  Sort&lt;?&gt;... sorts);
  * </pre>
  *
  * <p>For further information, refer to the {@linkplain Find API documentation}
@@ -784,11 +807,13 @@
  * allows its results to be split and retrieved in pages. For example,</p>
  *
  * <pre>
- * Product[] findByNameLikeOrderByAmountSoldDescIdAsc(
- *                 String pattern, PageRequest pageRequest);
+ * &#64;Find
+ * &#64;OrderBy(value = _Product.AMOUNT_SOLD, descending = true)
+ * &#64;OrderBy(ID)
+ * Product[] named(&#64;By(_Product.NAME) &#64;Is(LikeIgnoreCase) String pattern,
+ *                 PageRequest pageRequest);
  * ...
- * page1 = products.findByNameLikeOrderByAmountSoldDescIdAsc(
- *                 "%phone%", PageRequest.ofSize(20));
+ * page1 = products.named("%phone%", PageRequest.ofSize(20));
  * </pre>
  *
  * <p>When using pagination, always ensure that the ordering is consistent
@@ -802,16 +827,17 @@
  * For example,</p>
  *
  * <pre>
- * Product[] findByNameLikeAndPriceBetween(String pattern,
- *                                         float minPrice,
- *                                         float maxPrice,
- *                                         PageRequest pageRequest,
- *                                         Order&lt;Product&gt; order);
+ * &#64;Find
+ * Product[] search(&#64;By("name") &#64;Is(LikeIgnoreCase) String pattern,
+ *                  &#64;By("price") &#64;Is(GreaterThanEqual) float minPrice,
+ *                  &#64;By("price") &#64;Is(LessThanEqual) float maxPrice,
+ *                  PageRequest pageRequest,
+ *                  Order&lt;Product&gt; order);
  *
  * ...
  * PageRequest page1Request = PageRequest.ofSize(25);
  *
- * page1 = products.findByNameLikeAndPriceBetween(
+ * page1 = products.search(
  *                 namePattern, minPrice, maxPrice, page1Request,
  *                 Order.by(Sort.desc("price"), Sort.asc("id"));
  * </pre>
@@ -821,13 +847,17 @@
  * of {@link Sort} and passed to the repository find method. For example,</p>
  *
  * <pre>
- * Product[] findByNameLike(String pattern, Limit max, Order&lt;Product&gt; sortBy);
+ * &#64;Find
+ * Product[] named(&#64;By("name") &#64;Is(LikeIgnoreCase) String pattern,
+ *                 Limit max,
+ *                 Order&lt;Product&gt; sortBy);
  *
  * ...
- * found = products.findByNameLike(namePattern, Limit.of(25),
- *                                 Order.by(Sort.desc("price"),
- *                                          Sort.desc("amountSold"),
- *                                          Sort.asc("id")));
+ * found = products.named(namePattern,
+ *                        Limit.of(25),
+ *                        Order.by(Sort.desc("price"),
+ *                                 Sort.desc("amountSold"),
+ *                                 Sort.asc("id")));
  * </pre>
  *
  * <p>Generic, untyped {@link Sort} criteria can be supplied directly to a
@@ -835,13 +865,17 @@
  * For example,</p>
  *
  * <pre>
- * Product[] findByNameLike(String pattern, Limit max, {@code Sort<?>...} sortBy);
+ * &#64;Find
+ * Product[] named(&#64;By("name") &#64;Is(LikeIgnoreCase) String pattern,
+ *                 Limit max,
+ *                 {@code Sort<?>...} sortBy);
  *
  * ...
- * found = products.findByNameLike(namePattern, Limit.of(25),
- *                                 Sort.desc("price"),
- *                                 Sort.desc("amountSold"),
- *                                 Sort.asc("name"));
+ * found = products.named(namePattern,
+ *                        Limit.of(25),
+ *                        Sort.desc("price"),
+ *                        Sort.desc("amountSold"),
+ *                        Sort.asc("name"));
  * </pre>
  *
  * <h2>Returning subsets of entity attributes</h2>

From e9e7e1c4be395f3910bf02d8b4dcc7c027b7a608 Mon Sep 17 00:00:00 2001
From: Nathan Rauh <nathan.rauh@us.ibm.com>
Date: Thu, 7 Nov 2024 09:56:19 -0600
Subject: [PATCH 2/4] Switch to String constants with capitalized names

---
 api/src/main/java/jakarta/data/Limit.java     |  2 +-
 .../java/jakarta/data/page/CursoredPage.java  |  2 +-
 .../java/jakarta/data/page/PageRequest.java   |  4 +-
 .../main/java/jakarta/data/repository/Is.java | 81 ++++++++-----------
 .../java/jakarta/data/repository/OrderBy.java |  4 +-
 .../jakarta/data/repository/Repository.java   |  2 +-
 api/src/main/java/module-info.java            | 26 +++---
 7 files changed, 53 insertions(+), 68 deletions(-)

diff --git a/api/src/main/java/jakarta/data/Limit.java b/api/src/main/java/jakarta/data/Limit.java
index d06981a2e..33270b342 100644
--- a/api/src/main/java/jakarta/data/Limit.java
+++ b/api/src/main/java/jakarta/data/Limit.java
@@ -34,7 +34,7 @@
  *
  * <pre>
  * &#64;Find
- * Product[] named(&#64;By(_Product.NAME) &#64;Is(LikeIgnoreCase) String namePattern,
+ * Product[] named(&#64;By(_Product.NAME) &#64;Is(LIKE_IGNORE_CASE) String namePattern,
  *                 Limit limit,
  *                 Sort&lt;Product&gt;... sorts);
  * 
diff --git a/api/src/main/java/jakarta/data/page/CursoredPage.java b/api/src/main/java/jakarta/data/page/CursoredPage.java
index 71770f8ce..4716e0c3b 100644
--- a/api/src/main/java/jakarta/data/page/CursoredPage.java
+++ b/api/src/main/java/jakarta/data/page/CursoredPage.java
@@ -60,7 +60,7 @@
  * &#64;OrderBy(_Employee.FIRSTNAME)
  * &#64;OrderBy(_Employee.ID)
  * CursoredPage&lt;Employee&gt; withOvertime(
- *         &#64;By(_Employee.HOURSWORKED) &#64;Is(GreaterThan) int fullTimeHours,
+ *         &#64;By(_Employee.HOURSWORKED) &#64;Is(GREATER_THAN) int fullTimeHours,
  *         PageRequest pageRequest);
  * </pre>
  *
diff --git a/api/src/main/java/jakarta/data/page/PageRequest.java b/api/src/main/java/jakarta/data/page/PageRequest.java
index f7d90a4b6..b779a9369 100644
--- a/api/src/main/java/jakarta/data/page/PageRequest.java
+++ b/api/src/main/java/jakarta/data/page/PageRequest.java
@@ -40,8 +40,8 @@
  * &#64;Find
  * &#64;OrderBy("age")
  * &#64;OrderBy("ssn")
- * Person[] agedBetween(&#64;By("age") &#64;Is(GreaterThanEqual) int minAge,
- *                      &#64;By("age") &#64;Is(LessThanEqual) int maxAge,
+ * Person[] agedBetween(&#64;By("age") &#64;Is(GREATER_THAN_EQ) int minAge,
+ *                      &#64;By("age") &#64;Is(LESS_THAN_EQ) int maxAge,
  *                      PageRequest pageRequest);
  * </pre>
  *
diff --git a/api/src/main/java/jakarta/data/repository/Is.java b/api/src/main/java/jakarta/data/repository/Is.java
index 20adb800c..ac1cfea1b 100644
--- a/api/src/main/java/jakarta/data/repository/Is.java
+++ b/api/src/main/java/jakarta/data/repository/Is.java
@@ -36,26 +36,45 @@
  *
  *     // Find all Product entities where the price field is less than a maximum value.
  *     &#64;Find
- *     List&lt;Product&gt; pricedBelow(&#64;By(_Product.PRICE) &#64;Is(LessThan) float max);
+ *     List&lt;Product&gt; pricedBelow(&#64;By(_Product.PRICE) &#64;Is(LESS_THAN) float max);
  *
  *     // Find a page of Product entities where the name field matches a pattern, ignoring case.
  *     &#64;Find
- *     Page&lt;Product&gt; search(&#64;By(_Product.NAME) &#64;Is(LikeIgnoreCase) String pattern,
+ *     Page&lt;Product&gt; search(&#64;By(_Product.NAME) &#64;Is(LIKE_IGNORE_CASE) String pattern,
  *                          PageRequest pagination,
  *                          Order&lt;Product&gt; order);
  *
  *     // Remove Product entities with any of the unique identifiers listed.
  *     &#64;Delete
- *     void remove(&#64;By(ID) &#64;Is(In) List&lt;Long&gt; productIds);
+ *     void remove(&#64;By(ID) &#64;Is(IN) List&lt;Long&gt; productIds);
  * }
  * </pre>
  */
 @Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.PARAMETER)
 public @interface Is {
+    // TODO add JavaDoc with examples to these
+    String EQUAL                = "EQUAL";
+    String GREATER_THAN         = "GREATER_THAN";
+    String GREATER_THAN_EQ      = "GREATER_THAN_EQ";
+    String IGNORE_CASE          = "IGNORE_CASE";
+    String IN                   = "IN";
+    String LESS_THAN            = "LESS_THAN";
+    String LESS_THAN_EQ         = "LESS_THAN_EQ";
+    String LIKE                 = "LIKE";
+    String LIKE_IGNORE_CASE     = "LIKE_IGNORE_CASE";
+    String NOT                  = "NOT";
+    String NOT_IGNORE_CASE      = "NOT_IGNORE_CASE";
+    String NOT_IN               = "NOT_IN";
+    String NOT_LIKE             = "NOT_LIKE";
+    String NOT_LIKE_IGNORE_CASE = "NOT_LIKE_IGNORE_CASE";
+
     /**
      * <p>The type of comparison operation to use when comparing a persistent
-     * field against a value that is supplied to a repository method.</p>
+     * field against a value that is supplied to a repository method.
+     * For portable applications, the comparison operation must be one of the
+     * constants defined within this class. Jakarta Data providers might choose
+     * to provide their own constants as non-portable extensions.</p>
      *
      * <p>The following example compares the year a person was born against
      * a minimum and maximum year that are supplied as parameters to a repository
@@ -64,55 +83,21 @@
      * <pre>
      * &#64;Find
      * &#64;OrderBy(_Person.YEAR_BORN)
-     * List&lt;Person&gt; bornWithin(&#64;By(_Person.YEAR_BORN) &#64;Is(GreaterThanEqual) float minYear,
-     *                         &#64;By(_Person.YEAR_BORN) &#64;Is(LessThanEqual) float maxYear);
+     * List&lt;Person&gt; bornWithin(&#64;By(_Person.YEAR_BORN) &#64;Is(TREATER_THAN_EQ) float minYear,
+     *                         &#64;By(_Person.YEAR_BORN) &#64;Is(LESS_THAN_EQ) float maxYear);
      * </pre>
      *
-     * <p>The default comparison operation is the {@linkplain Op#Equal equality}
-     * comparison.</p> 
-     *
-     * @return the type of comparison operation.
-     */
-    Op value() default Op.Equal;
-
-    /**
-     * <p>Comparison operations for the {@link Is} annotation.</p>
+     * <p>The default comparison operation is the {@linkplain #EQUAL equality}
+     * comparison.</p>
      *
-     * <p>For more concise code, it can be convenient to statically import one
-     * or more comparison operations. For example:</p>
+     * <p>For concise code, it can be convenient for a repository interface to
+     * statically import one or more constants from this class. For example:</p>
      *
      * <pre>
-     * import static jakarta.data.repository.Is.Op.*;
+     * import static jakarta.data.repository.Is.*;
      * </pre>
+     *
+     * @return the type of comparison operation.
      */
-    public static enum Op {
-        // TODO add JavaDoc with examples to these
-        Equal,
-        GreaterThan,
-        GreaterThanEqual,
-        IgnoreCase,
-        In,
-        LessThan,
-        LessThanEqual,
-        Like,
-        LikeIgnoreCase,
-        // TODO might want to give more thought to the exact names,
-        Prefixed,
-        PrefixedIgnoreCase,
-        Substringed,
-        SubstringedIgnoreCase,
-        Suffixed,
-        SuffixedIgnoreCase,
-        Not,
-        NotIgnoreCase,
-        NotIn,
-        NotLike,
-        NotLikeIgnoreCase,
-        NotPrefixed,
-        NotPrefixedIgnoreCase,
-        NotSubstringed,
-        NotSubstringedIgnoreCase,
-        NotSuffixed,
-        NotSuffixedIgnoreCase;
-    }
+    String value() default EQUAL;
 }
diff --git a/api/src/main/java/jakarta/data/repository/OrderBy.java b/api/src/main/java/jakarta/data/repository/OrderBy.java
index 8c1822508..c6ce93653 100644
--- a/api/src/main/java/jakarta/data/repository/OrderBy.java
+++ b/api/src/main/java/jakarta/data/repository/OrderBy.java
@@ -64,7 +64,7 @@
  * <pre>
  * &#64;Find
  * &#64;OrderBy(value = _Product.PRICE, descending = true)
- * {@code Stream<Product>} pricedBelow(&#64;By(_Product.PRICE) &#64;Is(LessThan) double maxPrice);
+ * {@code Stream<Product>} pricedBelow(&#64;By(_Product.PRICE) &#64;Is(LESS_THAN) double maxPrice);
  * </pre>
  *
  * <p>A repository method with an {@code @OrderBy} annotation must not
@@ -118,7 +118,7 @@
      * <pre>
      * &#64;Find
      * &#64;OrderBy("age")
-     * Stream&lt;Person&gt; withLastName(&#64;By("lastName") &#64;Is(IgnoreCase) String surname);
+     * Stream&lt;Person&gt; withLastName(&#64;By("lastName") &#64;Is(IGNORE_CASE) String surname);
      * </pre>
      *
      * @return entity attribute name.
diff --git a/api/src/main/java/jakarta/data/repository/Repository.java b/api/src/main/java/jakarta/data/repository/Repository.java
index bfc59513c..93d7bb82d 100644
--- a/api/src/main/java/jakarta/data/repository/Repository.java
+++ b/api/src/main/java/jakarta/data/repository/Repository.java
@@ -39,7 +39,7 @@
  *
  *     &#64;Find
  *     &#64;OrderBy("price")
- *     List&lt;Product&gt; named(&#64;By("name") &#64;Is(LikeIgnoreCase) String namePattern);
+ *     List&lt;Product&gt; named(&#64;By("name") &#64;Is(LIKE_IGNORE_CASE) String namePattern);
  *
  *     &#64;Query("UPDATE Product SET price = price - (price * ?1) WHERE price * ?1 &lt;= ?2")
  *     int putOnSale(float rateOfDiscount, float maxDiscount);
diff --git a/api/src/main/java/module-info.java b/api/src/main/java/module-info.java
index 1638d5dde..789c762ef 100644
--- a/api/src/main/java/module-info.java
+++ b/api/src/main/java/module-info.java
@@ -74,8 +74,8 @@
  *     &#64;Find
  *     &#64;OrderBy("price")
  *     List&lt;Product&gt; search(
- *             &#64;By("name") &#64;Is(LikeIgnoreCase) String namePattern,
- *             &#64;By("price") &#64;Is(LessThanEqual) float max);
+ *             &#64;By("name") &#64;Is(LIKE_IGNORE_CASE) String namePattern,
+ *             &#64;By("price") &#64;Is(lESS_THAN_EQ) float max);
  *
  *     &#64;Query("UPDATE Product SET price = price * (1.0 - ?1) WHERE yearProduced &lt;= ?2")
  *     int discountOldInventory(float rateOfDiscount, int maxYear);
@@ -157,7 +157,7 @@
  *     &#64;Find
  *     &#64;OrderBy("address.zipCode")
  *     List&lt;Purchase&gt; forZipCodes(
- *             &#64;By("address.zipCode") &#64;Is(In) List&lt;Integer&gt; zipCodes);
+ *             &#64;By("address.zipCode") &#64;Is(IN) List&lt;Integer&gt; zipCodes);
  *
  *     &#64;Query("WHERE address.zipCode = ?1")
  *     List&lt;Purchase&gt; forZipCode(int zipCode);
@@ -729,7 +729,7 @@
  * &#64;OrderBy("firstName")
  * List&lt;Person&gt; ofNationalityAndOlderThan(
  *         Country nationality,
- *         &#64;By("age") &#64;Is(GreaterThan) int minAge);
+ *         &#64;By("age") &#64;Is(GREATER_THAN) int minAge);
  * </pre>
  *
  * <pre>
@@ -739,7 +739,7 @@
  *
  * <pre>
  * &#64;Delete
- * void remove(&#64;By("status") &#64;Is(In) List&lt;Status&gt; list);
+ * void remove(&#64;By("status") &#64;Is(IN) List&lt;Status&gt; list);
  * </pre>
  *
  * <p>The {@code _} character may be used in a method parameter name to
@@ -767,8 +767,8 @@
  * &#64;Find
  * Vehicle[] search(String make,
  *                  String model,
- *                  &#64;By(_Vehicle.YEAR) &#64;Is(GreaterThanEqual) int minYear,
- *                  &#64;By(_Vehicle.YEAR) &#64;Is(LessThanEqual) int maxYear,
+ *                  &#64;By(_Vehicle.YEAR) &#64;Is(GREATER_THAN_EQ) int minYear,
+ *                  &#64;By(_Vehicle.YEAR) &#64;Is(LESS_THAN_EQ) int maxYear,
  *                  Sort&lt;?&gt;... sorts);
  * </pre>
  *
@@ -810,7 +810,7 @@
  * &#64;Find
  * &#64;OrderBy(value = _Product.AMOUNT_SOLD, descending = true)
  * &#64;OrderBy(ID)
- * Product[] named(&#64;By(_Product.NAME) &#64;Is(LikeIgnoreCase) String pattern,
+ * Product[] named(&#64;By(_Product.NAME) &#64;Is(LIKE_IGNORE_CASE) String pattern,
  *                 PageRequest pageRequest);
  * ...
  * page1 = products.named("%phone%", PageRequest.ofSize(20));
@@ -828,9 +828,9 @@
  *
  * <pre>
  * &#64;Find
- * Product[] search(&#64;By("name") &#64;Is(LikeIgnoreCase) String pattern,
- *                  &#64;By("price") &#64;Is(GreaterThanEqual) float minPrice,
- *                  &#64;By("price") &#64;Is(LessThanEqual) float maxPrice,
+ * Product[] search(&#64;By("name") &#64;Is(LIKE_IGNORE_CASE) String pattern,
+ *                  &#64;By("price") &#64;Is(GREATER_THAN_EQ) float minPrice,
+ *                  &#64;By("price") &#64;Is(LESS_THAN_EQ) float maxPrice,
  *                  PageRequest pageRequest,
  *                  Order&lt;Product&gt; order);
  *
@@ -848,7 +848,7 @@
  *
  * <pre>
  * &#64;Find
- * Product[] named(&#64;By("name") &#64;Is(LikeIgnoreCase) String pattern,
+ * Product[] named(&#64;By("name") &#64;Is(LIKE_IGNORE_CASE) String pattern,
  *                 Limit max,
  *                 Order&lt;Product&gt; sortBy);
  *
@@ -866,7 +866,7 @@
  *
  * <pre>
  * &#64;Find
- * Product[] named(&#64;By("name") &#64;Is(LikeIgnoreCase) String pattern,
+ * Product[] named(&#64;By("name") &#64;Is(LIKE_IGNORE_CASE) String pattern,
  *                 Limit max,
  *                 {@code Sort<?>...} sortBy);
  *

From cd19343c5a3d25be57f40e2ca186e5abb21ff88f Mon Sep 17 00:00:00 2001
From: Nathan Rauh <nathan.rauh@us.ibm.com>
Date: Fri, 8 Nov 2024 13:44:03 -0600
Subject: [PATCH 3/4] Put the enumeration back, but keep the upper case, and
 shorten IGNORE_CASE to ANY_CASE

---
 api/src/main/java/jakarta/data/Limit.java     |  2 +-
 .../main/java/jakarta/data/repository/Is.java | 78 +++++++++++++------
 .../java/jakarta/data/repository/OrderBy.java |  2 +-
 .../jakarta/data/repository/Repository.java   |  2 +-
 api/src/main/java/module-info.java            | 10 +--
 5 files changed, 61 insertions(+), 33 deletions(-)

diff --git a/api/src/main/java/jakarta/data/Limit.java b/api/src/main/java/jakarta/data/Limit.java
index 33270b342..ddd1a150d 100644
--- a/api/src/main/java/jakarta/data/Limit.java
+++ b/api/src/main/java/jakarta/data/Limit.java
@@ -34,7 +34,7 @@
  *
  * <pre>
  * &#64;Find
- * Product[] named(&#64;By(_Product.NAME) &#64;Is(LIKE_IGNORE_CASE) String namePattern,
+ * Product[] named(&#64;By(_Product.NAME) &#64;Is(LIKE_ANY_CASE) String namePattern,
  *                 Limit limit,
  *                 Sort&lt;Product&gt;... sorts);
  * 
diff --git a/api/src/main/java/jakarta/data/repository/Is.java b/api/src/main/java/jakarta/data/repository/Is.java
index ac1cfea1b..c12eade51 100644
--- a/api/src/main/java/jakarta/data/repository/Is.java
+++ b/api/src/main/java/jakarta/data/repository/Is.java
@@ -25,8 +25,10 @@
 /**
  * <p>Annotates a parameter of a repository {@link Find} or {@link Delete} method,
  * indicating how a persistent field is compared against the parameter's value.
- * The {@link By} annotation is used on the same parameter to identify the
- * persistent field.</p>
+ * The {@link By} annotation can be used on the same parameter to identify the
+ * persistent field. Otherwise, if the {@code -parameters} compile option is
+ * enabled, the the persistent field is inferred by matching the name of the
+ * method parameter.</p>
  *
  * <p>For example,</p>
  *
@@ -40,7 +42,7 @@
  *
  *     // Find a page of Product entities where the name field matches a pattern, ignoring case.
  *     &#64;Find
- *     Page&lt;Product&gt; search(&#64;By(_Product.NAME) &#64;Is(LIKE_IGNORE_CASE) String pattern,
+ *     Page&lt;Product&gt; search(&#64;By(_Product.NAME) &#64;Is(LIKE_ANY_CASE) String pattern,
  *                          PageRequest pagination,
  *                          Order&lt;Product&gt; order);
  *
@@ -53,28 +55,10 @@
 @Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.PARAMETER)
 public @interface Is {
-    // TODO add JavaDoc with examples to these
-    String EQUAL                = "EQUAL";
-    String GREATER_THAN         = "GREATER_THAN";
-    String GREATER_THAN_EQ      = "GREATER_THAN_EQ";
-    String IGNORE_CASE          = "IGNORE_CASE";
-    String IN                   = "IN";
-    String LESS_THAN            = "LESS_THAN";
-    String LESS_THAN_EQ         = "LESS_THAN_EQ";
-    String LIKE                 = "LIKE";
-    String LIKE_IGNORE_CASE     = "LIKE_IGNORE_CASE";
-    String NOT                  = "NOT";
-    String NOT_IGNORE_CASE      = "NOT_IGNORE_CASE";
-    String NOT_IN               = "NOT_IN";
-    String NOT_LIKE             = "NOT_LIKE";
-    String NOT_LIKE_IGNORE_CASE = "NOT_LIKE_IGNORE_CASE";
 
     /**
      * <p>The type of comparison operation to use when comparing a persistent
-     * field against a value that is supplied to a repository method.
-     * For portable applications, the comparison operation must be one of the
-     * constants defined within this class. Jakarta Data providers might choose
-     * to provide their own constants as non-portable extensions.</p>
+     * field against a value that is supplied to a repository method..</p>
      *
      * <p>The following example compares the year a person was born against
      * a minimum and maximum year that are supplied as parameters to a repository
@@ -83,7 +67,7 @@
      * <pre>
      * &#64;Find
      * &#64;OrderBy(_Person.YEAR_BORN)
-     * List&lt;Person&gt; bornWithin(&#64;By(_Person.YEAR_BORN) &#64;Is(TREATER_THAN_EQ) float minYear,
+     * List&lt;Person&gt; bornWithin(&#64;By(_Person.YEAR_BORN) &#64;Is(GREATER_THAN_EQ) float minYear,
      *                         &#64;By(_Person.YEAR_BORN) &#64;Is(LESS_THAN_EQ) float maxYear);
      * </pre>
      *
@@ -94,10 +78,54 @@
      * statically import one or more constants from this class. For example:</p>
      *
      * <pre>
-     * import static jakarta.data.repository.Is.*;
+     * import static jakarta.data.repository.Is.Op.*;
      * </pre>
      *
      * @return the type of comparison operation.
      */
-    String value() default EQUAL;
+    Op value() default Op.EQUAL;
+
+    /**
+     * <p>Comparison operations for the {@link Is} annotation.</p>
+     *
+     * <p>For more concise code, it can be convenient to statically import one
+     * or more comparison operations. For example:</p>
+     *
+     * <pre>
+     * import static jakarta.data.repository.Is.Op.*;
+     * </pre>
+     */
+    public static enum Op {
+        // TODO add JavaDoc with examples to these
+        ANY_CASE,
+        EQUAL,
+        GREATER_THAN,
+        GREATER_THAN_ANY_CASE,
+        GREATER_THAN_EQ,
+        GREATER_THAN_EQ_ANY_CASE,
+        IN,
+        LESS_THAN,
+        LESS_THAN_ANY_CASE,
+        LESS_THAN_EQ,
+        LESS_THAN_EQ_ANY_CASE,
+        LIKE,
+        LIKE_ANY_CASE,
+        PREFIXED,
+        PREFIXED_ANY_CASE,
+        SUBSTRINGED,
+        SUBSTRINGED_ANY_CASE,
+        SUFFIXED,
+        SUFFIXED_ANY_CASE,
+        NOT,
+        NOT_ANY_CASE,
+        NOT_IN,
+        NOT_LIKE,
+        NOT_LIKE_ANY_CASE,
+        NOT_PREFIXED,
+        NOT_PREFIXED_ANY_CASE,
+        NOT_SUBSTRINGED,
+        NOT_SUBSTRINGED_ANY_CASE,
+        NOT_SUFFIXED,
+        NOT_SUFFIXED_ANY_CASE;
+    }
 }
diff --git a/api/src/main/java/jakarta/data/repository/OrderBy.java b/api/src/main/java/jakarta/data/repository/OrderBy.java
index c6ce93653..8d84373ec 100644
--- a/api/src/main/java/jakarta/data/repository/OrderBy.java
+++ b/api/src/main/java/jakarta/data/repository/OrderBy.java
@@ -118,7 +118,7 @@
      * <pre>
      * &#64;Find
      * &#64;OrderBy("age")
-     * Stream&lt;Person&gt; withLastName(&#64;By("lastName") &#64;Is(IGNORE_CASE) String surname);
+     * Stream&lt;Person&gt; withLastName(&#64;By("lastName") &#64;Is(ANY_CASE) String surname);
      * </pre>
      *
      * @return entity attribute name.
diff --git a/api/src/main/java/jakarta/data/repository/Repository.java b/api/src/main/java/jakarta/data/repository/Repository.java
index 93d7bb82d..d020f4d4e 100644
--- a/api/src/main/java/jakarta/data/repository/Repository.java
+++ b/api/src/main/java/jakarta/data/repository/Repository.java
@@ -39,7 +39,7 @@
  *
  *     &#64;Find
  *     &#64;OrderBy("price")
- *     List&lt;Product&gt; named(&#64;By("name") &#64;Is(LIKE_IGNORE_CASE) String namePattern);
+ *     List&lt;Product&gt; named(&#64;By("name") &#64;Is(LIKE_ANY_CASE) String namePattern);
  *
  *     &#64;Query("UPDATE Product SET price = price - (price * ?1) WHERE price * ?1 &lt;= ?2")
  *     int putOnSale(float rateOfDiscount, float maxDiscount);
diff --git a/api/src/main/java/module-info.java b/api/src/main/java/module-info.java
index 789c762ef..87281f858 100644
--- a/api/src/main/java/module-info.java
+++ b/api/src/main/java/module-info.java
@@ -74,7 +74,7 @@
  *     &#64;Find
  *     &#64;OrderBy("price")
  *     List&lt;Product&gt; search(
- *             &#64;By("name") &#64;Is(LIKE_IGNORE_CASE) String namePattern,
+ *             &#64;By("name") &#64;Is(LIKE_ANY_CASE) String namePattern,
  *             &#64;By("price") &#64;Is(lESS_THAN_EQ) float max);
  *
  *     &#64;Query("UPDATE Product SET price = price * (1.0 - ?1) WHERE yearProduced &lt;= ?2")
@@ -810,7 +810,7 @@
  * &#64;Find
  * &#64;OrderBy(value = _Product.AMOUNT_SOLD, descending = true)
  * &#64;OrderBy(ID)
- * Product[] named(&#64;By(_Product.NAME) &#64;Is(LIKE_IGNORE_CASE) String pattern,
+ * Product[] named(&#64;By(_Product.NAME) &#64;Is(LIKE_ANY_CASE) String pattern,
  *                 PageRequest pageRequest);
  * ...
  * page1 = products.named("%phone%", PageRequest.ofSize(20));
@@ -828,7 +828,7 @@
  *
  * <pre>
  * &#64;Find
- * Product[] search(&#64;By("name") &#64;Is(LIKE_IGNORE_CASE) String pattern,
+ * Product[] search(&#64;By("name") &#64;Is(LIKE_ANY_CASE) String pattern,
  *                  &#64;By("price") &#64;Is(GREATER_THAN_EQ) float minPrice,
  *                  &#64;By("price") &#64;Is(LESS_THAN_EQ) float maxPrice,
  *                  PageRequest pageRequest,
@@ -848,7 +848,7 @@
  *
  * <pre>
  * &#64;Find
- * Product[] named(&#64;By("name") &#64;Is(LIKE_IGNORE_CASE) String pattern,
+ * Product[] named(&#64;By("name") &#64;Is(LIKE_ANY_CASE) String pattern,
  *                 Limit max,
  *                 Order&lt;Product&gt; sortBy);
  *
@@ -866,7 +866,7 @@
  *
  * <pre>
  * &#64;Find
- * Product[] named(&#64;By("name") &#64;Is(LIKE_IGNORE_CASE) String pattern,
+ * Product[] named(&#64;By("name") &#64;Is(LIKE_ANY_CASE) String pattern,
  *                 Limit max,
  *                 {@code Sort<?>...} sortBy);
  *

From a55eebad5c5bc65cec7e25bc6c68442a29d98e68 Mon Sep 17 00:00:00 2001
From: Nathan Rauh <nathan.rauh@us.ibm.com>
Date: Mon, 17 Feb 2025 11:33:02 -0600
Subject: [PATCH 4/4] Reuse existing Operator enum

---
 api/src/main/java/jakarta/data/Limit.java     |  2 +-
 .../data/metamodel/restrict/Operator.java     | 23 ++++++-
 .../java/jakarta/data/page/PageRequest.java   |  4 +-
 .../jakarta/data/repository/IgnoreCase.java   | 60 +++++++++++++++++++
 .../main/java/jakarta/data/repository/Is.java | 60 +++----------------
 .../java/jakarta/data/repository/OrderBy.java |  2 +-
 .../jakarta/data/repository/Repository.java   |  2 +-
 api/src/main/java/module-info.java            | 20 +++----
 8 files changed, 106 insertions(+), 67 deletions(-)
 create mode 100644 api/src/main/java/jakarta/data/repository/IgnoreCase.java

diff --git a/api/src/main/java/jakarta/data/Limit.java b/api/src/main/java/jakarta/data/Limit.java
index ddd1a150d..7f144f252 100644
--- a/api/src/main/java/jakarta/data/Limit.java
+++ b/api/src/main/java/jakarta/data/Limit.java
@@ -34,7 +34,7 @@
  *
  * <pre>
  * &#64;Find
- * Product[] named(&#64;By(_Product.NAME) &#64;Is(LIKE_ANY_CASE) String namePattern,
+ * Product[] named(&#64;By(_Product.NAME) &#64;Is(LIKE) &#64;IgnoreCase String namePattern,
  *                 Limit limit,
  *                 Sort&lt;Product&gt;... sorts);
  * 
diff --git a/api/src/main/java/jakarta/data/metamodel/restrict/Operator.java b/api/src/main/java/jakarta/data/metamodel/restrict/Operator.java
index 8cb178c10..84015544d 100644
--- a/api/src/main/java/jakarta/data/metamodel/restrict/Operator.java
+++ b/api/src/main/java/jakarta/data/metamodel/restrict/Operator.java
@@ -27,7 +27,13 @@ public enum Operator {
     LIKE,
     NOT_EQUAL,
     NOT_IN,
-    NOT_LIKE;
+    NOT_LIKE,
+    NOT_PREFIXED,
+    NOT_SUBSTRINGED,
+    NOT_SUFFIXED,
+    PREFIXED,
+    SUBSTRINGED,
+    SUFFIXED;
 
     /**
      * Representation of the operator as it appears in query language.
@@ -35,6 +41,9 @@ public enum Operator {
      * in query langugae.
      *
      * @return the representation of the operator in query language.
+     *         For operators that have a more complex representation in
+     *         query language, this method returns the {@link #name()}
+     *         of the operator.
      */
     String asQueryLanguage() {
         return switch (this) {
@@ -48,6 +57,12 @@ String asQueryLanguage() {
             case NOT_EQUAL -> "<>";
             case NOT_IN -> "NOT IN";
             case NOT_LIKE -> "NOT LIKE";
+            case NOT_PREFIXED -> NOT_PREFIXED.name();
+            case NOT_SUBSTRINGED -> NOT_SUBSTRINGED.name();
+            case NOT_SUFFIXED -> NOT_SUFFIXED.name();
+            case PREFIXED -> PREFIXED.name();
+            case SUBSTRINGED -> SUBSTRINGED.name();
+            case SUFFIXED -> SUFFIXED.name();
         };
     }
 
@@ -68,6 +83,12 @@ Operator negate() {
             case NOT_EQUAL -> EQUAL;
             case NOT_IN -> IN;
             case NOT_LIKE -> LIKE;
+            case NOT_PREFIXED -> PREFIXED;
+            case NOT_SUBSTRINGED -> SUBSTRINGED;
+            case NOT_SUFFIXED -> SUFFIXED;
+            case PREFIXED -> NOT_PREFIXED;
+            case SUBSTRINGED -> NOT_SUBSTRINGED;
+            case SUFFIXED -> NOT_SUFFIXED;
         };
     }
 }
diff --git a/api/src/main/java/jakarta/data/page/PageRequest.java b/api/src/main/java/jakarta/data/page/PageRequest.java
index b779a9369..d88dc6a9c 100644
--- a/api/src/main/java/jakarta/data/page/PageRequest.java
+++ b/api/src/main/java/jakarta/data/page/PageRequest.java
@@ -40,8 +40,8 @@
  * &#64;Find
  * &#64;OrderBy("age")
  * &#64;OrderBy("ssn")
- * Person[] agedBetween(&#64;By("age") &#64;Is(GREATER_THAN_EQ) int minAge,
- *                      &#64;By("age") &#64;Is(LESS_THAN_EQ) int maxAge,
+ * Person[] agedBetween(&#64;By("age") &#64;Is(GREATER_THAN_EQUAL) int minAge,
+ *                      &#64;By("age") &#64;Is(LESS_THAN_EQUAL) int maxAge,
  *                      PageRequest pageRequest);
  * </pre>
  *
diff --git a/api/src/main/java/jakarta/data/repository/IgnoreCase.java b/api/src/main/java/jakarta/data/repository/IgnoreCase.java
new file mode 100644
index 000000000..da69c020c
--- /dev/null
+++ b/api/src/main/java/jakarta/data/repository/IgnoreCase.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ * 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.repository;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import jakarta.data.metamodel.restrict.Operator;
+
+/**
+ * <p>Annotates a parameter of a repository {@link Find} or {@link Delete} method,
+ * indicating that a persistent field should be compared ignoring case.
+ * The {@link By} annotation can be used on the same parameter to identify the
+ * persistent field. Otherwise, if the {@code -parameters} compile option is
+ * enabled, the the persistent field is inferred by matching the name of the
+ * method parameter. The {@link Operator#EQUAL EQUAL} comparison is assumed
+ * unless the {@link Is} annotation is used on the same parameter to choose a
+ * different type of comparison.</p>
+ *
+ * <p>For example,</p>
+ *
+ * <pre>
+ * &#64;Repository
+ * public interface People extends BasicRepository&lt;Person, Long&gt; {
+ *
+ *     // Find all Person entities where the lastName matches the respective value
+ *     // ignoring case.
+ *     &#64;Find
+ *     List&lt;Person&gt; withSurname(&#64;By(_Person.LASTNAME) &#64;IgnoreCase String surname);
+ *
+ *     // Find a page of Person entities where the lastName field begins with the
+ *     // supplied prefix, ignoring case.
+ *     &#64;Find
+ *     Page&lt;Person&gt; withSurnamePrefix(&#64;By(_Product.LASTNAME) &#64;Is(PREFIXED) &#64;IgnoreCase String prefix,
+ *                                    PageRequest pagination,
+ *                                    Order&lt;Person&gt; order);
+ * }
+ * </pre>
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface IgnoreCase {
+}
diff --git a/api/src/main/java/jakarta/data/repository/Is.java b/api/src/main/java/jakarta/data/repository/Is.java
index c12eade51..3d3d6ba43 100644
--- a/api/src/main/java/jakarta/data/repository/Is.java
+++ b/api/src/main/java/jakarta/data/repository/Is.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.
@@ -22,6 +22,8 @@
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
+import jakarta.data.metamodel.restrict.Operator;
+
 /**
  * <p>Annotates a parameter of a repository {@link Find} or {@link Delete} method,
  * indicating how a persistent field is compared against the parameter's value.
@@ -42,7 +44,7 @@
  *
  *     // Find a page of Product entities where the name field matches a pattern, ignoring case.
  *     &#64;Find
- *     Page&lt;Product&gt; search(&#64;By(_Product.NAME) &#64;Is(LIKE_ANY_CASE) String pattern,
+ *     Page&lt;Product&gt; search(&#64;By(_Product.NAME) &#64;Is(LIKE) &#64;IgnoreCase String pattern,
  *                          PageRequest pagination,
  *                          Order&lt;Product&gt; order);
  *
@@ -67,65 +69,21 @@
      * <pre>
      * &#64;Find
      * &#64;OrderBy(_Person.YEAR_BORN)
-     * List&lt;Person&gt; bornWithin(&#64;By(_Person.YEAR_BORN) &#64;Is(GREATER_THAN_EQ) float minYear,
-     *                         &#64;By(_Person.YEAR_BORN) &#64;Is(LESS_THAN_EQ) float maxYear);
+     * List&lt;Person&gt; bornWithin(&#64;By(_Person.YEAR_BORN) &#64;Is(GREATER_THAN_EQUAL) float minYear,
+     *                         &#64;By(_Person.YEAR_BORN) &#64;Is(LESS_THAN_EQUAL) float maxYear);
      * </pre>
      *
-     * <p>The default comparison operation is the {@linkplain #EQUAL equality}
+     * <p>The default comparison operation is the {@linkplain Operator#EQUAL equality}
      * comparison.</p>
      *
      * <p>For concise code, it can be convenient for a repository interface to
      * statically import one or more constants from this class. For example:</p>
      *
      * <pre>
-     * import static jakarta.data.repository.Is.Op.*;
+     * import static jakarta.data.metamodel.restrict.Operator.*;
      * </pre>
      *
      * @return the type of comparison operation.
      */
-    Op value() default Op.EQUAL;
-
-    /**
-     * <p>Comparison operations for the {@link Is} annotation.</p>
-     *
-     * <p>For more concise code, it can be convenient to statically import one
-     * or more comparison operations. For example:</p>
-     *
-     * <pre>
-     * import static jakarta.data.repository.Is.Op.*;
-     * </pre>
-     */
-    public static enum Op {
-        // TODO add JavaDoc with examples to these
-        ANY_CASE,
-        EQUAL,
-        GREATER_THAN,
-        GREATER_THAN_ANY_CASE,
-        GREATER_THAN_EQ,
-        GREATER_THAN_EQ_ANY_CASE,
-        IN,
-        LESS_THAN,
-        LESS_THAN_ANY_CASE,
-        LESS_THAN_EQ,
-        LESS_THAN_EQ_ANY_CASE,
-        LIKE,
-        LIKE_ANY_CASE,
-        PREFIXED,
-        PREFIXED_ANY_CASE,
-        SUBSTRINGED,
-        SUBSTRINGED_ANY_CASE,
-        SUFFIXED,
-        SUFFIXED_ANY_CASE,
-        NOT,
-        NOT_ANY_CASE,
-        NOT_IN,
-        NOT_LIKE,
-        NOT_LIKE_ANY_CASE,
-        NOT_PREFIXED,
-        NOT_PREFIXED_ANY_CASE,
-        NOT_SUBSTRINGED,
-        NOT_SUBSTRINGED_ANY_CASE,
-        NOT_SUFFIXED,
-        NOT_SUFFIXED_ANY_CASE;
-    }
+    Operator value() default Operator.EQUAL;
 }
diff --git a/api/src/main/java/jakarta/data/repository/OrderBy.java b/api/src/main/java/jakarta/data/repository/OrderBy.java
index 8d84373ec..ff1e1c6cc 100644
--- a/api/src/main/java/jakarta/data/repository/OrderBy.java
+++ b/api/src/main/java/jakarta/data/repository/OrderBy.java
@@ -118,7 +118,7 @@
      * <pre>
      * &#64;Find
      * &#64;OrderBy("age")
-     * Stream&lt;Person&gt; withLastName(&#64;By("lastName") &#64;Is(ANY_CASE) String surname);
+     * Stream&lt;Person&gt; withLastName(&#64;By("lastName") &#64;IgnoreCase String surname);
      * </pre>
      *
      * @return entity attribute name.
diff --git a/api/src/main/java/jakarta/data/repository/Repository.java b/api/src/main/java/jakarta/data/repository/Repository.java
index d020f4d4e..f3e820680 100644
--- a/api/src/main/java/jakarta/data/repository/Repository.java
+++ b/api/src/main/java/jakarta/data/repository/Repository.java
@@ -39,7 +39,7 @@
  *
  *     &#64;Find
  *     &#64;OrderBy("price")
- *     List&lt;Product&gt; named(&#64;By("name") &#64;Is(LIKE_ANY_CASE) String namePattern);
+ *     List&lt;Product&gt; named(&#64;By("name") &#64;Is(LIKE) &#64;IgnoreCase String namePattern);
  *
  *     &#64;Query("UPDATE Product SET price = price - (price * ?1) WHERE price * ?1 &lt;= ?2")
  *     int putOnSale(float rateOfDiscount, float maxDiscount);
diff --git a/api/src/main/java/module-info.java b/api/src/main/java/module-info.java
index 87281f858..4be5dae70 100644
--- a/api/src/main/java/module-info.java
+++ b/api/src/main/java/module-info.java
@@ -74,8 +74,8 @@
  *     &#64;Find
  *     &#64;OrderBy("price")
  *     List&lt;Product&gt; search(
- *             &#64;By("name") &#64;Is(LIKE_ANY_CASE) String namePattern,
- *             &#64;By("price") &#64;Is(lESS_THAN_EQ) float max);
+ *             &#64;By("name") &#64;Is(LIKE) &#64;IgnoreCase String namePattern,
+ *             &#64;By("price") &#64;Is(lESS_THAN_EQUAL) float max);
  *
  *     &#64;Query("UPDATE Product SET price = price * (1.0 - ?1) WHERE yearProduced &lt;= ?2")
  *     int discountOldInventory(float rateOfDiscount, int maxYear);
@@ -767,8 +767,8 @@
  * &#64;Find
  * Vehicle[] search(String make,
  *                  String model,
- *                  &#64;By(_Vehicle.YEAR) &#64;Is(GREATER_THAN_EQ) int minYear,
- *                  &#64;By(_Vehicle.YEAR) &#64;Is(LESS_THAN_EQ) int maxYear,
+ *                  &#64;By(_Vehicle.YEAR) &#64;Is(GREATER_THAN_EQUAL) int minYear,
+ *                  &#64;By(_Vehicle.YEAR) &#64;Is(LESS_THAN_EQUAL) int maxYear,
  *                  Sort&lt;?&gt;... sorts);
  * </pre>
  *
@@ -810,7 +810,7 @@
  * &#64;Find
  * &#64;OrderBy(value = _Product.AMOUNT_SOLD, descending = true)
  * &#64;OrderBy(ID)
- * Product[] named(&#64;By(_Product.NAME) &#64;Is(LIKE_ANY_CASE) String pattern,
+ * Product[] named(&#64;By(_Product.NAME) &#64;Is(LIKE) &#64;IgnoreCase String pattern,
  *                 PageRequest pageRequest);
  * ...
  * page1 = products.named("%phone%", PageRequest.ofSize(20));
@@ -828,9 +828,9 @@
  *
  * <pre>
  * &#64;Find
- * Product[] search(&#64;By("name") &#64;Is(LIKE_ANY_CASE) String pattern,
- *                  &#64;By("price") &#64;Is(GREATER_THAN_EQ) float minPrice,
- *                  &#64;By("price") &#64;Is(LESS_THAN_EQ) float maxPrice,
+ * Product[] search(&#64;By("name") &#64;Is(LIKE) &#64;IgnoreCase String pattern,
+ *                  &#64;By("price") &#64;Is(GREATER_THAN_EQUAL) float minPrice,
+ *                  &#64;By("price") &#64;Is(LESS_THAN_EQUAL) float maxPrice,
  *                  PageRequest pageRequest,
  *                  Order&lt;Product&gt; order);
  *
@@ -848,7 +848,7 @@
  *
  * <pre>
  * &#64;Find
- * Product[] named(&#64;By("name") &#64;Is(LIKE_ANY_CASE) String pattern,
+ * Product[] named(&#64;By("name") &#64;Is(LIKE) &#64;IgnoreCase String pattern,
  *                 Limit max,
  *                 Order&lt;Product&gt; sortBy);
  *
@@ -866,7 +866,7 @@
  *
  * <pre>
  * &#64;Find
- * Product[] named(&#64;By("name") &#64;Is(LIKE_ANY_CASE) String pattern,
+ * Product[] named(&#64;By("name") &#64;Is(LIKE) &#64;IgnoreCase String pattern,
  *                 Limit max,
  *                 {@code Sort<?>...} sortBy);
  *