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()); }