From fe93ee7aa391455ed64f998fa11c0dd91186a4ad Mon Sep 17 00:00:00 2001
From: Nathan Rauh
Date: Thu, 12 Mar 2026 15:05:16 -0500
Subject: [PATCH 1/4] Tests for Sorts obtained from Path attributes
---
.../expression/path/ComparablePathTest.java | 5 +-
.../expression/path/NavigablePathTest.java | 162 ++++++++++++++++--
2 files changed, 151 insertions(+), 16 deletions(-)
diff --git a/api/src/test/java/jakarta/data/spi/expression/path/ComparablePathTest.java b/api/src/test/java/jakarta/data/spi/expression/path/ComparablePathTest.java
index 209c91800..1585bf173 100644
--- a/api/src/test/java/jakarta/data/spi/expression/path/ComparablePathTest.java
+++ b/api/src/test/java/jakarta/data/spi/expression/path/ComparablePathTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2025,2026 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.
@@ -21,7 +21,6 @@
import jakarta.data.expression.NavigableExpression;
import jakarta.data.metamodel.ComparableAttribute;
import jakarta.data.metamodel.NavigableAttribute;
-import jakarta.data.spi.expression.path.ComparablePath;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.DisplayName;
@@ -56,7 +55,7 @@ void shouldCreateComparablePath() {
NavigableExpression expr = _Author.publisher;
ComparableAttribute attr = _Publisher.rating;
- var path = ComparablePath.of(expr, attr);
+ var path = expr.navigate(attr);
SoftAssertions.assertSoftly(soft -> {
soft.assertThat(path).isNotNull();
diff --git a/api/src/test/java/jakarta/data/spi/expression/path/NavigablePathTest.java b/api/src/test/java/jakarta/data/spi/expression/path/NavigablePathTest.java
index 52d0d1561..4fbfa4cf0 100644
--- a/api/src/test/java/jakarta/data/spi/expression/path/NavigablePathTest.java
+++ b/api/src/test/java/jakarta/data/spi/expression/path/NavigablePathTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2025,2026 Contributors to the Eclipse Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,9 +17,15 @@
*/
package jakarta.data.spi.expression.path;
+import java.time.LocalDate;
+
import jakarta.data.expression.NavigableExpression;
+import jakarta.data.metamodel.BooleanAttribute;
+import jakarta.data.metamodel.ComparableAttribute;
import jakarta.data.metamodel.NavigableAttribute;
-import jakarta.data.spi.expression.path.NavigablePath;
+import jakarta.data.metamodel.NumericAttribute;
+import jakarta.data.metamodel.TemporalAttribute;
+import jakarta.data.metamodel.TextAttribute;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.DisplayName;
@@ -30,17 +36,48 @@
*/
class NavigablePathTest {
- static class Publisher {
+ static enum BusinessType {
+ COMMERCIAL,
+ GOVERNMENT,
+ NONPROFIT
+ }
+
+ static class BusinessInfo {
+ Boolean active;
+ LocalDate founded;
String name;
+ BusinessType type;
+ int zipcode;
+ }
+
+ static class Publisher {
+ BusinessInfo info;
}
static class Book {
Publisher publisher;
}
+ interface _BusinessInfo {
+ BooleanAttribute active =
+ BooleanAttribute.of(BusinessInfo.class, "active", Boolean.class);
+
+ TemporalAttribute founded =
+ TemporalAttribute.of(BusinessInfo.class, "founded", LocalDate.class);
+
+ TextAttribute name =
+ TextAttribute.of(BusinessInfo.class, "name");
+
+ ComparableAttribute type =
+ ComparableAttribute.of(BusinessInfo.class, "type", BusinessType.class);
+
+ NumericAttribute zipcode =
+ NumericAttribute.of(BusinessInfo.class, "zipcode", int.class);
+ }
+
interface _Publisher {
- NavigableAttribute name =
- NavigableAttribute.of(Publisher.class, "name", String.class);
+ NavigableAttribute info =
+ NavigableAttribute.of(Publisher.class, "info", BusinessInfo.class);
}
interface _Book {
@@ -49,18 +86,117 @@ interface _Book {
}
@Test
- @DisplayName("should create NavigablePath from expression and attribute")
+ @DisplayName("should create BooleanPath from path and attribute")
+ void shouldCreateBooleanPath() {
+ BooleanAttribute publisherInfoActive =
+ _Book.publisher.navigate(_Publisher.info)
+ .navigate(_BusinessInfo.active);
+
+ SoftAssertions.assertSoftly(soft -> {
+ soft.assertThat(publisherInfoActive.name())
+ .isEqualTo("publisher.info.active");
+ soft.assertThat(publisherInfoActive.asc().property())
+ .isEqualTo("publisher.info.active");
+ soft.assertThat(publisherInfoActive.desc().property())
+ .isEqualTo("publisher.info.active");
+ soft.assertThat(publisherInfoActive.toString())
+ .isEqualTo("book.publisher.info.active");
+ });
+ }
+
+ @Test
+ @DisplayName("should create ComparablePath from path and attribute")
+ void shouldCreateComparablePath() {
+ ComparableAttribute publisherBusinessInfoType =
+ _Book.publisher.navigate(_Publisher.info)
+ .navigate(_BusinessInfo.type);
+
+ SoftAssertions.assertSoftly(soft -> {
+ soft.assertThat(publisherBusinessInfoType.name())
+ .isEqualTo("publisher.info.type");
+ soft.assertThat(publisherBusinessInfoType.asc().property())
+ .isEqualTo("publisher.info.type");
+ soft.assertThat(publisherBusinessInfoType.desc().property())
+ .isEqualTo("publisher.info.type");
+ soft.assertThat(publisherBusinessInfoType.toString())
+ .isEqualTo("book.publisher.info.type");
+ });
+ }
+
+ @Test
+ @DisplayName("should create NavigablePath from path and attribute")
void shouldCreateNavigablePath() {
- NavigableExpression expr = _Book.publisher;
- NavigableAttribute attr = _Publisher.name;
- var path = NavigablePath.of(expr, attr);
+ SoftAssertions.assertSoftly(soft -> {
+ soft.assertThat(_Book.publisher.name())
+ .isEqualTo("publisher");
+ soft.assertThat(_Book.publisher.toString())
+ .isEqualTo("book.publisher");
+ });
+
+ NavigableExpression publisherInfo =
+ _Book.publisher.navigate(_Publisher.info);
+
+ SoftAssertions.assertSoftly(soft -> {
+ soft.assertThat(publisherInfo.toString())
+ .isEqualTo("book.publisher.info");
+ });
+ }
+
+ @Test
+ @DisplayName("should create NumericPath from path and attribute")
+ void shouldCreateNumericPath() {
+ NumericAttribute publisherInfoZipcode =
+ _Book.publisher.navigate(_Publisher.info)
+ .navigate(_BusinessInfo.zipcode);
+
+ SoftAssertions.assertSoftly(soft -> {
+ soft.assertThat(publisherInfoZipcode.name())
+ .isEqualTo("publisher.info.zipcode");
+ soft.assertThat(publisherInfoZipcode.asc().property())
+ .isEqualTo("publisher.info.zipcode");
+ soft.assertThat(publisherInfoZipcode.desc().property())
+ .isEqualTo("publisher.info.zipcode");
+ soft.assertThat(publisherInfoZipcode.toString())
+ .isEqualTo("book.publisher.info.zipcode");
+ });
+ }
+
+ @Test
+ @DisplayName("should create TemporalPath from path and attribute")
+ void shouldCreateTemporalPath() {
+ TemporalAttribute publisherInfoFounded =
+ _Book.publisher.navigate(_Publisher.info)
+ .navigate(_BusinessInfo.founded);
+
+ SoftAssertions.assertSoftly(soft -> {
+ soft.assertThat(publisherInfoFounded.name())
+ .isEqualTo("publisher.info.founded");
+ soft.assertThat(publisherInfoFounded.asc().property())
+ .isEqualTo("publisher.info.founded");
+ soft.assertThat(publisherInfoFounded.desc().property())
+ .isEqualTo("publisher.info.founded");
+ soft.assertThat(publisherInfoFounded.toString())
+ .isEqualTo("book.publisher.info.founded");
+ });
+ }
+
+ @Test
+ @DisplayName("should create TextPath from path and attribute")
+ void shouldCreateTextPath() {
+ TextAttribute publisherInfoName =
+ _Book.publisher.navigate(_Publisher.info)
+ .navigate(_BusinessInfo.name);
SoftAssertions.assertSoftly(soft -> {
- soft.assertThat(path).isNotNull();
- soft.assertThat(path).isInstanceOf(NavigableExpression.class);
- soft.assertThat(path.toString())
- .isEqualTo("book.publisher.name");
+ soft.assertThat(publisherInfoName.name())
+ .isEqualTo("publisher.info.name");
+ soft.assertThat(publisherInfoName.asc().property())
+ .isEqualTo("publisher.info.name");
+ soft.assertThat(publisherInfoName.descIgnoreCase().property())
+ .isEqualTo("publisher.info.name");
+ soft.assertThat(publisherInfoName.toString())
+ .isEqualTo("book.publisher.info.name");
});
}
}
From f6d432666d284615aea9143419b712fa4032d859 Mon Sep 17 00:00:00 2001
From: Nathan Rauh
Date: Wed, 20 May 2026 15:34:45 -0500
Subject: [PATCH 2/4] Update the tests for sort expressions
---
.../expression/path/NavigablePathTest.java | 73 ++++++++++---------
1 file changed, 38 insertions(+), 35 deletions(-)
diff --git a/api/src/test/java/jakarta/data/spi/expression/path/NavigablePathTest.java b/api/src/test/java/jakarta/data/spi/expression/path/NavigablePathTest.java
index 4fbfa4cf0..629a05795 100644
--- a/api/src/test/java/jakarta/data/spi/expression/path/NavigablePathTest.java
+++ b/api/src/test/java/jakarta/data/spi/expression/path/NavigablePathTest.java
@@ -19,7 +19,12 @@
import java.time.LocalDate;
+import jakarta.data.expression.BooleanExpression;
+import jakarta.data.expression.ComparableExpression;
import jakarta.data.expression.NavigableExpression;
+import jakarta.data.expression.NumericExpression;
+import jakarta.data.expression.TemporalExpression;
+import jakarta.data.expression.TextExpression;
import jakarta.data.metamodel.BooleanAttribute;
import jakarta.data.metamodel.ComparableAttribute;
import jakarta.data.metamodel.NavigableAttribute;
@@ -88,17 +93,15 @@ interface _Book {
@Test
@DisplayName("should create BooleanPath from path and attribute")
void shouldCreateBooleanPath() {
- BooleanAttribute publisherInfoActive =
+ BooleanExpression publisherInfoActive =
_Book.publisher.navigate(_Publisher.info)
.navigate(_BusinessInfo.active);
SoftAssertions.assertSoftly(soft -> {
- soft.assertThat(publisherInfoActive.name())
- .isEqualTo("publisher.info.active");
- soft.assertThat(publisherInfoActive.asc().property())
- .isEqualTo("publisher.info.active");
- soft.assertThat(publisherInfoActive.desc().property())
- .isEqualTo("publisher.info.active");
+ soft.assertThat(publisherInfoActive.asc().expression().toString())
+ .isEqualTo("book.publisher.info.active");
+ soft.assertThat(publisherInfoActive.desc().expression().toString())
+ .isEqualTo("book.publisher.info.active");
soft.assertThat(publisherInfoActive.toString())
.isEqualTo("book.publisher.info.active");
});
@@ -107,17 +110,17 @@ void shouldCreateBooleanPath() {
@Test
@DisplayName("should create ComparablePath from path and attribute")
void shouldCreateComparablePath() {
- ComparableAttribute publisherBusinessInfoType =
+ ComparableExpression publisherBusinessInfoType =
_Book.publisher.navigate(_Publisher.info)
.navigate(_BusinessInfo.type);
SoftAssertions.assertSoftly(soft -> {
- soft.assertThat(publisherBusinessInfoType.name())
- .isEqualTo("publisher.info.type");
- soft.assertThat(publisherBusinessInfoType.asc().property())
- .isEqualTo("publisher.info.type");
- soft.assertThat(publisherBusinessInfoType.desc().property())
- .isEqualTo("publisher.info.type");
+ soft.assertThat(publisherBusinessInfoType.asc().expression()
+ .toString())
+ .isEqualTo("book.publisher.info.type");
+ soft.assertThat(publisherBusinessInfoType.desc().expression()
+ .toString())
+ .isEqualTo("book.publisher.info.type");
soft.assertThat(publisherBusinessInfoType.toString())
.isEqualTo("book.publisher.info.type");
});
@@ -146,17 +149,15 @@ void shouldCreateNavigablePath() {
@Test
@DisplayName("should create NumericPath from path and attribute")
void shouldCreateNumericPath() {
- NumericAttribute publisherInfoZipcode =
+ NumericExpression publisherInfoZipcode =
_Book.publisher.navigate(_Publisher.info)
.navigate(_BusinessInfo.zipcode);
SoftAssertions.assertSoftly(soft -> {
- soft.assertThat(publisherInfoZipcode.name())
- .isEqualTo("publisher.info.zipcode");
- soft.assertThat(publisherInfoZipcode.asc().property())
- .isEqualTo("publisher.info.zipcode");
- soft.assertThat(publisherInfoZipcode.desc().property())
- .isEqualTo("publisher.info.zipcode");
+ soft.assertThat(publisherInfoZipcode.asc().expression().toString())
+ .isEqualTo("book.publisher.info.zipcode");
+ soft.assertThat(publisherInfoZipcode.desc().expression().toString())
+ .isEqualTo("book.publisher.info.zipcode");
soft.assertThat(publisherInfoZipcode.toString())
.isEqualTo("book.publisher.info.zipcode");
});
@@ -165,17 +166,15 @@ void shouldCreateNumericPath() {
@Test
@DisplayName("should create TemporalPath from path and attribute")
void shouldCreateTemporalPath() {
- TemporalAttribute publisherInfoFounded =
+ TemporalExpression publisherInfoFounded =
_Book.publisher.navigate(_Publisher.info)
.navigate(_BusinessInfo.founded);
SoftAssertions.assertSoftly(soft -> {
- soft.assertThat(publisherInfoFounded.name())
- .isEqualTo("publisher.info.founded");
- soft.assertThat(publisherInfoFounded.asc().property())
- .isEqualTo("publisher.info.founded");
- soft.assertThat(publisherInfoFounded.desc().property())
- .isEqualTo("publisher.info.founded");
+ soft.assertThat(publisherInfoFounded.asc().expression().toString())
+ .isEqualTo("book.publisher.info.founded");
+ soft.assertThat(publisherInfoFounded.desc().expression().toString())
+ .isEqualTo("book.publisher.info.founded");
soft.assertThat(publisherInfoFounded.toString())
.isEqualTo("book.publisher.info.founded");
});
@@ -184,17 +183,21 @@ void shouldCreateTemporalPath() {
@Test
@DisplayName("should create TextPath from path and attribute")
void shouldCreateTextPath() {
- TextAttribute publisherInfoName =
+ TextExpression publisherInfoName =
_Book.publisher.navigate(_Publisher.info)
.navigate(_BusinessInfo.name);
SoftAssertions.assertSoftly(soft -> {
- soft.assertThat(publisherInfoName.name())
- .isEqualTo("publisher.info.name");
- soft.assertThat(publisherInfoName.asc().property())
- .isEqualTo("publisher.info.name");
- soft.assertThat(publisherInfoName.descIgnoreCase().property())
- .isEqualTo("publisher.info.name");
+ soft.assertThat(publisherInfoName.asc().expression().toString())
+ .isEqualTo("book.publisher.info.name");
+ soft.assertThat(publisherInfoName.desc().expression().toString())
+ .isEqualTo("book.publisher.info.name");
+ soft.assertThat(publisherInfoName.ascIgnoreCase().expression()
+ .toString())
+ .isEqualTo("book.publisher.info.name");
+ soft.assertThat(publisherInfoName.descIgnoreCase().expression()
+ .toString())
+ .isEqualTo("book.publisher.info.name");
soft.assertThat(publisherInfoName.toString())
.isEqualTo("book.publisher.info.name");
});
From 1d4ab0239398bb27cbb7677c56320e20178c0ebb Mon Sep 17 00:00:00 2001
From: Nathan Rauh
Date: Wed, 20 May 2026 15:50:06 -0500
Subject: [PATCH 3/4] Additional tests for sort expressions
---
.../data/expression/ExpressionTest.java | 50 +++++++++++++++++++
1 file changed, 50 insertions(+)
diff --git a/api/src/test/java/jakarta/data/expression/ExpressionTest.java b/api/src/test/java/jakarta/data/expression/ExpressionTest.java
index 0778aa38b..c0abca1fe 100644
--- a/api/src/test/java/jakarta/data/expression/ExpressionTest.java
+++ b/api/src/test/java/jakarta/data/expression/ExpressionTest.java
@@ -20,6 +20,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import jakarta.data.Sort;
import jakarta.data.constraint.EqualTo;
import jakarta.data.constraint.AtMost;
import jakarta.data.mock.entity.Book;
@@ -160,4 +161,53 @@ void shouldRestrictLengthOfText() {
.isEqualTo(50);
});
}
+
+ @Test
+ void testSortOnLeft3() {
+ TextExpression first3LettersOfTitle = _Book.title.right(3);
+ Sort first3LettersOfTitleAsc =
+ first3LettersOfTitle.ascIgnoreCase();
+
+ SoftAssertions.assertSoftly(soft -> {
+ soft.assertThat(first3LettersOfTitleAsc.expression())
+ .isEqualTo(first3LettersOfTitle);
+
+ soft.assertThat(first3LettersOfTitleAsc.isAscending())
+ .isTrue();
+
+ soft.assertThat(first3LettersOfTitleAsc.isDescending())
+ .isFalse();
+
+ soft.assertThat(first3LettersOfTitleAsc.ignoreCase())
+ .isEqualTo(true);
+
+ soft.assertThat(first3LettersOfTitleAsc.property())
+ .isNull();
+ });
+ }
+
+ @Test
+ void testSortOnDivisionExpression() {
+ NumericExpression pagesPerChapter =
+ _Book.numPages.dividedBy(_Book.numChapters);
+ Sort pagesPerChapterDesc = pagesPerChapter.desc();
+
+ SoftAssertions.assertSoftly(soft -> {
+ soft.assertThat(pagesPerChapterDesc.expression())
+ .isEqualTo(pagesPerChapter);
+
+ soft.assertThat(pagesPerChapterDesc.isAscending())
+ .isFalse();
+
+ soft.assertThat(pagesPerChapterDesc.isDescending())
+ .isTrue();
+
+ soft.assertThat(pagesPerChapterDesc.ignoreCase())
+ .isEqualTo(false);
+
+ soft.assertThat(pagesPerChapterDesc.property())
+ .isNull();
+ });
+ }
+
}
From 1da4b4905cfe355f0ad0882d60333375a2b47d6f Mon Sep 17 00:00:00 2001
From: Nathan Rauh
Date: Wed, 20 May 2026 15:52:06 -0500
Subject: [PATCH 4/4] Sort on expressions
---
api/src/main/java/jakarta/data/Sort.java | 86 ++++++++++++++-----
.../data/expression/ComparableExpression.java | 22 +++++
.../data/expression/TextExpression.java | 22 +++++
.../data/metamodel/ComparableAttribute.java | 23 +++++
.../jakarta/data/metamodel/TextAttribute.java | 4 +-
5 files changed, 136 insertions(+), 21 deletions(-)
diff --git a/api/src/main/java/jakarta/data/Sort.java b/api/src/main/java/jakarta/data/Sort.java
index 004c217ad..16c494908 100644
--- a/api/src/main/java/jakarta/data/Sort.java
+++ b/api/src/main/java/jakarta/data/Sort.java
@@ -17,16 +17,19 @@
*/
package jakarta.data;
+import jakarta.data.expression.ComparableExpression;
import jakarta.data.messages.Messages;
+import jakarta.data.metamodel.Attribute;
import jakarta.data.metamodel.StaticMetamodel;
import jakarta.data.repository.OrderBy;
/**
- * Requests sorting on a given entity attribute.
+ * Requests sorting on a given entity attribute or expression.
*
* An instance of {@code Sort} specifies a sorting criterion based
- * on an entity attribute, with a sorting {@linkplain Direction direction} and
- * well-defined case sensitivity.
+ * on an entity attribute or an expression involving entity attributes,
+ * with a sorting {@linkplain Direction direction} and well-defined
+ * case sensitivity.
*
* A query method of a repository may have a parameter or parameters
* of type {@code Sort} if its return type indicates that it may return multiple
@@ -78,8 +81,13 @@
* if the database is incapable of ordering the query results using the given
* sort criteria.
*
- * @param entity class of the entity attribute upon which to sort.
- * @param property name of the entity attribute to order by.
+ * @param type of entity from which query results are obtained.
+ * @param expression an expression that computes a value by which to
+ * order results. Alternatively, {@code null} if
+ * {@code property} is supplied instead.
+ * @param property name of an entity attribute to order by.
+ * Alternatively, {@code null} if {@code expression}
+ * is supplied instead.
* @param isAscending whether ordering for this attribute is ascending
* ({@code true}) or descending ({@code false}).
* @param ignoreCase whether or not to request case insensitive ordering
@@ -88,7 +96,8 @@
* {@link Nulls#FIRST FIRST}, {@link Nulls#LAST LAST}, or
* {@linkplain Nulls#UNSPECIFIED by the data store}.
*/
-public record Sort(String property,
+public record Sort(ComparableExpression> expression,
+ String property,
boolean isAscending,
boolean ignoreCase,
Nulls nullOrdering) {
@@ -159,15 +168,21 @@ public enum Nulls {
* @since 1.1
*/
public Sort {
- if (property == null) {
+ if (expression == null && property == null) {
throw new NullPointerException(
- Messages.get("001.arg.required", "attribute"));
+ Messages.get("001.arg.required", "expression"));
}
if (nullOrdering == null) {
throw new NullPointerException(
Messages.get("001.arg.required", "nullOrdering"));
}
+
+ if (property != null && expression != null) {
+ // TODO add a message
+ throw new IllegalArgumentException(
+ "property: " + property + ", expression: " + expression);
+ }
}
/**
@@ -181,18 +196,39 @@ public enum Nulls {
* from a database with case sensitive collation.
*/
public Sort(String property, boolean isAscending, boolean ignoreCase) {
- this(property, isAscending, ignoreCase, Nulls.UNSPECIFIED);
+ this(null,
+ property,
+ isAscending,
+ ignoreCase,
+ Nulls.UNSPECIFIED);
}
// Override to provide method documentation:
/**
- * Name of the entity attribute to order by.
+ * An expression to order by. The presence of a sort expression is
+ * mutually exclusive with the presence of a
+ * {@linkplain #property() sort attribute name}.
*
- * @return The attribute name to order by; will never be {@code null}.
+ * @return The attribute name to order by, or {@code null} if this
+ * {@code Sort} instance pertains to a {@link #property()}
+ */
+ public ComparableExpression> expression() {
+ return expression;
+ }
+
+ /**
+ * Name of the entity attribute to order by The presence of a
+ * sort attribute name is mutually exclusive with the presence of a
+ * sort {@link #expression()}.
+ *
+ * @return The attribute name to order by, or {@code null} if this
+ * {@code Sort} instance pertains to an {@link #expression()}
*/
public String property() {
- return property;
+ return expression instanceof Attribute> attr
+ ? attr.name()
+ : property;
}
// Override to provide method documentation:
@@ -262,7 +298,8 @@ public static Sort of(String attribute,
Messages.get("001.arg.required", "direction"));
}
- return new Sort<>(attribute,
+ return new Sort<>(null,
+ attribute,
Direction.ASC.equals(direction),
ignoreCase,
Nulls.UNSPECIFIED);
@@ -292,7 +329,8 @@ public static Sort of(String attribute,
Messages.get("001.arg.required", "direction"));
}
- return new Sort<>(attribute,
+ return new Sort<>(null,
+ attribute,
Direction.ASC.equals(direction),
ignoreCase,
nullOrdering);
@@ -309,7 +347,7 @@ public static Sort of(String attribute,
* @throws NullPointerException when the attribute name is null.
*/
public static Sort asc(String attribute) {
- return new Sort<>(attribute, true, false, Nulls.UNSPECIFIED);
+ return new Sort<>(null, attribute, true, false, Nulls.UNSPECIFIED);
}
/**
@@ -322,7 +360,7 @@ public static Sort asc(String attribute) {
* @throws NullPointerException when the attribute name is null.
*/
public static Sort ascIgnoreCase(String attribute) {
- return new Sort<>(attribute, true, true, Nulls.UNSPECIFIED);
+ return new Sort<>(null, attribute, true, true, Nulls.UNSPECIFIED);
}
/**
@@ -336,7 +374,7 @@ public static Sort ascIgnoreCase(String attribute) {
* @throws NullPointerException when the attribute name is null.
*/
public static Sort desc(String attribute) {
- return new Sort<>(attribute, false, false, Nulls.UNSPECIFIED);
+ return new Sort<>(null, attribute, false, false, Nulls.UNSPECIFIED);
}
/**
@@ -350,7 +388,7 @@ public static Sort desc(String attribute) {
* @throws NullPointerException when the attribute name is null.
*/
public static Sort descIgnoreCase(String attribute) {
- return new Sort<>(attribute, false, true, Nulls.UNSPECIFIED);
+ return new Sort<>(null, attribute, false, true, Nulls.UNSPECIFIED);
}
/**
@@ -374,7 +412,11 @@ public static Sort descIgnoreCase(String attribute) {
* @since 1.1
*/
public Sort nullsFirst() {
- return new Sort<>(property, isAscending, ignoreCase, Nulls.FIRST);
+ return new Sort<>(expression,
+ property,
+ isAscending,
+ ignoreCase,
+ Nulls.FIRST);
}
/**
@@ -398,6 +440,10 @@ public Sort nullsFirst() {
* @since 1.1
*/
public Sort nullsLast() {
- return new Sort<>(property, isAscending, ignoreCase, Nulls.LAST);
+ return new Sort<>(expression,
+ property,
+ isAscending,
+ ignoreCase,
+ Nulls.LAST);
}
}
\ No newline at end of file
diff --git a/api/src/main/java/jakarta/data/expression/ComparableExpression.java b/api/src/main/java/jakarta/data/expression/ComparableExpression.java
index e374bf9a5..02170a8cc 100644
--- a/api/src/main/java/jakarta/data/expression/ComparableExpression.java
+++ b/api/src/main/java/jakarta/data/expression/ComparableExpression.java
@@ -19,6 +19,8 @@
import jakarta.data.constraint.Between;
import jakarta.data.constraint.GreaterThan;
+import jakarta.data.Sort;
+import jakarta.data.Sort.Nulls;
import jakarta.data.constraint.AtLeast;
import jakarta.data.constraint.LessThan;
import jakarta.data.constraint.AtMost;
@@ -46,6 +48,16 @@
public interface ComparableExpression>
extends Expression {
+ /**
+ * Obtain a request for an ascending {@link Sort} based on the value
+ * to which this expression computes.
+ *
+ * @return a request for an ascending sort.
+ */
+ default Sort asc() {
+ return new Sort(this, null, true, false, Nulls.UNSPECIFIED);
+ }
+
/**
* Obtains a {@link Restriction} that requires that this expression
* evaluate to a value falling within the range between (and inclusive of)
@@ -95,6 +107,16 @@ default > Restriction between(
Between.bounds(minExpression, maxExpression));
}
+ /**
+ * Obtain a request for a descending {@link Sort} based on the value
+ * to which this expression computes.
+ *
+ * @return a request for a descending sort.
+ */
+ default Sort desc() {
+ return new Sort<>(this, null, false, false, Nulls.UNSPECIFIED);
+ }
+
/**
* Obtains a {@link Restriction} that requires that this expression
* evaluate to a value greater than the given value.
diff --git a/api/src/main/java/jakarta/data/expression/TextExpression.java b/api/src/main/java/jakarta/data/expression/TextExpression.java
index 2b813f1a1..d5d9c37a3 100644
--- a/api/src/main/java/jakarta/data/expression/TextExpression.java
+++ b/api/src/main/java/jakarta/data/expression/TextExpression.java
@@ -24,6 +24,8 @@
import static jakarta.data.spi.expression.function.TextFunctionExpression.RIGHT;
import static jakarta.data.spi.expression.function.TextFunctionExpression.UPPER;
+import jakarta.data.Sort;
+import jakarta.data.Sort.Nulls;
import jakarta.data.constraint.Like;
import jakarta.data.constraint.NotLike;
import jakarta.data.messages.Messages;
@@ -46,6 +48,26 @@
*/
public interface TextExpression extends ComparableExpression {
+ /**
+ * Obtain a request for an ascending, case-insensitive {@link Sort}
+ * based on the value to which this expression computes.
+ *
+ * @return a request for an ascending, case-insensitive sort.
+ */
+ default Sort ascIgnoreCase() {
+ return new Sort<>(this, null, true, true, Nulls.UNSPECIFIED);
+ }
+
+ /**
+ * Obtain a request for a descending, case insensitive {@link Sort}
+ * based on the value to which this expression computes.
+ *
+ * @return a request for a descending, case insensitive sort.
+ */
+ default Sort descIgnoreCase() {
+ return new Sort<>(this, null, false, true, Nulls.UNSPECIFIED);
+ }
+
/**
* Returns {@code String.class} as the type of the textual expression.
*
diff --git a/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java b/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java
index 8440333c0..d476cc734 100644
--- a/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java
@@ -17,6 +17,7 @@
*/
package jakarta.data.metamodel;
+import jakarta.data.Sort;
import jakarta.data.expression.ComparableExpression;
import jakarta.data.messages.Messages;
@@ -86,5 +87,27 @@ static > ComparableAttribute of(
return new ComparableAttributeRecord<>(entityClass, name, attributeType);
}
+
+ /**
+ * Obtain a request for an ascending {@link Sort} based on the entity
+ * attribute.
+ *
+ * @return a request for an ascending sort on the entity attribute.
+ */
+ @Override
+ default Sort asc() {
+ return Sort.asc(name());
+ }
+
+ /**
+ * Obtain a request for a descending {@link Sort} based on the entity
+ * attribute.
+ *
+ * @return a request for a descending sort on the entity attribute.
+ */
+ @Override
+ default Sort desc() {
+ return Sort.desc(name());
+ }
}
diff --git a/api/src/main/java/jakarta/data/metamodel/TextAttribute.java b/api/src/main/java/jakarta/data/metamodel/TextAttribute.java
index 0eeb7c4aa..0c51a1976 100644
--- a/api/src/main/java/jakarta/data/metamodel/TextAttribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/TextAttribute.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023,2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2023,2026 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.
@@ -35,6 +35,7 @@ public interface TextAttribute extends ComparableAttribute, TextEx
* @return a request for an ascending, case-insensitive sort on the entity
* attribute.
*/
+ @Override
default Sort ascIgnoreCase() {
return Sort.ascIgnoreCase(name());
}
@@ -58,6 +59,7 @@ default Class type() {
* @return a request for a descending, case insensitive sort on the entity
* attribute.
*/
+ @Override
default Sort descIgnoreCase() {
return Sort.descIgnoreCase(name());
}