Repository: jakartaee/data
Pull request #683 (2024-04-10 18:04:59)

PageRequest toString wrongly states a cursor has multiple keys
toString of both Pagination (PageRequest) and PageRequestCursor (Cursor) both output a string that says the cursor has X number of keys. This is inaccurate. There is only one key, which is formed by all of the values.  This PR makes corrections to the toString output.


Patch:
diff --git a/api/src/main/java/jakarta/data/page/PageRequestCursor.java b/api/src/main/java/jakarta/data/page/PageRequestCursor.java
index 655051f2..1bc97f59 100644
--- a/api/src/main/java/jakarta/data/page/PageRequestCursor.java
+++ b/api/src/main/java/jakarta/data/page/PageRequestCursor.java
@@ -38,7 +38,7 @@ class PageRequestCursor implements PageRequest.Cursor {
     PageRequestCursor(Object... key) {
         this.key = key;
         if (key == null || key.length == 0)
-            throw new IllegalArgumentException("No key values were provided.");
+            throw new IllegalArgumentException("No values were provided.");
     }
 
     @Override
@@ -69,6 +69,6 @@ class PageRequestCursor implements PageRequest.Cursor {
     @Override
     public String toString() {
         return "Cursor@" + Integer.toHexString(hashCode()) +
-                " with " + key.length + " keys";
+                " with " + key.length + " values";
     }
 }
diff --git a/api/src/main/java/jakarta/data/page/Pagination.java b/api/src/main/java/jakarta/data/page/Pagination.java
index b80f4868..ed46f091 100644
--- a/api/src/main/java/jakarta/data/page/Pagination.java
+++ b/api/src/main/java/jakarta/data/page/Pagination.java
@@ -68,7 +68,7 @@ record Pagination(long page, int size, Mode mode, Cursor type, boolean requestTo
                 .append(", size=").append(size)
                 .append(", mode=").append(mode);
         if (type != null) {
-            s.append(", ").append(type.size()).append(" keys");
+            s.append(", cursor size=").append(type.size());
         }
         return s.append("}").toString();
     }
diff --git a/spec/src/main/asciidoc/method-query.asciidoc b/spec/src/main/asciidoc/method-query.asciidoc
index e08acd35..dbbb4373 100644
--- a/spec/src/main/asciidoc/method-query.asciidoc
+++ b/spec/src/main/asciidoc/method-query.asciidoc
@@ -57,8 +57,6 @@ Therefore, these alternative approaches are strongly preferred for newly-written
 
 A Jakarta Data provider is required to support the Query by Method Name extension in Jakarta Data 1.0.
 
-NOTE: A Jakarta Data provider backed by a key-value or wide-column datastore is not required to support Query by Method Name.
-
 WARNING: This functionality is considered deprecated, and the requirement that a provider support the Query by Method Name extension will be removed in a future version of Jakarta Data.
 
 == Query by Method Name
@@ -84,8 +82,8 @@ The parsing of query method names follows a specific format:
 
 - The method name consists of the subject, the predicate, and optionally the order clause.
 - The subject begins with the action (such as `find` or `delete`) and is optionally followed by an expression (for example, `First10`), followed by any number of other characters, followed by `By`.
-- The predicate defines the condition or filtering criteria, where multiple conditions are delimited by `And` or `Or`. For example, `PriceLessThanAndNameLike`. Each condition consists of a property name, optionally followed by `IgnoreCase` (for text properties), optionally followed by `Not`, optionally followed by an operator such as `LessThan` or `Like`. Absence of the operator implies the equality condition.
-- The order clause, which is optional, begins with `OrderBy` and is followed by one or more ordered pairings consisting of entity attribute name and direction of sorting (`Asc` or `Desc`). The direction can be omitted if there is only one property, in which case `Asc` is implied. Ordering of results is performed by sorting the first entity attribute according to its respective direction, where results that sort to the same position are subsequently sorted by the second entity attribute according to its respective direction, and so forth.
+- The predicate defines the query's condition or filtering criteria, where multiple conditions are delimited by `And` or `Or`. For example, `PriceLessThanAndNameLike`. Each condition consists of a property name, optionally followed by `IgnoreCase` (for text properties), optionally followed by `Not`, optionally followed by an operator such as `LessThan` or `Like`. Absence of the operator implies the equality condition.
+- The order clause, which is optional, begins with `OrderBy` and is followed by one or more ordered pairings consisting of entity attribute name and direction of sorting (`Asc` or `Desc`). The direction can be omitted on the final property, in which case `Asc` is implied. Ordering of results is performed by sorting the first entity attribute according to its respective direction, where results that sort to the same position are subsequently sorted by the second entity attribute according to its respective direction, and so forth.
 - The method name is formed by combining the subject, predicate, and order clause, in that order.
 
 NOTE: This specification uses the terms subject and predicate in a way that aligns with industry terminology rather than how they are defined in English grammar.
@@ -94,11 +92,11 @@ Queries can also handle entities with relation attributes by specifying the rela
 
 Example query methods:
 
-- `findByName(String name)` -- find entities by the `name` property.
-- `findByAgeGreaterThan(int age)` -- find entities where `age` is greater than the specified value.
-- `findByAuthorName(String authorName)` -- find entities by the `authorName` property of a related entity.
-- `findByCategoryNameAndPriceLessThan(String categoryName, double price)` -- find entities by `categoryName` and `price` properties, applying an `And` condition.
-- `findByNameLikeOrderByPriceDescIdAsc` -- find entities by matching the `name` property against a pattern, sorting the results by `price` in descending order, and sorting results with the same `price` by the `id` in ascending order.
+- `findByName(String name)`: Find entities by the 'name' property.
+- `findByAgeGreaterThan(int age)`: Find entities where 'age' is greater than the specified value.
+- `findByAuthorName(String authorName)`: Find entities by the 'authorName' property of a related entity.
+- `findByCategoryNameAndPriceLessThan(String categoryName, double price)`: Find entities by 'categoryName' and 'price' properties, applying an 'And' condition.
+- `findByNameLikeOrderByPriceDescIdAsc`: Find entities by matching the 'name' property against a pattern, sorting the results by 'price' in descending order, and sorting results with the same 'price' by the 'id' in ascending order.
 
 === BNF Grammar for Query Methods
 
@@ -134,8 +132,6 @@ order : "OrderBy" (property | orderItem+)
 orderItem : property ("Asc" | "Desc")
 ----
 
-Quoted names are considered case-sensitive keywords.
-
 Explanation of the BNF elements:
 
 - `query`: May be a `find` query, or a `delete`, `count`, or `exists` operation.
@@ -155,139 +151,152 @@ Explanation of the BNF elements:
 
 === Query by Method Name Keywords
 
-An implementation of Query by Method Name must support the following types of operation.
+The following tables list the _Query by Method Name_ keywords that must be supported by Jakarta Data providers, except where explicitly indicated for a type of database.
 
-.Query by Method Name Actions
-[cols="12,~"]
+[cols="12,~,25"]
 |===
-| Action | Description
+| Action | Description | Not Required For
 
 |`find`
 |Returns entity instances representing the records which satisfy the restriction, or representing all records if there is no restriction.
+|Key-value, Wide-Column
 
 |`delete`
 |Deletes every record which satisfy the restriction, or all records if there is no restriction, and returns either no result (`void`) or the number of records deleted.
+|Key-value, Wide-Column
 
 |`count`
 |Returns the number of records which satisfy the restriction, or the total number of records if there is no restriction.
+|Key-value, Wide-Column
 
 |`exists`
 |Returns `true` if at least one record satisfies the restriction or if there is at least one record in the database when there is no restriction.
+|Key-value, Wide-Column
 |===
 
-An implementation of Query by Method Name must support the following keywords.
+[NOTE]
+====
+The "Not Required For" column indicates the database types for which the respective keyword is not required or applicable.
+====
+Jakarta Data implementations must support the following list of Query by Method Name keywords, except where indicated for a database type. A repository method must raise `java.lang.UnsupportedOperationException` or a more specific subclass of the exception if the database does not provide the requested functionality.
 
-.Query by Method Name Keywords
-[cols="15,~,~"]
+[cols="15,~,~,25"]
 |===
-| Keyword | Description | Method signature example
+| Keyword |Description | Method signature Sample | Not Required For
 
 |`And`
 |The `And` operator requires both conditions to match.
 |`findByNameAndYear`
+|Key-value, Wide-Column
 
 |`Or`
 |The `Or` operator requires at least one of the conditions to match.
 |`findByNameOrYear`
+|Key-value, Wide-Column
 
 |`Not`
 |Negates the condition that immediately follows the `Not` keyword. When used without a subsequent keyword, means not equal to.
 |`findByNameNotLike`
+|Key-value, Wide-Column
 
-|`First`
-|For a query with ordered results, limits the quantity of results to the number following First, or if there is no subsequent number, to a single result.
-|`findFirst10By`
-
-|`OrderBy`
-|Specify a static sorting order followed by one or more ordered pairings of a property path and direction (`Asc` or `Desc`). The direction `Asc` can be omitted from the final property listed, in which case ascending order is implied for that property.
-|`findByAgeOrderByHeightDescIdAsc` `findByAgeOrderById`
-
-|`Desc`
-|Specify a static sorting order of descending.
-|`findByNameOrderByAgeDesc`
-
-|`Asc`
-|Specify a static sorting order of ascending.
-|`findByNameOrderByAgeAsc`
-
-|===
-
-For relational databases, the logical operator `And` takes precedence over `Or`, meaning that `And` is evaluated on conditions before `Or` when both are specified on the same method. For other database types, the precedence is limited to the capabilities of the database. For example, some graph databases are limited to precedence in traversal order.
-
-NOTE: An implementation of Query by Method Name backed by a document or graph database is not required to support the `First` keyword. A repository method must raise `java.lang.UnsupportedOperationException` or a more specific subclass of the exception if the database does not support this functionality.
-
-=== Query by Method Name Conditions
-
-In addition to equality conditions, Query by Method Name defines the following kinds of condition.
-
-.Query by Method Name Conditions
-[cols="15,10,^12,~,~"]
-|===
-| Keyword | Property type | Parameters  | Description | Method signature example
-
-|`Between` | Any sortable type | 2
+|`Between`
 |Find results where the property is between (inclusive of) two given values, with the first value being the inclusive minimum and the second value being the inclusive maximum.
 |`findByDateBetween`
+|Key-value, Wide-Column
 
-|`Contains` | `String` | 1
-|Matches string values with the given substring, which can be a pattern.
+|`Contains`
+|Matches String values with the given substring, which can be a pattern.
 |`findByProductNameContains`
+|Key-value, Wide-Column, Document, Graph
 
-|`EndsWith` | `String` | 1
+|`EndsWith`
 |Matches String values with the given ending, which can be a pattern.
 |`findByProductNameEndsWith`
+|Key-value, Wide-Column, Document, Graph
+
+|`First`
+|For a query with ordered results, limits the quantity of results to the number following First, or if there is no subsequent number, to a single result.
+|`findFirst10By`
+|Key-value, Wide-Column, Document, Graph
 
-|`LessThan` | Any sortable type | 1
+|`LessThan`
 |Find results where the property is less than the given value
 |`findByAgeLessThan`
+|Key-value, Wide-Column
 
-|`GreaterThan` | Any sortable type | 1
+|`GreaterThan`
 |Find results where the property is greater than the given value
 |`findByAgeGreaterThan`
+|Key-value, Wide-Column
 
-|`LessThanEqual` | Any sortable type | 1
+|`LessThanEqual`
 |Find results where the property is less than or equal to the given value
 |`findByAgeLessThanEqual`
+|Key-value, Wide-Column
 
-|`GreaterThanEqual` | Any sortable type | 1
+|`GreaterThanEqual`
 |Find results where the property is greater than or equal to the given value
 |`findByAgeGreaterThanEqual`
+|Key-value, Wide-Column
 
-|`Like` | `String` | 1
-|Matches string values against the given pattern.
+|`Like`
+|Matches String values against the given pattern.
 |`findByTitleLike`
+|Key-value, Wide-Column, Document, Graph
 
-|`IgnoreCase` | `String` |
+|`IgnoreCase`
 |Requests that string values be compared independent of case for query conditions and ordering.
 |`findByStreetNameIgnoreCaseLike`
+|Key-value, Wide-Column, Document, Graph
 
-|`In` | Any type | 1 `Set`
+|`In`
 |Find results where the property is one of the values that are contained within the given `Set`.
 |`findByIdIn`
+|Key-value, Wide-Column, Document, Graph
 
-|`Null` | Any type | 0
+|`Null`
 |Finds results where the property has a null value.
 |`findByYearRetiredNull`
+|Key-value, Wide-Column, Document, Graph
 
-|`StartsWith` | `String` | 1
+|`StartsWith`
 |Matches String values with the given beginning, which can be a pattern.
 |`findByFirstNameStartsWith`
+|Key-value, Wide-Column, Document, Graph
 
-|`True` | `Boolean` or `boolean` | 0
-|Finds results where the property has a boolean value of `true`.
+|`True`
+|Finds results where the property has a boolean value of true.
 |`findBySalariedTrue`
+|Key-value, Wide-Column
 
-|`False` | `Boolean` or `boolean` | 0
-|Finds results where the property has a boolean value of `false`.
+|`False`
+|Finds results where the property has a boolean value of false.
 |`findByCompletedFalse`
+|Key-value, Wide-Column
 
-|===
+|`OrderBy`
+|Specify a static sorting order followed by one or more ordered pairings of a property path and direction (`Asc` or `Desc`). The direction `Asc` can be omitted from the final property listed, in which case ascending order is implied for that property.
+|`findByAgeOrderByHeightDescIdAsc` `findByAgeOrderById`
+|Key-value, Wide-Column
+
+|`Desc`
+|Specify a static sorting order of descending.
+|`findByNameOrderByAgeDesc`
+|Key-value, Wide-Column
+
+|`Asc`
+|Specify a static sorting order of ascending.
+|`findByNameOrderByAgeAsc`
+|Key-value, Wide-Column
 
-Most _Query by Method Name_ conditions require a single repository method parameter. The `Between` condition requires two parameters. `Null`, `True`, and `False` require none. An `In` condition requires a parameter of type `Set<T>` where `T` is the type of the property. The repository method parameters used for _Query by Method Name_ conditions follow the order in which the _Query by Method Name_ conditions appear within the method name.
+|===
 
-Wildcard characters for patterns are determined by the data store. For relational databases, `_` matches any one character and `%` matches zero or more characters.
+[NOTE]
+====
+The "Not Required For" column indicates the database types for which the respective keyword is not required or applicable.
+====
 
-NOTE: An implementation of Query by Method Name backed by a document or graph database is not required to support `Contains`, `EndsWith`, `StartsWith`, `Like`, `IgnoreCase`, `In`, or `Null`. A repository method must raise `java.lang.UnsupportedOperationException` or a more specific subclass of the exception if the database does not provide the requested functionality.
+Most _Query by Method Name_ conditions require a single repository method parameter. The `Between` condition requires two parameters. `Null`, `True`, and `False` require none. The repository method parameters used for _Query by Method Name_ conditions follow the order in which the _Query by Method Name_ conditions appear within the method name.
 
 In the following example the value of the first parameter, `namePattern`, is used for `NameLike`, the values of the second and third parameters, `minYear` and `maxYear`, are used for `YearMadeBetween`, and the value of the fourth parameter, `maxPrice`, is used for `PriceLessThan`.
 
@@ -301,11 +310,18 @@ List<Product> findByNameLikeAndYearMadeBetweenAndPriceLessThan(String namePatter
                                                                Order<Product> sortBy)
 ----
 
+==== Patterns
+
+Wildcard characters for patterns are determined by the data access provider. For relational databases, `_` matches any one character and `%` matches 0 or more characters.
+
+==== Logical Operator Precedence
+
+For relational databases, the logical operator `And` takes precedence over `Or`, meaning that `And` is evaluated on conditions before `Or` when both are specified on the same method. For other database types, the precedence is limited to the capabilities of the database. For example, some graph databases are limited to precedence in traversal order.
+
 === Return Types
 
 The return type of a Query by Method Name is determined as indicated in the following table, where `E` is the queried entity type.
 
-.Repository Method Return Types
 [cols="25,20,~"]
 |===
 | Operation | Return type | Notes
@@ -337,7 +353,7 @@ Resolution of a persistent field involves the following steps:
 
 4. If no matching persistent field is found in either of the previous steps, the provider is permitted to reject the query method or to throw `UnsupportedOperationException` when the method is called.
 
-A persistent field name used in a _Query by Method Name_ must not contain a keyword reserved by the <<BNF Grammar for Query Methods,grammar>>.
+A persistent field name used in a _Query by Method Name_ must not contain a <<Query by Method Name Keywords,keyword>> reserved by _Query by Method Name_.
 
 ==== Scenario 1: Person Repository with Unambiguous Resolution
 


Test patch:
diff --git a/api/src/test/java/jakarta/data/page/PageRequestCursorTest.java b/api/src/test/java/jakarta/data/page/PageRequestCursorTest.java
index 66b7ca04..231cd9dc 100644
--- a/api/src/test/java/jakarta/data/page/PageRequestCursorTest.java
+++ b/api/src/test/java/jakarta/data/page/PageRequestCursorTest.java
@@ -130,10 +130,10 @@ class PageRequestCursorTest {
         assertSoftly(softly -> {
 
             softly.assertThat(afterKeySet.toString())
-              .isEqualTo("PageRequest{page=1, size=200, mode=CURSOR_NEXT, 2 keys}");
+              .isEqualTo("PageRequest{page=1, size=200, mode=CURSOR_NEXT, cursor size=2}");
 
             softly.assertThat(beforeKeySet.toString())
-                    .isEqualTo("PageRequest{page=1, size=100, mode=CURSOR_PREVIOUS, 2 keys}");
+                    .isEqualTo("PageRequest{page=1, size=100, mode=CURSOR_PREVIOUS, cursor size=2}");
 
         });
     }