This is an automated email from the ASF dual-hosted git repository. joerghoh pushed a commit to branch SLING-13248 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-resourceresolver.git
commit ca6560abd58681cb9f44dce8816fd6ef4555b5c6 Author: Joerg Hoh <[email protected]> AuthorDate: Tue Jul 21 12:42:27 2026 +0200 SLING-13248 - Fix false-positive ordering check for multivalued properties in PagedQueryIterator --- .../impl/mapping/PagedQueryIterator.java | 8 +++++- .../impl/mapping/PagedQueryIteratorTest.java | 32 ++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/PagedQueryIterator.java b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/PagedQueryIterator.java index 40356f06..fc533144 100644 --- a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/PagedQueryIterator.java +++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/PagedQueryIterator.java @@ -18,6 +18,8 @@ */ package org.apache.sling.resourceresolver.impl.mapping; +import java.util.Arrays; +import java.util.Comparator; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.concurrent.TimeUnit; @@ -88,7 +90,11 @@ public class PagedQueryIterator implements Iterator<Resource> { final String[] values = resource.getValueMap().get(propertyName, defaultValue); if (values.length > 0) { - String value = values[0]; + // The query orders by FIRST([propertyName]), i.e. the smallest value of a (possibly + // multivalued) property. The ValueMap, however, returns the values in storage order, so + // values[0] is not necessarily the smallest one. Use the minimum to match the query's + // sort key; otherwise the ordering checks below raise false positives (SLING-13266). + String value = Arrays.stream(values).min(Comparator.naturalOrder()).orElse(values[0]); if (value.compareTo(lastKey) < 0) { String message = String.format( "unexpected query result in page %d, property name '%s', got '%s', despite querying for > '%s'", diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/PagedQueryIteratorTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/PagedQueryIteratorTest.java index 61e1cc5e..5741ea21 100644 --- a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/PagedQueryIteratorTest.java +++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/PagedQueryIteratorTest.java @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -170,6 +171,37 @@ public class PagedQueryIteratorTest extends AbstractMappingMapEntriesTest { checkResult(it, expected); } + /** + * A multivalued property is returned by the query in the order of its smallest value + * ({@code FIRST([prop])}), but the ValueMap exposes the values in storage order. The iterator + * must therefore compare on the smallest value, not on {@code values[0]}: when index 0 is not + * the smallest value, the two orderings diverge and comparing on {@code values[0]} would raise + * a spurious {@link PagedQueryIterator.QueryImplementationException}. + */ + @Test + public void testMultiValuedFirstNotAtIndexZero() { + // resources are returned in FIRST()/minimum order: "/aaa" < "/acs-commons" + Resource r1 = multiValuedResource("2025-10-14T20:32:01.481Z", "/aaa"); + Resource r2 = multiValuedResource("/acs-commons"); + Collection<Resource> resources = Arrays.asList(r1, r2); + when(resourceResolver.findResources("multivalued ''", "JCR-SQL2")).thenReturn(resources.iterator()); + PagedQueryIterator it = new PagedQueryIterator("alias", PROPNAME, resourceResolver, "multivalued '%s'", 2000); + + List<Resource> result = new ArrayList<>(); + while (it.hasNext()) { + result.add(it.next()); + } + assertEquals(Arrays.asList(r1, r2), result); + } + + private static Resource multiValuedResource(String... values) { + ValueMap m = mock(ValueMap.class); + when(m.get(eq(PROPNAME), any(Object.class))).thenReturn(values); + Resource r = mock(Resource.class); + when(r.getValueMap()).thenReturn(m); + return r; + } + private static Collection<Resource> toResourceList(String... keys) { Collection<Resource> result = new ArrayList<>(); for (String key : keys) {
