This is an automated email from the ASF dual-hosted git repository.

sergehuber pushed a commit to branch UNOMI-948-range-query
in repository https://gitbox.apache.org/repos/asf/unomi.git

commit 32251df335ad95d6518782335ca5278255bb6877
Author: Serge Huber <[email protected]>
AuthorDate: Thu Jun 25 16:29:57 2026 +0200

    UNOMI-948: Add PersistenceService.rangeQuery with ES/OS implementations
    
    Expose rangeQuery on the persistence SPI and implement it for Elasticsearch,
    OpenSearch, and the in-memory test harness. Add smoke integration tests and
    strengthen unit-test pagination assertions for totalSize.
    
    Related: UNOMI-956 (full PersistenceService IT coverage follow-up).
---
 .../test/java/org/apache/unomi/itests/AllITs.java  |   1 +
 .../apache/unomi/itests/PersistenceServiceIT.java  | 115 +++++++++++++++++++++
 .../ElasticSearchPersistenceServiceImpl.java       |   5 +
 .../OpenSearchPersistenceServiceImpl.java          |   5 +
 .../unomi/persistence/spi/PersistenceService.java  |  18 ++++
 .../impl/InMemoryPersistenceServiceImpl.java       |   4 +-
 .../impl/InMemoryPersistenceServiceImplTest.java   |   3 +
 7 files changed, 148 insertions(+), 3 deletions(-)

diff --git a/itests/src/test/java/org/apache/unomi/itests/AllITs.java 
b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
index b450b3310..ace015dfb 100644
--- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java
+++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
@@ -33,6 +33,7 @@ import org.junit.runners.Suite.SuiteClasses;
 @SuiteClasses({
         Migrate16xToCurrentVersionIT.class,
         MigrationIT.class,
+        PersistenceServiceIT.class,
         BasicIT.class,
         ConditionEvaluatorIT.class,
         ConditionQueryBuilderIT.class,
diff --git 
a/itests/src/test/java/org/apache/unomi/itests/PersistenceServiceIT.java 
b/itests/src/test/java/org/apache/unomi/itests/PersistenceServiceIT.java
new file mode 100644
index 000000000..8ced3264a
--- /dev/null
+++ b/itests/src/test/java/org/apache/unomi/itests/PersistenceServiceIT.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package org.apache.unomi.itests;
+
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.Profile;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerSuite;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Integration tests for {@link 
org.apache.unomi.persistence.spi.PersistenceService} query APIs against the live
+ * search backend (Elasticsearch or OpenSearch). Initial coverage focuses on 
{@code rangeQuery}; additional methods
+ * should be covered in follow-up work (UNOMI-956).
+ */
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerSuite.class)
+public class PersistenceServiceIT extends BaseIT {
+
+    private static final String AGE_PROPERTY = "properties.age";
+
+    private final List<String> profileIds = new ArrayList<>();
+
+    @After
+    public void tearDown() throws InterruptedException {
+        for (String profileId : profileIds) {
+            persistenceService.remove(profileId, Profile.class);
+        }
+        profileIds.clear();
+        refreshPersistence(Profile.class);
+    }
+
+    @Test
+    public void testRangeQueryReturnsProfilesInNumericRange() throws 
InterruptedException {
+        saveProfileWithAge("range-query-it-low", 10);
+        saveProfileWithAge("range-query-it-match-a", 20);
+        saveProfileWithAge("range-query-it-match-b", 30);
+        saveProfileWithAge("range-query-it-match-c", 40);
+        saveProfileWithAge("range-query-it-high", 50);
+
+        refreshPersistence(Profile.class);
+
+        // Upper bound uses "41" so Elasticsearch (exclusive lt) still 
includes age 40.
+        PartialList<Profile> results = keepTrying(
+                "Range query should return profiles with age between 20 and 
40",
+                () -> persistenceService.rangeQuery(AGE_PROPERTY, "20", "41", 
AGE_PROPERTY + ":asc", Profile.class, 0, -1),
+                r -> r != null && r.getList().size() == 3,
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+
+        Assert.assertEquals(3, results.getList().size());
+        Assert.assertEquals(20, results.getList().get(0).getProperty("age"));
+        Assert.assertEquals(30, results.getList().get(1).getProperty("age"));
+        Assert.assertEquals(40, results.getList().get(2).getProperty("age"));
+    }
+
+    @Test
+    public void testRangeQuerySupportsPagination() throws InterruptedException 
{
+        for (int age = 1; age <= 5; age++) {
+            saveProfileWithAge("range-query-it-page-" + age, age);
+        }
+
+        refreshPersistence(Profile.class);
+
+        PartialList<Profile> firstPage = keepTrying(
+                "First range query page should be available",
+                () -> persistenceService.rangeQuery(AGE_PROPERTY, "1", "6", 
AGE_PROPERTY + ":asc", Profile.class, 0, 2),
+                r -> r != null && r.getList().size() == 2 && r.getTotalSize() 
== 5,
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+
+        Assert.assertEquals(2, firstPage.getList().size());
+        Assert.assertEquals(5, firstPage.getTotalSize());
+        Assert.assertEquals(1, firstPage.getList().get(0).getProperty("age"));
+        Assert.assertEquals(2, firstPage.getList().get(1).getProperty("age"));
+
+        PartialList<Profile> lastPage = keepTrying(
+                "Last range query page should be available",
+                () -> persistenceService.rangeQuery(AGE_PROPERTY, "1", "6", 
AGE_PROPERTY + ":asc", Profile.class, 4, 2),
+                r -> r != null && r.getList().size() == 1 && r.getTotalSize() 
== 5,
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+
+        Assert.assertEquals(1, lastPage.getList().size());
+        Assert.assertEquals(5, lastPage.getTotalSize());
+        Assert.assertEquals(5, lastPage.getList().get(0).getProperty("age"));
+    }
+
+    private void saveProfileWithAge(String idSuffix, int age) {
+        Profile profile = new Profile();
+        profile.setItemId(idSuffix + "-" + UUID.randomUUID());
+        profile.setProperty("age", age);
+        persistenceService.save(profile);
+        profileIds.add(profile.getItemId());
+    }
+}
diff --git 
a/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java
 
b/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java
index 386729d50..bc27f6781 100644
--- 
a/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java
+++ 
b/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java
@@ -2074,6 +2074,11 @@ public class ElasticSearchPersistenceServiceImpl 
implements PersistenceService,
         return query(Query.of(q -> q.queryString(qs -> qs.query(fulltext))), 
sortBy, clazz, offset, size, null, null);
     }
 
+    @Override
+    public <T extends Item> PartialList<T> rangeQuery(String fieldName, String 
from, String to, String sortBy, Class<T> clazz, int offset, int size) {
+        return query(Query.of(q -> q.range(r -> r.untyped(v -> 
v.field(fieldName).gte(JsonData.of(from)).lt(JsonData.of(to))))), sortBy, 
clazz, offset, size, null, null);
+    }
+
     @Override public long queryCount(Condition query, String itemType) {
         try {
             return conditionESQueryBuilderDispatcher.count(query);
diff --git 
a/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java
 
b/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java
index 9e2344ed0..b7a639879 100644
--- 
a/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java
+++ 
b/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java
@@ -1948,6 +1948,11 @@ public class OpenSearchPersistenceServiceImpl implements 
PersistenceService, Syn
         return query(Query.of(q->q.queryString(s->s.query(fulltext))), sortBy, 
clazz, offset, size, null, null);
     }
 
+    @Override
+    public <T extends Item> PartialList<T> rangeQuery(String fieldName, String 
from, String to, String sortBy, Class<T> clazz, int offset, int size) {
+        return query(Query.of(q -> q.range(r -> 
r.field(fieldName).from(JsonData.of(from)).to(JsonData.of(to)))), sortBy, 
clazz, offset, size, null, null);
+    }
+
     @Override
     public long queryCount(Condition query, String itemType) {
         try {
diff --git 
a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/PersistenceService.java
 
b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/PersistenceService.java
index 9a99b3420..3b6acfccb 100644
--- 
a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/PersistenceService.java
+++ 
b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/PersistenceService.java
@@ -693,6 +693,24 @@ public interface PersistenceService {
      */
     <T extends Item> void purgeTimeBasedItems(int existsNumberOfDays, Class<T> 
clazz);
 
+    /**
+     * Retrieves all items of the specified Item subclass which specified 
ranged property is within the specified bounds, ordered according to the 
specified {@code sortBy} String
+     * and and paged: only {@code size} of them are retrieved, starting with 
the {@code offset}-th one.
+     *
+     * @param <T>    the type of the Item subclass we want to retrieve
+     * @param s      the name of the range property we want items to retrieve 
to be included between the specified start and end points
+     * @param from   the beginning of the range we want to consider
+     * @param to     the end of the range we want to consider
+     * @param sortBy an optional ({@code null} if no sorting is required) 
String of comma ({@code ,}) separated property names on which ordering should 
be performed, ordering
+     *               elements according to the property order in the String, 
considering each in turn and moving on to the next one in case of equality of 
all preceding ones.
+     *               Each property name is optionally followed by a column 
({@code :}) and an order specifier: {@code asc} or {@code desc}.
+     * @param clazz  the {@link Item} subclass of the items we want to retrieve
+     * @param offset zero or a positive integer specifying the position of the 
first item in the total ordered collection of matching items
+     * @param size   a positive integer specifying how many matching items 
should be retrieved or {@code -1} if all of them should be retrieved
+     * @return a {@link PartialList} of items matching the specified criteria
+     */
+    <T extends Item> PartialList<T> rangeQuery(String s, String from, String 
to, String sortBy, Class<T> clazz, int offset, int size);
+
     /**
      * Retrieves the specified metrics for the specified field of items of the 
specified type as defined by the Item subclass public field {@code ITEM_TYPE} 
and matching the
      * specified {@link Condition}.
diff --git 
a/services/src/test/java/org/apache/unomi/services/impl/InMemoryPersistenceServiceImpl.java
 
b/services/src/test/java/org/apache/unomi/services/impl/InMemoryPersistenceServiceImpl.java
index a71c1b604..facd96002 100644
--- 
a/services/src/test/java/org/apache/unomi/services/impl/InMemoryPersistenceServiceImpl.java
+++ 
b/services/src/test/java/org/apache/unomi/services/impl/InMemoryPersistenceServiceImpl.java
@@ -968,9 +968,7 @@ public class InMemoryPersistenceServiceImpl implements 
PersistenceService {
         return createPartialList(items, offset, size);
     }
 
-    /**
-     * Test-harness helper for range queries. Not on PersistenceService on 
master yet.
-     */
+    @Override
     public <T extends Item> PartialList<T> rangeQuery(String fieldName, String 
from, String to, String sortBy, Class<T> clazz, int offset, int size) {
         List<T> items = filterItemsByClass(clazz);
         items = items.stream()
diff --git 
a/services/src/test/java/org/apache/unomi/services/impl/InMemoryPersistenceServiceImplTest.java
 
b/services/src/test/java/org/apache/unomi/services/impl/InMemoryPersistenceServiceImplTest.java
index efcedf92e..0f9e7857f 100644
--- 
a/services/src/test/java/org/apache/unomi/services/impl/InMemoryPersistenceServiceImplTest.java
+++ 
b/services/src/test/java/org/apache/unomi/services/impl/InMemoryPersistenceServiceImplTest.java
@@ -3140,6 +3140,7 @@ public class InMemoryPersistenceServiceImplTest {
 
             // then
             assertEquals(2, page1.getList().size());
+            assertEquals(5, page1.getTotalSize());
             assertEquals(1.0, page1.getList().get(0).getNumericValue());
             assertEquals(2.0, page1.getList().get(1).getNumericValue());
 
@@ -3152,6 +3153,7 @@ public class InMemoryPersistenceServiceImplTest {
 
             // then
             assertEquals(2, page2.getList().size());
+            assertEquals(5, page2.getTotalSize());
             assertEquals(3.0, page2.getList().get(0).getNumericValue());
             assertEquals(4.0, page2.getList().get(1).getNumericValue());
 
@@ -3164,6 +3166,7 @@ public class InMemoryPersistenceServiceImplTest {
 
             // then
             assertEquals(1, page3.getList().size());
+            assertEquals(5, page3.getTotalSize());
             assertEquals(5.0, page3.getList().get(0).getNumericValue());
         }
 

Reply via email to