Repository: jakartaee/data
Pull request #99 (2022-12-06 21:27:21)

Case insensitive comparisons
Existing repository implementations have repository keywords ([Micronaut](https://micronaut-projects.github.io/micronaut-data/latest/guide/), [Spring](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/)) and other [methods](https://docs.spring.io/spring-data/commons/docs/1.9.1.RELEASE/api/org/springframework/data/domain/Sort.Order.html#ignoreCase--) to indicate case insensitive comparisons for string properties.  These are useful so this pull adds to Jakarta Data.

Signed-off-by: Nathan Rauh <nathan.rauh@us.ibm.com>


Patch:
diff --git a/api/src/main/java/jakarta/data/repository/OrderBy.java b/api/src/main/java/jakarta/data/repository/OrderBy.java
index 86c1c296..a735e9f9 100644
--- a/api/src/main/java/jakarta/data/repository/OrderBy.java
+++ b/api/src/main/java/jakarta/data/repository/OrderBy.java
@@ -70,6 +70,18 @@ public @interface OrderBy {
      */
     boolean descending() default false;
 
+    /**
+     * <p>Indicates whether or not to request case insensitive ordering
+     * from a database with case sensitive collation.
+     * A database with case insensitive collation performs case insensitive
+     * ordering regardless of the requested <code>ignoreCase</code> value.</p>
+     *
+     * <p>The default value is <code>false</code>.</p>
+     *
+     * @return whether or not to request case insensitive sorting for the property.
+     */
+    boolean ignoreCase() default false;
+
     /**
      * <p>Entity attribute name to sort by.</p>
      *
diff --git a/api/src/main/java/jakarta/data/repository/Repository.java b/api/src/main/java/jakarta/data/repository/Repository.java
index 4dfa4548..5ba8b5e5 100644
--- a/api/src/main/java/jakarta/data/repository/Repository.java
+++ b/api/src/main/java/jakarta/data/repository/Repository.java
@@ -187,6 +187,14 @@ import java.lang.annotation.Target;
  * <td>Requires that the entity's attribute value be at least as big as the parameter value.</td>
  * <td><code>findByAgeGreaterThanEqual(minimumAge)</code></td></tr>
  *
+ * <tr style="vertical-align: top"><td><code>IgnoreCase</code></td>
+ * <td>strings</td>
+ * <td>Requires case insensitive comparison. For query conditions
+ * as well as ordering, the <code>IgnoreCase</code> keyword can be
+ * specified immediately following the entity property name.</td>
+ * <td><code>countByStatusIgnoreCaseNotLike("%Delivered%")</code>
+ * <br><code>findByZipcodeOrderByStreetIgnoreCaseAscHouseNumAsc(55904)</code></td></tr>
+ *
  * <tr style="vertical-align: top"><td><code>In</code></td>
  * <td>all attribute types</td>
  * <td>Requires that the entity's attribute value be within the list that is the parameter value.</td>
diff --git a/api/src/main/java/jakarta/data/repository/Sort.java b/api/src/main/java/jakarta/data/repository/Sort.java
index 76a46d9e..93b45d8f 100644
--- a/api/src/main/java/jakarta/data/repository/Sort.java
+++ b/api/src/main/java/jakarta/data/repository/Sort.java
@@ -56,9 +56,12 @@ public final class Sort {
 
     private final Direction direction;
 
-    private Sort(String property, Direction direction) {
+    private final boolean ignoreCase;
+
+    private Sort(String property, Direction direction, boolean ignoreCase) {
         this.property = property;
         this.direction = direction;
+        this.ignoreCase = ignoreCase;
     }
 
     /**
@@ -68,6 +71,18 @@ public final class Sort {
         return this.property;
     }
 
+    /**
+     * <p>Indicates whether or not to request case insensitive ordering
+     * from a database with case sensitive collation.
+     * A database with case insensitive collation performs case insensitive
+     * ordering regardless of the requested <code>ignoreCase</code> value.</p>
+     *
+     * @return Returns whether or not to request case insensitive sorting for the property.
+     */
+    public boolean ignoreCase() {
+        return ignoreCase;
+    }
+
     /**
      * @return Returns whether sorting for this property shall be ascending.
      */
@@ -91,56 +106,85 @@ public final class Sort {
             return false;
         }
         Sort sort = (Sort) o;
-        return Objects.equals(property, sort.property) && direction == sort.direction;
+        return Objects.equals(property, sort.property) && direction == sort.direction && ignoreCase == sort.ignoreCase;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(property, direction);
+        return Objects.hash(property, direction, ignoreCase);
     }
 
     @Override
     public String toString() {
-        return "Sort{" +
-                "property='" + property + '\'' +
-                ", direction=" + direction +
-                '}';
+        StringBuilder s = new StringBuilder(property.length() + 32)
+                .append("Sort{property='").append(property)
+                .append("', direction=").append(direction);
+        if (ignoreCase)
+            s.append(", ignore case");
+        s.append('}');
+        return s.toString();
     }
 
     /**
      * Create a {@link Sort} instance
      *
      * @param property  the property name to order by
-     * @param direction The direction order by
+     * @param direction the direction in which to order.
+     * @param ignoreCase whether to request a case insensitive ordering.
      * @return an {@link Sort} instance. Never {@code null}.
      * @throws NullPointerException when there is a null parameter
      */
-    public static Sort of(String property, Direction direction) {
+    public static Sort of(String property, Direction direction, boolean ignoreCase) {
         Objects.requireNonNull(property, "property is required");
         Objects.requireNonNull(direction, "direction is required");
-        return new Sort(property, direction);
+        return new Sort(property, direction, ignoreCase);
     }
 
     /**
      * Create a {@link Sort} instance with ascending direction {@link  Direction#ASC}
+     * that does not request case insensitive ordering.
      *
      * @param property the property name to order by
      * @return a {@link Sort} instance. Never {@code null}.
      * @throws NullPointerException when the property is null
      */
     public static Sort asc(String property) {
-        return of(property, Direction.ASC);
+        return of(property, Direction.ASC, false);
+    }
+
+    /**
+     * Create a {@link Sort} instance with ascending direction {@link  Direction#ASC}
+     * and case insensitive ordering.
+     *
+     * @param property the property name to order by.
+     * @return a {@link Sort} instance. Never {@code null}.
+     * @throws NullPointerException when the property is null.
+     */
+    public static Sort ascIgnoreCase(String property) {
+        return of(property, Direction.ASC, true);
     }
 
     /**
-     * Create a {@link Sort} instance on descending direction {@link  Direction#DESC}
+     * Create a {@link Sort} instance with descending direction {@link  Direction#DESC}
+     * that does not request case insensitive ordering.
      *
      * @param property the property name to order by
      * @return a {@link Sort} instance. Never {@code null}.
      * @throws NullPointerException when the property is null
      */
     public static Sort desc(String property) {
-        return of(property, Direction.DESC);
+        return of(property, Direction.DESC, false);
     }
 
+    /**
+     * Create a {@link Sort} instance with descending direction {@link  Direction#DESC}
+     * and case insensitive ordering.
+     *
+     * @param property the property name to order by.
+     * @return a {@link Sort} instance. Never {@code null}.
+     * @throws NullPointerException when the property is null.
+     */
+    public static Sort descIgnoreCase(String property) {
+        return of(property, Direction.DESC, true);
+    }
 }
\ No newline at end of file
diff --git a/spec/src/main/asciidoc/repository.asciidoc b/spec/src/main/asciidoc/repository.asciidoc
index 8fb2b625..baf38a80 100644
--- a/spec/src/main/asciidoc/repository.asciidoc
+++ b/spec/src/main/asciidoc/repository.asciidoc
@@ -221,6 +221,10 @@ Jakarta Data implementations often support the following list of predicate keywo
 |Finds string values "like" the given expression
 |findByTitleLike
 
+|IgnoreCase
+|Requests that string values be compared independent of case for query conditions and ordering.
+|findByStreetNameIgnoreCaseLike
+
 |In
 |Find results where the property is one of the values that are contained within the given list
 |findByIdIn


Test patch:
diff --git a/api/src/test/java/jakarta/data/repository/SortTest.java b/api/src/test/java/jakarta/data/repository/SortTest.java
index 0a96555d..cb2fbce9 100644
--- a/api/src/test/java/jakarta/data/repository/SortTest.java
+++ b/api/src/test/java/jakarta/data/repository/SortTest.java
@@ -29,34 +29,36 @@ class SortTest {
     @Test
     @DisplayName("Should throw NullPointerException when one of the properties are null")
     void shouldReturnErrorWhenPropertyDirectionNull() {
-        assertThatNullPointerException().isThrownBy(() -> Sort.of(null, null));
-        assertThatNullPointerException().isThrownBy(() -> Sort.of(NAME, null));
-        assertThatNullPointerException().isThrownBy(() -> Sort.of(null, Direction.ASC));
+        assertThatNullPointerException().isThrownBy(() -> Sort.of(null, null, false));
+        assertThatNullPointerException().isThrownBy(() -> Sort.of(NAME, null, true));
+        assertThatNullPointerException().isThrownBy(() -> Sort.of(null, Direction.ASC, false));
     }
 
     @Test
-    @DisplayName("Should ascending short when direction is ASC")
+    @DisplayName("Should use ascending sort when direction is ASC")
     void shouldCreateAscendingSort() {
-        Sort order = Sort.of(NAME, Direction.ASC);
+        Sort order = Sort.of(NAME, Direction.ASC, false);
 
         assertSoftly(softly -> {
             softly.assertThat(order).isNotNull();
             softly.assertThat(order.property()).isEqualTo(NAME);
             softly.assertThat(order.isAscending()).isTrue();
             softly.assertThat(order.isDescending()).isFalse();
+            softly.assertThat(order.ignoreCase()).isFalse();
         });
     }
 
     @Test
     @DisplayName("Should descending short when direction is DESC")
     void shouldCreateDescendingSort() {
-        Sort order = Sort.of(NAME, Direction.DESC);
+        Sort order = Sort.of(NAME, Direction.DESC, true);
 
         assertSoftly(softly -> {
             softly.assertThat(order).isNotNull();
             softly.assertThat(order.property()).isEqualTo(NAME);
             softly.assertThat(order.isAscending()).isFalse();
             softly.assertThat(order.isDescending()).isTrue();
+            softly.assertThat(order.ignoreCase()).isTrue();
         });
     }
 
@@ -70,6 +72,21 @@ class SortTest {
             softly.assertThat(order.property()).isEqualTo(NAME);
             softly.assertThat(order.isAscending()).isTrue();
             softly.assertThat(order.isDescending()).isFalse();
+            softly.assertThat(order.ignoreCase()).isFalse();
+        });
+    }
+
+    @Test
+    @DisplayName("Should use ascending sort ignoring case when Sort.ascIgnoreCase method is used")
+    void shouldCreateAscIgnoreCase() {
+        Sort order = Sort.ascIgnoreCase("name");
+
+        assertSoftly(softly -> {
+            softly.assertThat(order).isNotNull();
+            softly.assertThat(order.property()).isEqualTo(NAME);
+            softly.assertThat(order.isAscending()).isTrue();
+            softly.assertThat(order.isDescending()).isFalse();
+            softly.assertThat(order.ignoreCase()).isTrue();
         });
     }
 
@@ -83,6 +100,21 @@ class SortTest {
             softly.assertThat(order.property()).isEqualTo(NAME);
             softly.assertThat(order.isAscending()).isFalse();
             softly.assertThat(order.isDescending()).isTrue();
+            softly.assertThat(order.ignoreCase()).isFalse();
+        });
+    }
+
+    @Test
+    @DisplayName("Should use descending sort ignoring case when Sort.descIgnoreCase method is used")
+    void shouldCreateDescIgnoreCase() {
+        Sort order = Sort.descIgnoreCase(NAME);
+
+        assertSoftly(softly -> {
+            softly.assertThat(order).isNotNull();
+            softly.assertThat(order.property()).isEqualTo(NAME);
+            softly.assertThat(order.isAscending()).isFalse();
+            softly.assertThat(order.isDescending()).isTrue();
+            softly.assertThat(order.ignoreCase()).isTrue();
         });
     }
 }