This is an automated email from the ASF dual-hosted git repository.
sergehuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/unomi.git
The following commit(s) were added to refs/heads/master by this push:
new 8c5bfa2fe UNOMI-948: Add PersistenceService.rangeQuery API (#781)
8c5bfa2fe is described below
commit 8c5bfa2fee3de57646074540b00e38e6070af52b
Author: Serge Huber <[email protected]>
AuthorDate: Sat Jun 27 15:02:19 2026 +0200
UNOMI-948: Add PersistenceService.rangeQuery API (#781)
Merge range query improvements as well as IT fixes
---
.../test/java/org/apache/unomi/itests/AllITs.java | 1 +
.../apache/unomi/itests/PersistenceServiceIT.java | 115 +++++++++++++++++++++
.../ElasticSearchPersistenceServiceImpl.java | 14 +++
.../OpenSearchPersistenceServiceImpl.java | 14 +++
.../unomi/persistence/spi/PersistenceService.java | 21 ++++
.../impl/InMemoryPersistenceServiceImpl.java | 4 +-
.../impl/InMemoryPersistenceServiceImplTest.java | 3 +
.../impl/cluster/ClusterServiceImplTest.java | 8 +-
8 files changed, 175 insertions(+), 5 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..a2c9e581e
--- /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);
+
+ // Both bounds are inclusive, so age 40 (the "to" value) must be
included while age 50 is excluded.
+ PartialList<Profile> results = keepTrying(
+ "Range query should return profiles with age between 20 and
40",
+ () -> persistenceService.rangeQuery(AGE_PROPERTY, "20", "40",
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..efed6c03c 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,20 @@ 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);
+ if (from != null) {
+ v.gte(JsonData.of(from));
+ }
+ if (to != null) {
+ v.lte(JsonData.of(to));
+ }
+ return v;
+ }))), 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..42e95e543 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,20 @@ 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);
+ if (from != null) {
+ r.from(JsonData.of(from));
+ }
+ if (to != null) {
+ r.to(JsonData.of(to));
+ }
+ return r;
+ })), 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..8e8b22622 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,27 @@ 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 paged: only {@code size} of them are retrieved, starting with the
{@code offset}-th one.
+ * <p>
+ * Both bounds are inclusive: items whose property value equals {@code
from} or {@code to} are included in the results. Either bound may be {@code
null} to leave that side
+ * of the range unbounded.
+ *
+ * @param <T> the type of the Item subclass we want to retrieve
+ * @param fieldName 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 (inclusive) of the range we want to
consider, or {@code null} for no lower bound
+ * @param to the end (inclusive) of the range we want to consider,
or {@code null} for no upper bound
+ * @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 fieldName, 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());
}
diff --git
a/services/src/test/java/org/apache/unomi/services/impl/cluster/ClusterServiceImplTest.java
b/services/src/test/java/org/apache/unomi/services/impl/cluster/ClusterServiceImplTest.java
index 432bf165f..435360e48 100644
---
a/services/src/test/java/org/apache/unomi/services/impl/cluster/ClusterServiceImplTest.java
+++
b/services/src/test/java/org/apache/unomi/services/impl/cluster/ClusterServiceImplTest.java
@@ -107,8 +107,12 @@ public class ClusterServiceImplTest {
// Set scheduler in cluster service - this would normally be done by
OSGi but we need to do it manually in tests
clusterService.setSchedulerService(schedulerService);
- // Explicitly initialize scheduled tasks to handle the circular
dependency properly
- clusterService.initializeScheduledTasks();
+ // Note: scheduled tasks are intentionally NOT started here.
clusterStaleNodesCleanup and
+ // clusterNodeStatisticsUpdate both have initialDelay=0, so starting
them eagerly in every
+ // test's setUp() raced the test body on a real background thread pool
and could delete
+ // freshly-saved fixtures before assertions ran (flaky only under
CI-like scheduling/timing).
+ // Tests that actually exercise scheduled-task behavior call
clusterService.init() themselves,
+ // which starts them at the right time.
}
@Test