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 5740a815f UNOMI-1003: Sort merged segment/scoring lists and fix
missing limit (#917)
5740a815f is described below
commit 5740a815fb7fe8bfb6dc2a706efed917ee76c63c
Author: Serge Huber <[email protected]>
AuthorDate: Thu Oct 15 21:39:59 2026 +0200
UNOMI-1003: Sort merged segment/scoring lists and fix missing limit (#917)
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../java/org/apache/unomi/itests/SegmentIT.java | 102 ++++++++
manual/src/main/asciidoc/configuration.adoc | 12 +
.../asciidoc/migrations/migrate-3.0-to-4.0.adoc | 57 +++++
.../ElasticSearchPersistenceServiceImpl.java | 9 +-
.../OpenSearchPersistenceServiceImpl.java | 9 +-
.../unomi/persistence/spi/PersistenceService.java | 10 +
.../impl/segments/MergedMetadataListHelper.java | 170 +++++++++++++
.../services/impl/segments/SegmentServiceImpl.java | 213 ++++------------
.../impl/InMemoryPersistenceServiceImpl.java | 17 +-
.../impl/InMemoryPersistenceServiceImplTest.java | 27 +-
.../segments/MergedMetadataListHelperTest.java | 280 +++++++++++++++++++++
.../impl/segments/SegmentServiceImplTest.java | 243 ++++++++++++++++++
12 files changed, 965 insertions(+), 184 deletions(-)
diff --git a/itests/src/test/java/org/apache/unomi/itests/SegmentIT.java
b/itests/src/test/java/org/apache/unomi/itests/SegmentIT.java
index e4bffff64..1811d65ff 100644
--- a/itests/src/test/java/org/apache/unomi/itests/SegmentIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/SegmentIT.java
@@ -17,8 +17,14 @@
package org.apache.unomi.itests;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.util.EntityUtils;
import org.apache.unomi.api.Event;
import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.PartialList;
import org.apache.unomi.api.Profile;
import org.apache.unomi.api.conditions.Condition;
import org.apache.unomi.api.exceptions.BadSegmentConditionException;
@@ -793,4 +799,100 @@ public class SegmentIT extends BaseIT {
updatedProfile.getScores() == null ||
!updatedProfile.getScores().containsKey("relative-date-scoring-test")), 1000,
20);
}
+
+ @Test
+ public void testQuerySegmentsWithNoLimitReturnsFullList() throws Exception
{
+ Metadata segmentOneMetadata = new Metadata("query-no-limit-segment-1");
+ segmentOneMetadata.setName("Query No Limit Segment 1");
+ Segment segmentOne = new Segment(segmentOneMetadata);
+ Condition segmentOneCondition = new
Condition(definitionsService.getConditionType("profilePropertyCondition"));
+ segmentOneCondition.setParameter("propertyName", "properties.email");
+ segmentOneCondition.setParameter("comparisonOperator", "exists");
+ segmentOne.setCondition(segmentOneCondition);
+ segmentService.setSegmentDefinition(segmentOne);
+
+ Metadata segmentTwoMetadata = new Metadata("query-no-limit-segment-2");
+ segmentTwoMetadata.setName("Query No Limit Segment 2");
+ Segment segmentTwo = new Segment(segmentTwoMetadata);
+ Condition segmentTwoCondition = new
Condition(definitionsService.getConditionType("profilePropertyCondition"));
+ segmentTwoCondition.setParameter("propertyName",
"properties.firstName");
+ segmentTwoCondition.setParameter("comparisonOperator", "exists");
+ segmentTwo.setCondition(segmentTwoCondition);
+ segmentService.setSegmentDefinition(segmentTwo);
+
+ keepTrying("Segments should be listed",
+ () -> segmentService.getSegmentMetadatas(0, 50,
null).getList(),
+ list -> list.stream().anyMatch(m ->
"query-no-limit-segment-1".equals(m.getId()))
+ && list.stream().anyMatch(m ->
"query-no-limit-segment-2".equals(m.getId())),
+ DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+
+ PartialList<?> noLimit = postMetadataQuery("/cxs/segments/query",
"{}");
+ // Like the persistence services, a query without a limit returns
defaultQueryLimit items (10 unless configured)
+ Assert.assertTrue("totalSize should cover the full list",
noLimit.getTotalSize() >= 2);
+ Assert.assertEquals("Query without limit should return the default
page", Math.min(noLimit.getTotalSize(), 10), noLimit.getList().size());
+
+ // 2.x/3.0 clients send -1 for "all" and persistence-style sort
criteria
+ List<String> sortedIds =
metadataQueryIds(postMetadataQuery("/cxs/segments/query",
"{\"limit\":-1,\"sortby\":\"metadata.name:desc\"}"));
+ Assert.assertTrue("Should return both created segments",
sortedIds.containsAll(Arrays.asList("query-no-limit-segment-1",
"query-no-limit-segment-2")));
+ Assert.assertTrue("Segments should be sorted by name descending",
sortedIds.indexOf("query-no-limit-segment-2") <
sortedIds.indexOf("query-no-limit-segment-1"));
+
+ // Unknown sort fields are ignored, as they were by the persistence
services
+ Assert.assertTrue("Unknown sort field should be ignored",
+ metadataQueryIds(postMetadataQuery("/cxs/segments/query",
"{\"limit\":-1,\"sortby\":\"notAField:asc\"}")).contains("query-no-limit-segment-1"));
+ }
+
+ @Test
+ public void testQueryScoringsWithNoLimitReturnsFullList() throws Exception
{
+ Metadata scoringOneMetadata = new Metadata("query-no-limit-scoring-1");
+ scoringOneMetadata.setName("Query No Limit Scoring 1");
+ Scoring scoringOne = new Scoring(scoringOneMetadata);
+ scoringOne.setElements(new ArrayList<>());
+ segmentService.setScoringDefinition(scoringOne);
+
+ Metadata scoringTwoMetadata = new Metadata("query-no-limit-scoring-2");
+ scoringTwoMetadata.setName("Query No Limit Scoring 2");
+ Scoring scoringTwo = new Scoring(scoringTwoMetadata);
+ scoringTwo.setElements(new ArrayList<>());
+ segmentService.setScoringDefinition(scoringTwo);
+
+ keepTrying("Scorings should be listed",
+ () -> segmentService.getScoringMetadatas(0, 50,
null).getList(),
+ list -> list.stream().anyMatch(m ->
"query-no-limit-scoring-1".equals(m.getId()))
+ && list.stream().anyMatch(m ->
"query-no-limit-scoring-2".equals(m.getId())),
+ DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+
+ PartialList<?> noLimit = postMetadataQuery("/cxs/scoring/query", "{}");
+ // Like the persistence services, a query without a limit returns
defaultQueryLimit items (10 unless configured)
+ Assert.assertTrue("totalSize should cover the full list",
noLimit.getTotalSize() >= 2);
+ Assert.assertEquals("Query without limit should return the default
page", Math.min(noLimit.getTotalSize(), 10), noLimit.getList().size());
+
+ // 2.x/3.0 clients send -1 for "all" and persistence-style sort
criteria
+ List<String> sortedIds =
metadataQueryIds(postMetadataQuery("/cxs/scoring/query",
"{\"limit\":-1,\"sortby\":\"metadata.name:desc\"}"));
+ Assert.assertTrue("Should return both created scorings",
sortedIds.containsAll(Arrays.asList("query-no-limit-scoring-1",
"query-no-limit-scoring-2")));
+ Assert.assertTrue("Scorings should be sorted by name descending",
sortedIds.indexOf("query-no-limit-scoring-2") <
sortedIds.indexOf("query-no-limit-scoring-1"));
+
+ // Unknown sort fields are ignored, as they were by the persistence
services
+ Assert.assertTrue("Unknown sort field should be ignored",
+ metadataQueryIds(postMetadataQuery("/cxs/scoring/query",
"{\"limit\":-1,\"sortby\":\"notAField:asc\"}")).contains("query-no-limit-scoring-1"));
+ }
+
+ private PartialList<?> postMetadataQuery(String path, String body) throws
Exception {
+ HttpPost request = new HttpPost(getFullUrl(path));
+ request.setEntity(new StringEntity(body,
ContentType.APPLICATION_JSON));
+ try (CloseableHttpResponse response = executeHttpRequest(request)) {
+ Assert.assertEquals("Query " + body + " on " + path + " should
return 200", 200, response.getStatusLine().getStatusCode());
+ PartialList<?> result =
getObjectMapper().readValue(EntityUtils.toString(response.getEntity()),
PartialList.class);
+ Assert.assertNotNull(result);
+ Assert.assertNotNull(result.getList());
+ return result;
+ }
+ }
+
+ private List<String> metadataQueryIds(PartialList<?> result) {
+ List<String> ids = new ArrayList<>();
+ for (Object item : result.getList()) {
+ ids.add((String) ((Map<?, ?>) item).get("id"));
+ }
+ return ids;
+ }
}
diff --git a/manual/src/main/asciidoc/configuration.adoc
b/manual/src/main/asciidoc/configuration.adoc
index c3b037ce1..3ba40441e 100644
--- a/manual/src/main/asciidoc/configuration.adoc
+++ b/manual/src/main/asciidoc/configuration.adoc
@@ -105,6 +105,18 @@
org.apache.unomi.opensearch.password=${env:OPENSEARCH_INITIAL_ADMIN_PASSWORD:-ad
org.apache.unomi.opensearch.sslTrustAllCertificates=true
----
+A query that does not give a size (for example a `/cxs/segments/query` body
without `limit`) returns
+`defaultQueryLimit` results, 10 by default. A size of `-1` returns every
result. The setting applies to every
+search, including the segment and scoring lists that merge system and tenant
items:
+
+[source]
+----
+# env UNOMI_ELASTICSEARCH_DEFAULTQUERYLIMIT
+org.apache.unomi.elasticsearch.defaultQueryLimit=10
+# env UNOMI_OPENSEARCH_DEFAULTQUERYLIMIT
+org.apache.unomi.opensearch.defaultQueryLimit=10
+----
+
To select which search engine to use, you can:
1. Use the appropriate configuration properties above
2. When building from source, use the appropriate Maven profile:
diff --git a/manual/src/main/asciidoc/migrations/migrate-3.0-to-4.0.adoc
b/manual/src/main/asciidoc/migrations/migrate-3.0-to-4.0.adoc
index 5444d8cf6..4f7ae942a 100644
--- a/manual/src/main/asciidoc/migrations/migrate-3.0-to-4.0.adoc
+++ b/manual/src/main/asciidoc/migrations/migrate-3.0-to-4.0.adoc
@@ -266,6 +266,63 @@ This affects every endpoint that takes a `Query`, among
them `/cxs/profiles/sear
* A client that omits the `condition` field entirely still searches over
everything, and it needs no change.
* A client whose condition names a type that the server does not know now
reads an empty list where it used to read every item. Check the server log for
the condition type it names, and deploy the plugin that defines it.
+[#_segment_and_scoring_lists]
+==== Segment and scoring lists merge system and tenant items (4.0)
+
+A tenant sees its own segments and scoring plans together with those of the
system tenant, and a tenant item
+replaces a system item with the same id. Because the two sets are merged after
they are read, 4.0 sorts and pages
+the segment and scoring lists itself instead of leaving it to the search
engine. This applies to
+`GET /cxs/segments`, `POST /cxs/segments/query`, `GET /cxs/scoring`, `POST
/cxs/scoring/query` and the GraphQL
+`findSegments` query.
+
+Paging and sorting follow the 3.0 rules:
+
+* `offset` and `limit` page the merged list. A missing `limit` returns
`defaultQueryLimit` items, 10 unless
+ configured (see
<<changing_the_default_configuration_using_property_files,the search engine
configuration>>), and `-1` returns every item.
+ The response echoes the requested `offset` and `limit`.
+* `sortby` takes the same syntax: comma-separated `field` or `field:desc`
criteria. Any criterion other than an
+ exact `:desc` suffix sorts ascending.
+* A field with no value sorts last in both directions. A field with several
values, such as `metadata.tags`,
+ sorts on its smallest value ascending and its largest value descending.
+* A sort field the server does not know is ignored, as it was when the search
engine ignored an unmapped field.
+
+The sortable fields are `metadata.id`, `metadata.name`,
`metadata.description`, `metadata.scope`,
+`metadata.tags`, `metadata.systemTags`, `metadata.enabled`,
`metadata.missingPlugins`, `metadata.hidden`,
+`metadata.readOnly`, `itemId`, `tenantId`, `createdBy`, `lastModifiedBy`,
`creationDate`,
+`lastModificationDate` and `lastSyncDate`. Each `metadata.*` field can also be
named without its prefix, for
+example `name`.
+
+.What changed
+[cols="1,2,2", options="header"]
+|===
+| Request | 3.0 | 4.0
+
+| `limit` is `-1`
+| Every item, with `totalSize` reported as `0`.
+| Every item, with `totalSize` reported as the number of items.
+
+| Several items have the same value for the sort field
+| Their order was left to the search engine.
+| They are ordered by `itemId`, so pages are stable.
+
+| `sortby` names a field without its `metadata.` prefix, or an audit field
such as `creationDate`
+| Ignored.
+| Sorted on.
+
+| A text value longer than 256 characters, in a sort
+| Sorted as a missing value, because the search engine does not index it for
sorting.
+| Sorted on its full value.
+
+| A negative `offset`, or `offset` plus `limit` above 10,000
+| Rejected by the search engine.
+| A negative offset counts as `0`, and large pages are returned.
+|===
+
+===== Migrating a client
+
+* A client that pages with `limit` and `offset`, or reads every item with
`-1`, needs no change.
+* A client that read `totalSize` after asking for `-1` now gets the real count
instead of `0`.
+
=== Migrating your existing data
==== Multi-Tenancy Impact
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 fea126b58..46cdad5ba 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
@@ -130,7 +130,7 @@ public class ElasticSearchPersistenceServiceImpl implements
PersistenceService,
private ConditionESQueryBuilderDispatcher
conditionESQueryBuilderDispatcher;
private Map<String, String> routingByType;
- private Integer defaultQueryLimit = 10;
+ private int defaultQueryLimit = 10;
private final Integer removeByQueryTimeoutInMinutes = 10;
private Integer taskWaitingTimeout = 3600000;
private Integer taskWaitingPollingInterval = 1000;
@@ -243,10 +243,15 @@ public class ElasticSearchPersistenceServiceImpl
implements PersistenceService,
this.indexMaxDocValueFieldsSearch = indexMaxDocValueFieldsSearch;
}
- public void setDefaultQueryLimit(Integer defaultQueryLimit) {
+ public void setDefaultQueryLimit(int defaultQueryLimit) {
this.defaultQueryLimit = defaultQueryLimit;
}
+ @Override
+ public int getDefaultQueryLimit() {
+ return defaultQueryLimit;
+ }
+
public void setRoutingByType(Map<String, String> routingByType) {
this.routingByType = routingByType;
}
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 5b4270f57..a83d6a7b6 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
@@ -127,7 +127,7 @@ public class OpenSearchPersistenceServiceImpl implements
PersistenceService, Syn
private ConditionOSQueryBuilderDispatcher
conditionOSQueryBuilderDispatcher;
private Map<String, String> routingByType;
- private Integer defaultQueryLimit = 10;
+ private int defaultQueryLimit = 10;
private Integer removeByQueryTimeoutInMinutes = 10;
private Integer taskWaitingTimeout = 3600000;
private Integer taskWaitingPollingInterval = 1000;
@@ -249,10 +249,15 @@ public class OpenSearchPersistenceServiceImpl implements
PersistenceService, Syn
this.indexMaxDocValueFieldsSearch = indexMaxDocValueFieldsSearch;
}
- public void setDefaultQueryLimit(Integer defaultQueryLimit) {
+ public void setDefaultQueryLimit(int defaultQueryLimit) {
this.defaultQueryLimit = defaultQueryLimit;
}
+ @Override
+ public int getDefaultQueryLimit() {
+ return defaultQueryLimit;
+ }
+
public void setRoutingByType(Map<String, String> routingByType) {
this.routingByType = routingByType;
}
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 5289917e0..2c64c1b01 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
@@ -698,6 +698,16 @@ public interface PersistenceService {
*/
Map<String, Long> aggregateWithOptimizedQuery(Condition filter,
BaseAggregate aggregate, String itemType, int size);
+ /**
+ * Returns the number of results a query returns when it is not given a
size (a negative size other than
+ * {@code -1}, which returns all results). Services that page results in
memory use it to match the queries.
+ *
+ * @return the configured default query limit
+ */
+ default int getDefaultQueryLimit() {
+ return 10;
+ }
+
/**
* Refreshes persistence engine indices when required by the backend.
*/
diff --git
a/services/src/main/java/org/apache/unomi/services/impl/segments/MergedMetadataListHelper.java
b/services/src/main/java/org/apache/unomi/services/impl/segments/MergedMetadataListHelper.java
new file mode 100644
index 000000000..1df7b90e1
--- /dev/null
+++
b/services/src/main/java/org/apache/unomi/services/impl/segments/MergedMetadataListHelper.java
@@ -0,0 +1,170 @@
+/*
+ * 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.services.impl.segments;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.MetadataItem;
+import org.apache.unomi.api.PartialList;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * Sorts and pages merged system+tenant {@link MetadataItem} lists for segment
and scoring queries.
+ * <p>
+ * Before multi-tenancy these lists were sorted and paged by the search
engine, so this helper reproduces the
+ * persistence semantics that Unomi 2.x and 3.0 clients rely on:
+ * <ul>
+ * <li>a size of {@code -1} returns every item, an unset ({@link
Integer#MIN_VALUE}) or other negative size
+ * returns the persistence default query limit, and the requested size is
echoed back as the page size;</li>
+ * <li>sort criteria use the persistence syntax ({@code
field[:asc|:desc]}, comma separated), and criteria
+ * on unknown fields are ignored rather than rejected;</li>
+ * <li>missing values sort last in both directions, and multi-valued
fields sort on their smallest value
+ * ascending and largest value descending.</li>
+ * </ul>
+ * Only a fixed set of fields can be sorted on; values are read through plain
getters, never by reflection on a
+ * caller-supplied path.
+ */
+final class MergedMetadataListHelper {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(MergedMetadataListHelper.class);
+
+ private static final Map<String, Function<MetadataItem, Object>>
SORT_FIELDS = new HashMap<>();
+
+ static {
+ addMetadataField("id", Metadata::getId);
+ addMetadataField("name", Metadata::getName);
+ addMetadataField("description", Metadata::getDescription);
+ addMetadataField("scope", Metadata::getScope);
+ addMetadataField("tags", Metadata::getTags);
+ addMetadataField("systemTags", Metadata::getSystemTags);
+ addMetadataField("enabled", Metadata::isEnabled);
+ addMetadataField("missingPlugins", Metadata::isMissingPlugins);
+ addMetadataField("hidden", Metadata::isHidden);
+ addMetadataField("readOnly", Metadata::isReadOnly);
+ SORT_FIELDS.put("itemId", MetadataItem::getItemId);
+ SORT_FIELDS.put("tenantId", MetadataItem::getTenantId);
+ SORT_FIELDS.put("createdBy", MetadataItem::getCreatedBy);
+ SORT_FIELDS.put("lastModifiedBy", MetadataItem::getLastModifiedBy);
+ SORT_FIELDS.put("creationDate", MetadataItem::getCreationDate);
+ SORT_FIELDS.put("lastModificationDate",
MetadataItem::getLastModificationDate);
+ SORT_FIELDS.put("lastSyncDate", MetadataItem::getLastSyncDate);
+ }
+
+ private static final Comparator<MetadataItem> TIE_BREAKER =
+ Comparator.comparing(MetadataItem::getItemId,
Comparator.nullsLast(Comparator.naturalOrder()));
+
+ private MergedMetadataListHelper() {
+ }
+
+ /**
+ * Registers a metadata field under both its persistence path ({@code
metadata.name}) and its short alias
+ * ({@code name}).
+ */
+ private static void addMetadataField(String name, Function<Metadata,
Object> getter) {
+ Function<MetadataItem, Object> extractor = item -> item.getMetadata()
!= null ? getter.apply(item.getMetadata()) : null;
+ SORT_FIELDS.put("metadata." + name, extractor);
+ SORT_FIELDS.put(name, extractor);
+ }
+
+ /**
+ * @param defaultQueryLimit the number of items returned when no size is
given, normally
+ * {@link
org.apache.unomi.persistence.spi.PersistenceService#getDefaultQueryLimit()}
+ */
+ static <T extends MetadataItem> PartialList<Metadata>
sortAndPage(Collection<T> mergedItems, int offset, int size, String sortBy,
+ int
defaultQueryLimit) {
+ List<T> list = new ArrayList<>(mergedItems);
+ sort(list, sortBy);
+
+ int totalSize = list.size();
+ int fromIndex = Math.max(offset, 0);
+ int pageSize = size == -1 ? totalSize : (size < 0 ? defaultQueryLimit
: size);
+ int toIndex = (int) Math.min((long) fromIndex + pageSize, totalSize);
+
+ List<Metadata> page = new ArrayList<>();
+ if (fromIndex < toIndex) {
+ for (T item : list.subList(fromIndex, toIndex)) {
+ page.add(item.getMetadata());
+ }
+ }
+ return new PartialList<>(page, offset, size, totalSize,
PartialList.Relation.EQUAL);
+ }
+
+ static void sort(List<? extends MetadataItem> items, String sortBy) {
+ Comparator<MetadataItem> comparator = buildComparator(sortBy);
+ if (comparator != null) {
+ items.sort(comparator.thenComparing(TIE_BREAKER));
+ }
+ }
+
+ private static Comparator<MetadataItem> buildComparator(String sortBy) {
+ if (StringUtils.isBlank(sortBy)) {
+ return null;
+ }
+ Comparator<MetadataItem> comparator = null;
+ for (String criterion : sortBy.split(",")) {
+ // Same parsing as the persistence services: the field is
everything before the last ':' and the
+ // order is descending only for a ":desc" suffix.
+ String field = StringUtils.substringBeforeLast(criterion,
":").trim();
+ Function<MetadataItem, Object> extractor = SORT_FIELDS.get(field);
+ if (extractor == null) {
+ LOGGER.debug("Ignoring unsupported sort field '{}' in '{}'",
field, sortBy);
+ continue;
+ }
+ boolean descending = criterion.trim().endsWith(":desc");
+ Comparator<MetadataItem> fieldComparator = (left, right) ->
+ compareValues(sortValue(extractor.apply(left),
descending), sortValue(extractor.apply(right), descending), descending);
+ comparator = comparator == null ? fieldComparator :
comparator.thenComparing(fieldComparator);
+ }
+ return comparator;
+ }
+
+ /**
+ * Reduces a multi-valued field to the value the search engine sorts on:
the smallest one ascending, the largest
+ * one descending. An empty collection counts as a missing value.
+ */
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ private static Object sortValue(Object value, boolean descending) {
+ if (!(value instanceof Collection)) {
+ return value;
+ }
+ Collection<Comparable> values = (Collection<Comparable>) value;
+ if (values.isEmpty()) {
+ return null;
+ }
+ return descending ? Collections.max(values) : Collections.min(values);
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ private static int compareValues(Object left, Object right, boolean
descending) {
+ if (left == null || right == null) {
+ // Missing values go last whatever the order.
+ return left == null ? (right == null ? 0 : 1) : -1;
+ }
+ int comparison = ((Comparable) left).compareTo(right);
+ return descending ? -comparison : comparison;
+ }
+}
diff --git
a/services/src/main/java/org/apache/unomi/services/impl/segments/SegmentServiceImpl.java
b/services/src/main/java/org/apache/unomi/services/impl/segments/SegmentServiceImpl.java
index ed1b71911..d91fd9e94 100644
---
a/services/src/main/java/org/apache/unomi/services/impl/segments/SegmentServiceImpl.java
+++
b/services/src/main/java/org/apache/unomi/services/impl/segments/SegmentServiceImpl.java
@@ -279,91 +279,49 @@ public class SegmentServiceImpl extends
AbstractMultiTypeCachingService implemen
public PartialList<Metadata> getSegmentMetadatas(String scope, int offset,
int size, String sortBy) {
String currentTenantId =
contextManager.getCurrentContext().getTenantId();
- List<Metadata> details = new LinkedList<>();
+ Map<String, Segment> merged = new LinkedHashMap<>();
// Get system tenant segments first
if (!TenantService.SYSTEM_TENANT.equals(currentTenantId)) {
contextManager.executeAsSystem(() -> {
- Condition systemTenantCondition = new
Condition(definitionsService.getConditionType("booleanCondition"));
- systemTenantCondition.setParameter("operator", "and");
- List<Condition> systemConditions = new ArrayList<>();
-
- Condition systemTenantCheck = new
Condition(definitionsService.getConditionType("sessionPropertyCondition"));
- systemTenantCheck.setParameter("propertyName", "tenantId");
- systemTenantCheck.setParameter("comparisonOperator", "equals");
- systemTenantCheck.setParameter("propertyValue",
TenantService.SYSTEM_TENANT);
- systemConditions.add(systemTenantCheck);
-
- if (scope != null) {
- Condition systemScopeCheck = new
Condition(definitionsService.getConditionType("sessionPropertyCondition"));
- systemScopeCheck.setParameter("propertyName",
"metadata.scope");
- systemScopeCheck.setParameter("comparisonOperator",
"equals");
- systemScopeCheck.setParameter("propertyValue", scope);
- systemConditions.add(systemScopeCheck);
- }
-
- systemTenantCondition.setParameter("subConditions",
systemConditions);
-
- PartialList<Segment> systemSegments =
persistenceService.query(systemTenantCondition, sortBy, Segment.class, 0, -1);
+ Condition systemTenantCondition =
buildTenantScopeCondition(TenantService.SYSTEM_TENANT, scope);
+ PartialList<Segment> systemSegments =
persistenceService.query(systemTenantCondition, null, Segment.class, 0, -1);
for (Segment definition : systemSegments.getList()) {
- details.add(definition.getMetadata());
+ merged.put(definition.getMetadata().getId(), definition);
}
return null;
});
}
// Get current tenant segments (will override system segments with
same ID)
- Condition tenantCondition = new
Condition(definitionsService.getConditionType("booleanCondition"));
- tenantCondition.setParameter("operator", "and");
- List<Condition> conditions = new ArrayList<>();
+ Condition tenantCondition = buildTenantScopeCondition(currentTenantId,
scope);
+ PartialList<Segment> segments =
persistenceService.query(tenantCondition, null, Segment.class, 0, -1);
+ for (Segment definition : segments.getList()) {
+ merged.put(definition.getMetadata().getId(), definition);
+ }
+ return MergedMetadataListHelper.sortAndPage(merged.values(), offset,
size, sortBy, persistenceService.getDefaultQueryLimit());
+ }
+
+ private Condition buildTenantScopeCondition(String tenantId, String scope)
{
Condition tenantCheck = new
Condition(definitionsService.getConditionType("sessionPropertyCondition"));
tenantCheck.setParameter("propertyName", "tenantId");
tenantCheck.setParameter("comparisonOperator", "equals");
- tenantCheck.setParameter("propertyValue", currentTenantId);
- conditions.add(tenantCheck);
-
- if (scope != null) {
- Condition scopeCheck = new
Condition(definitionsService.getConditionType("sessionPropertyCondition"));
- scopeCheck.setParameter("propertyName", "metadata.scope");
- scopeCheck.setParameter("comparisonOperator", "equals");
- scopeCheck.setParameter("propertyValue", scope);
- conditions.add(scopeCheck);
- }
-
- tenantCondition.setParameter("subConditions", conditions);
-
- PartialList<Segment> segments =
persistenceService.query(tenantCondition, sortBy, Segment.class, 0, -1);
- Map<String, Metadata> mergedDetails = new HashMap<>();
+ tenantCheck.setParameter("propertyValue", tenantId);
- // Add system tenant segments first
- for (Metadata metadata : details) {
- mergedDetails.put(metadata.getId(), metadata);
+ if (scope == null) {
+ return tenantCheck;
}
- // Override with current tenant segments
- for (Segment definition : segments.getList()) {
- mergedDetails.put(definition.getMetadata().getId(),
definition.getMetadata());
- }
-
- // Convert to list and apply pagination
- List<Metadata> finalDetails = new ArrayList<>(mergedDetails.values());
- if (sortBy != null) {
- // TODO: Implement sorting of merged results
- }
+ Condition scopeCheck = new
Condition(definitionsService.getConditionType("sessionPropertyCondition"));
+ scopeCheck.setParameter("propertyName", "metadata.scope");
+ scopeCheck.setParameter("comparisonOperator", "equals");
+ scopeCheck.setParameter("propertyValue", scope);
- int totalSize = finalDetails.size();
- int fromIndex = offset;
- int toIndex = offset + size;
- if (fromIndex >= totalSize) {
- return new PartialList<Metadata>(new ArrayList<>(), offset, size,
totalSize, PartialList.Relation.EQUAL);
- }
- if (toIndex > totalSize) {
- toIndex = totalSize;
- }
- finalDetails = finalDetails.subList(fromIndex, toIndex);
-
- return new PartialList<Metadata>(finalDetails, offset, size,
totalSize, PartialList.Relation.EQUAL);
+ Condition combined = new
Condition(definitionsService.getConditionType("booleanCondition"));
+ combined.setParameter("operator", "and");
+ combined.setParameter("subConditions", Arrays.asList(tenantCheck,
scopeCheck));
+ return combined;
}
public PartialList<Metadata> getSegmentMetadatas(Query query) {
@@ -1598,62 +1556,34 @@ public class SegmentServiceImpl extends
AbstractMultiTypeCachingService implemen
protected <T extends MetadataItem> PartialList<Metadata> getMetadatas(int
offset, int size, String sortBy, Class<T> clazz) {
String currentTenantId =
contextManager.getCurrentContext().getTenantId();
- List<Metadata> details = new LinkedList<>();
+ Map<String, T> merged = new LinkedHashMap<>();
// Get system tenant items first
if (!TenantService.SYSTEM_TENANT.equals(currentTenantId)) {
contextManager.executeAsSystem(() -> {
- Condition systemTenantCondition = new
Condition(definitionsService.getConditionType("sessionPropertyCondition"));
- systemTenantCondition.setParameter("propertyName", "tenantId");
- systemTenantCondition.setParameter("comparisonOperator",
"equals");
- systemTenantCondition.setParameter("propertyValue",
TenantService.SYSTEM_TENANT);
- PartialList<T> systemItems =
persistenceService.query(systemTenantCondition, sortBy, clazz, 0, -1);
+ Condition systemTenantCondition =
buildTenantScopeCondition(TenantService.SYSTEM_TENANT, null);
+ PartialList<T> systemItems =
persistenceService.query(systemTenantCondition, null, clazz, 0, -1);
for (T definition : systemItems.getList()) {
- details.add(definition.getMetadata());
+ merged.put(definition.getMetadata().getId(), definition);
}
return null;
});
}
// Get current tenant items (will override system items with same ID)
- Condition tenantCondition = new
Condition(definitionsService.getConditionType("sessionPropertyCondition"));
- tenantCondition.setParameter("propertyName", "tenantId");
- tenantCondition.setParameter("comparisonOperator", "equals");
- tenantCondition.setParameter("propertyValue", currentTenantId);
- PartialList<T> items = persistenceService.query(tenantCondition,
sortBy, clazz, 0, -1);
- Map<String, Metadata> mergedDetails = new HashMap<>();
-
- // Add system tenant items first
- for (Metadata metadata : details) {
- mergedDetails.put(metadata.getId(), metadata);
- }
-
- // Override with current tenant items
+ Condition tenantCondition = buildTenantScopeCondition(currentTenantId,
null);
+ PartialList<T> items = persistenceService.query(tenantCondition, null,
clazz, 0, -1);
for (T definition : items.getList()) {
- mergedDetails.put(definition.getMetadata().getId(),
definition.getMetadata());
+ merged.put(definition.getMetadata().getId(), definition);
}
- // Convert to list and apply pagination
- List<Metadata> finalDetails = new ArrayList<>(mergedDetails.values());
- if (sortBy != null) {
- // TODO: Implement sorting of merged results
- }
-
- int totalSize = finalDetails.size();
- int fromIndex = offset;
- int toIndex = offset + size;
- if (fromIndex >= totalSize) {
- return new PartialList<Metadata>(new ArrayList<>(), offset, size,
totalSize, PartialList.Relation.EQUAL);
- }
- if (toIndex > totalSize) {
- toIndex = totalSize;
- }
- finalDetails = finalDetails.subList(fromIndex, toIndex);
-
- return new PartialList<Metadata>(finalDetails, offset, size,
totalSize, PartialList.Relation.EQUAL);
+ return MergedMetadataListHelper.sortAndPage(merged.values(), offset,
size, sortBy, persistenceService.getDefaultQueryLimit());
}
protected <T extends MetadataItem> PartialList<Metadata>
getMetadatas(Query query, Class<T> clazz) {
+ if (query.isForceRefresh()) {
+ persistenceService.refreshIndex(clazz);
+ }
if (query.getCondition() != null) {
definitionsService.getConditionValidationService().validate(query.getCondition());
}
@@ -1663,77 +1593,40 @@ public class SegmentServiceImpl extends
AbstractMultiTypeCachingService implemen
return new PartialList<>();
}
- List<Metadata> details = new LinkedList<>();
+ Map<String, T> merged = new LinkedHashMap<>();
// Get system tenant items first
if (!TenantService.SYSTEM_TENANT.equals(currentTenantId)) {
contextManager.executeAsSystem(() -> {
- Condition systemTenantCondition = new
Condition(definitionsService.getConditionType("booleanCondition"));
- systemTenantCondition.setParameter("operator", "and");
- List<Condition> systemConditions = new ArrayList<>();
-
- Condition systemTenantCheck = new
Condition(definitionsService.getConditionType("sessionPropertyCondition"));
- systemTenantCheck.setParameter("propertyName", "tenantId");
- systemTenantCheck.setParameter("comparisonOperator", "equals");
- systemTenantCheck.setParameter("propertyValue",
TenantService.SYSTEM_TENANT);
- systemConditions.add(systemTenantCheck);
-
- systemConditions.add(query.getCondition());
- systemTenantCondition.setParameter("subConditions",
systemConditions);
-
- PartialList<T> systemItems =
persistenceService.query(systemTenantCondition, query.getSortby(), clazz, 0,
-1);
+ Condition systemTenantCondition =
buildTenantQueryCondition(TenantService.SYSTEM_TENANT, query.getCondition());
+ PartialList<T> systemItems =
persistenceService.query(systemTenantCondition, null, clazz, 0, -1);
for (T definition : systemItems.getList()) {
- details.add(definition.getMetadata());
+ merged.put(definition.getMetadata().getId(), definition);
}
return null;
});
}
// Get current tenant items (will override system items with same ID)
- Condition tenantCondition = new
Condition(definitionsService.getConditionType("booleanCondition"));
- tenantCondition.setParameter("operator", "and");
- List<Condition> conditions = new ArrayList<>();
-
- Condition tenantCheck = new
Condition(definitionsService.getConditionType("sessionPropertyCondition"));
- tenantCheck.setParameter("propertyName", "tenantId");
- tenantCheck.setParameter("comparisonOperator", "equals");
- tenantCheck.setParameter("propertyValue", currentTenantId);
- conditions.add(tenantCheck);
-
- conditions.add(query.getCondition());
- tenantCondition.setParameter("subConditions", conditions);
-
- PartialList<T> items = persistenceService.query(tenantCondition,
query.getSortby(), clazz, 0, -1);
- Map<String, Metadata> mergedDetails = new HashMap<>();
-
- // Add system tenant items first
- for (Metadata metadata : details) {
- mergedDetails.put(metadata.getId(), metadata);
- }
-
- // Override with current tenant items
+ Condition tenantCondition = buildTenantQueryCondition(currentTenantId,
query.getCondition());
+ PartialList<T> items = persistenceService.query(tenantCondition, null,
clazz, 0, -1);
for (T definition : items.getList()) {
- mergedDetails.put(definition.getMetadata().getId(),
definition.getMetadata());
+ merged.put(definition.getMetadata().getId(), definition);
}
- // Convert to list and apply pagination
- List<Metadata> finalDetails = new ArrayList<>(mergedDetails.values());
- if (query.getSortby() != null) {
- // TODO: Implement sorting of merged results
- }
+ return MergedMetadataListHelper.sortAndPage(merged.values(),
query.getOffset(), query.getLimit(), query.getSortby(),
+ persistenceService.getDefaultQueryLimit());
+ }
- int totalSize = finalDetails.size();
- int fromIndex = query.getOffset();
- int toIndex = fromIndex + query.getLimit();
- if (fromIndex >= totalSize) {
- return new PartialList<Metadata>(new ArrayList<>(),
query.getOffset(), query.getLimit(), totalSize, PartialList.Relation.EQUAL);
+ private Condition buildTenantQueryCondition(String tenantId, Condition
queryCondition) {
+ Condition tenantCheck = buildTenantScopeCondition(tenantId, null);
+ if (queryCondition == null) {
+ return tenantCheck;
}
- if (toIndex > totalSize) {
- toIndex = totalSize;
- }
- finalDetails = finalDetails.subList(fromIndex, toIndex);
-
- return new PartialList<Metadata>(finalDetails, query.getOffset(),
query.getLimit(), totalSize, PartialList.Relation.EQUAL);
+ Condition combined = new
Condition(definitionsService.getConditionType("booleanCondition"));
+ combined.setParameter("operator", "and");
+ combined.setParameter("subConditions", Arrays.asList(tenantCheck,
queryCondition));
+ return combined;
}
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 210f6913c..2f71e8ce6 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
@@ -93,7 +93,7 @@ public class InMemoryPersistenceServiceImpl implements
PersistenceService {
private final Map<String, RefreshPolicy> itemTypeToRefreshPolicy = new
ConcurrentHashMap<>();
// Default query limit (simulates Elasticsearch/OpenSearch default query
limit)
- private Integer defaultQueryLimit = 10;
+ private int defaultQueryLimit = 10;
// Tenant transformation listeners (simulates Elasticsearch/OpenSearch
tenant transformations)
private final List<TenantTransformationListener> transformationListeners =
new ArrayList<>();
@@ -1050,8 +1050,8 @@ public class InMemoryPersistenceServiceImpl implements
PersistenceService {
List<T> matchingItems = query(condition, sortBy, clazz);
int totalSize = matchingItems.size();
- // Apply default query limit when size < 0 (simulates
Elasticsearch/OpenSearch behavior)
- int effectiveSize = size < 0 ? defaultQueryLimit : size;
+ // Apply default query limit when size < 0, except -1 which returns
everything (simulates Elasticsearch/OpenSearch behavior)
+ int effectiveSize = size == -1 ? totalSize : (size < 0 ?
defaultQueryLimit : size);
// If size is negative and no scroll, use default limit
if (size < 0 && scrollTimeValidity == null) {
@@ -2203,8 +2203,8 @@ public class InMemoryPersistenceServiceImpl implements
PersistenceService {
private <T extends Item> PartialList<T> createPartialList(List<T> items,
int offset, int size) {
int totalSize = items.size();
- // Apply default query limit when size < 0 (simulates
Elasticsearch/OpenSearch behavior)
- int effectiveSize = size < 0 ? defaultQueryLimit : size;
+ // Apply default query limit when size < 0, except -1 which returns
everything (simulates Elasticsearch/OpenSearch behavior)
+ int effectiveSize = size == -1 ? totalSize : (size < 0 ?
defaultQueryLimit : size);
int fromIndex = Math.min(offset, totalSize);
int toIndex = Math.min(offset + effectiveSize, totalSize);
List<T> pageItems = items.subList(fromIndex, toIndex);
@@ -2919,7 +2919,8 @@ public class InMemoryPersistenceServiceImpl implements
PersistenceService {
*
* @return the default query limit
*/
- public Integer getDefaultQueryLimit() {
+ @Override
+ public int getDefaultQueryLimit() {
return defaultQueryLimit;
}
@@ -2928,8 +2929,8 @@ public class InMemoryPersistenceServiceImpl implements
PersistenceService {
*
* @param defaultQueryLimit the default query limit to set
*/
- public void setDefaultQueryLimit(Integer defaultQueryLimit) {
- this.defaultQueryLimit = defaultQueryLimit != null &&
defaultQueryLimit > 0 ? defaultQueryLimit : 10;
+ public void setDefaultQueryLimit(int defaultQueryLimit) {
+ this.defaultQueryLimit = defaultQueryLimit > 0 ? defaultQueryLimit :
10;
}
/**
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 3d730133d..b998ae172 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
@@ -6363,7 +6363,7 @@ public class InMemoryPersistenceServiceImplTest {
@Nested
class DefaultQueryLimitTests {
@Test
- void shouldApplyDefaultLimitWhenSizeIsNegative() {
+ void shouldApplyDefaultLimitOnlyWhenSizeIsUnset() {
// Create service with default limit of 10
InMemoryPersistenceServiceImpl service = new
InMemoryPersistenceServiceImpl(
executionContextManager, conditionEvaluatorDispatcher);
@@ -6379,11 +6379,14 @@ public class InMemoryPersistenceServiceImplTest {
// The totalSize is calculated from items that are currently
queryable (refreshed)
// So we need to ensure all items are refreshed before verifying
totalSize
PartialList<TestMetadataItem> result = TestHelper.retryUntil(
- () -> service.query(null, null, TestMetadataItem.class, 0, -1),
+ () -> service.query(null, null, TestMetadataItem.class, 0,
Integer.MIN_VALUE),
r -> r != null && r.getTotalSize() == 20 && r.getList().size()
== 10
);
- assertEquals(10, result.getList().size(), "Query with size=-1
should return default limit of 10");
+ assertEquals(10, result.getList().size(), "Query with an unset
size should return default limit of 10");
assertEquals(20, result.getTotalSize(), "Total size should be 20");
+
+ PartialList<TestMetadataItem> all = service.query(null, null,
TestMetadataItem.class, 0, -1);
+ assertEquals(20, all.getList().size(), "Query with size=-1 should
return all items, like Elasticsearch/OpenSearch");
}
@Test
@@ -6403,10 +6406,10 @@ public class InMemoryPersistenceServiceImplTest {
// The totalSize is calculated from items that are currently
queryable (refreshed)
// So we need to ensure all items are refreshed before verifying
totalSize
PartialList<TestMetadataItem> result = TestHelper.retryUntil(
- () -> service.query(null, null, TestMetadataItem.class, 0, -1,
null),
+ () -> service.query(null, null, TestMetadataItem.class, 0,
Integer.MIN_VALUE, null),
r -> r != null && r.getTotalSize() == 20 && r.getList().size()
== 10
);
- assertEquals(10, result.getList().size(), "Query with size=-1
should return default limit of 10");
+ assertEquals(10, result.getList().size(), "Query with an unset
size should return default limit of 10");
assertEquals(20, result.getTotalSize(), "Total size should be 20");
}
@@ -6449,10 +6452,10 @@ public class InMemoryPersistenceServiceImplTest {
// Retry until both list size and totalSize are correct — same
race as shouldRespectExplicitSizeWhenProvided.
PartialList<TestMetadataItem> result = TestHelper.retryUntil(
- () -> service.query(null, null, TestMetadataItem.class, 0, -1),
+ () -> service.query(null, null, TestMetadataItem.class, 0,
Integer.MIN_VALUE),
r -> r != null && r.getTotalSize() == 20 && r.getList().size()
== 5
);
- assertEquals(5, result.getList().size(), "Query with size=-1
should return custom default limit of 5");
+ assertEquals(5, result.getList().size(), "Query with an unset size
should return custom default limit of 5");
assertEquals(20, result.getTotalSize(), "Total size should be 20");
}
@@ -6470,12 +6473,12 @@ public class InMemoryPersistenceServiceImplTest {
service.save(item);
}
- // Query with field matching and size = -1 - retry until items are
available
+ // Query with field matching and an unset size - retry until items
are available
PartialList<TestMetadataItem> result =
TestHelper.retryQueryUntilAvailable(
- () -> service.query("name", "test", null,
TestMetadataItem.class, 0, -1),
+ () -> service.query("name", "test", null,
TestMetadataItem.class, 0, Integer.MIN_VALUE),
10
);
- assertEquals(10, result.getList().size(), "Query with size=-1
should return default limit of 10");
+ assertEquals(10, result.getList().size(), "Query with an unset
size should return default limit of 10");
}
@Test
@@ -6496,10 +6499,10 @@ public class InMemoryPersistenceServiceImplTest {
// The totalSize is calculated from items that are currently
queryable (refreshed)
// So we need to ensure all items are refreshed before verifying
totalSize
PartialList<TestMetadataItem> result = TestHelper.retryUntil(
- () -> service.rangeQuery("numericValue", "0", "20", null,
TestMetadataItem.class, 0, -1),
+ () -> service.rangeQuery("numericValue", "0", "20", null,
TestMetadataItem.class, 0, Integer.MIN_VALUE),
r -> r != null && r.getTotalSize() == 20 && r.getList().size()
== 10
);
- assertEquals(10, result.getList().size(), "Range query with
size=-1 should return default limit of 10");
+ assertEquals(10, result.getList().size(), "Range query with an
unset size should return default limit of 10");
assertEquals(20, result.getTotalSize(), "Total size should be 20");
}
diff --git
a/services/src/test/java/org/apache/unomi/services/impl/segments/MergedMetadataListHelperTest.java
b/services/src/test/java/org/apache/unomi/services/impl/segments/MergedMetadataListHelperTest.java
new file mode 100644
index 000000000..15df89f3d
--- /dev/null
+++
b/services/src/test/java/org/apache/unomi/services/impl/segments/MergedMetadataListHelperTest.java
@@ -0,0 +1,280 @@
+/*
+ * 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.services.impl.segments;
+
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.segments.Segment;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class MergedMetadataListHelperTest {
+
+ private static final int DEFAULT_QUERY_LIMIT = 10;
+
+ @Test
+ public void testSortByNameAscendingAndDescending() {
+ List<Segment> items = threeSegments();
+
+ MergedMetadataListHelper.sort(items, "name:asc");
+ assertEquals(Arrays.asList("a", "b", "c"), ids(items));
+
+ MergedMetadataListHelper.sort(items, "metadata.name:desc");
+ assertEquals(Arrays.asList("c", "b", "a"), ids(items));
+
+ MergedMetadataListHelper.sort(items, "metadata.name");
+ assertEquals(Arrays.asList("a", "b", "c"), ids(items));
+ }
+
+ @Test
+ public void testSortByCreationDateAscendingAndDescending() {
+ List<Segment> items = threeSegments();
+
+ MergedMetadataListHelper.sort(items, "creationDate:asc");
+ assertEquals(Arrays.asList("c", "b", "a"), ids(items));
+
+ MergedMetadataListHelper.sort(items, "creationDate:desc");
+ assertEquals(Arrays.asList("a", "b", "c"), ids(items));
+ }
+
+ @Test
+ public void testMultipleCriteriaAndBooleanField() {
+ Segment disabledB = segment("b", "Same", daysAgo(1));
+ disabledB.getMetadata().setEnabled(false);
+ Segment enabledC = segment("c", "Same", daysAgo(2));
+ Segment enabledA = segment("a", "Other", daysAgo(3));
+ List<Segment> items = new ArrayList<>(Arrays.asList(disabledB,
enabledC, enabledA));
+
+ MergedMetadataListHelper.sort(items,
"metadata.enabled:desc,metadata.name:asc");
+ assertEquals(Arrays.asList("a", "c", "b"), ids(items));
+ }
+
+ @Test
+ public void testFieldsSortableInPreviousVersionsAreSupported() {
+ Segment readOnly = segment("a", "Alpha", daysAgo(1));
+ readOnly.getMetadata().setReadOnly(true);
+ List<Segment> items = new ArrayList<>(Arrays.asList(readOnly,
segment("b", "Bravo", daysAgo(2))));
+
+ MergedMetadataListHelper.sort(items, "metadata.readOnly:asc");
+ assertEquals(Arrays.asList("b", "a"), ids(items));
+
+ for (String field : Arrays.asList("metadata.hidden",
"metadata.missingPlugins", "metadata.tags",
+ "metadata.systemTags", "metadata.description",
"metadata.scope", "metadata.id", "itemId",
+ "tenantId", "createdBy", "lastModifiedBy",
"lastModificationDate", "lastSyncDate")) {
+ assertDoesNotThrow(() -> MergedMetadataListHelper.sort(items,
field + ":desc"), field);
+ }
+ }
+
+ @Test
+ public void
testMultiValuedFieldSortsOnSmallestAscendingAndLargestDescending() {
+ Segment a = segment("a", "Alpha", daysAgo(1));
+ a.getMetadata().setTags(new LinkedHashSet<>(Arrays.asList("m", "z")));
+ Segment b = segment("b", "Bravo", daysAgo(2));
+ b.getMetadata().setTags(new LinkedHashSet<>(Arrays.asList("b", "n")));
+ Segment untagged = segment("c", "Charlie", daysAgo(3));
+ List<Segment> items = new ArrayList<>(Arrays.asList(untagged, a, b));
+
+ MergedMetadataListHelper.sort(items, "metadata.tags:asc");
+ assertEquals(Arrays.asList("b", "a", "c"), ids(items));
+
+ MergedMetadataListHelper.sort(items, "metadata.tags:desc");
+ assertEquals(Arrays.asList("a", "b", "c"), ids(items));
+ }
+
+ @Test
+ public void testMissingValuesSortLastInBothDirections() {
+ Segment noDate = segment("x", "Xray", null);
+ List<Segment> items = new ArrayList<>(Arrays.asList(noDate,
segment("a", "Alpha", daysAgo(1)), segment("b", "Bravo", daysAgo(2))));
+
+ MergedMetadataListHelper.sort(items, "creationDate:asc");
+ assertEquals(Arrays.asList("b", "a", "x"), ids(items));
+
+ MergedMetadataListHelper.sort(items, "creationDate:desc");
+ assertEquals(Arrays.asList("a", "b", "x"), ids(items));
+ }
+
+ @Test
+ public void testTiesAreBrokenByItemId() {
+ List<Segment> items = new ArrayList<>(Arrays.asList(
+ segment("c", "Same", daysAgo(1)),
+ segment("a", "Same", daysAgo(2)),
+ segment("b", "Same", daysAgo(3))
+ ));
+
+ MergedMetadataListHelper.sort(items, "name:desc");
+ assertEquals(Arrays.asList("a", "b", "c"), ids(items));
+ }
+
+ @Test
+ public void testUnknownOrMalformedCriteriaAreIgnored() {
+ List<Segment> items = threeSegments();
+ List<String> original = ids(items);
+
+ MergedMetadataListHelper.sort(items, "unknownField:asc");
+ assertEquals(original, ids(items));
+
+ MergedMetadataListHelper.sort(items, ",unknownField:desc,");
+ assertEquals(original, ids(items));
+
+ MergedMetadataListHelper.sort(items, "unknownField:desc,name:asc");
+ assertEquals(Arrays.asList("a", "b", "c"), ids(items));
+ }
+
+ @Test
+ public void testOnlyExactDescSuffixSortsDescending() {
+ List<Segment> items = threeSegments();
+
+ MergedMetadataListHelper.sort(items, "name:descending");
+ assertEquals(Arrays.asList("a", "b", "c"), ids(items));
+
+ MergedMetadataListHelper.sort(items, "name:DESC");
+ assertEquals(Arrays.asList("a", "b", "c"), ids(items));
+ }
+
+ @Test
+ public void testNoSortKeepsMergedOrder() {
+ List<Segment> items = threeSegments();
+ List<String> original = ids(items);
+
+ MergedMetadataListHelper.sort(items, null);
+ assertEquals(original, ids(items));
+
+ PartialList<Metadata> page =
MergedMetadataListHelper.sortAndPage(items, 0, -1, " ", DEFAULT_QUERY_LIMIT);
+ assertEquals(original, metadataIds(page));
+ }
+
+ @Test
+ public void testLimitSemanticsMatchPersistence() {
+ List<Segment> items = new ArrayList<>();
+ for (int i = 0; i < DEFAULT_QUERY_LIMIT + 5; i++) {
+ items.add(segment(String.format("s%02d", i),
String.format("Segment %02d", i), daysAgo(i)));
+ }
+ int total = items.size();
+
+ PartialList<Metadata> unset =
MergedMetadataListHelper.sortAndPage(items, 0, Integer.MIN_VALUE, "name:asc",
DEFAULT_QUERY_LIMIT);
+ assertEquals(DEFAULT_QUERY_LIMIT, unset.getList().size());
+ assertEquals(total, unset.getTotalSize());
+ assertEquals(Integer.MIN_VALUE, unset.getPageSize());
+
+ PartialList<Metadata> otherNegative =
MergedMetadataListHelper.sortAndPage(items, 0, -5, "name:asc",
DEFAULT_QUERY_LIMIT);
+ assertEquals(DEFAULT_QUERY_LIMIT, otherNegative.getList().size());
+
+ PartialList<Metadata> all =
MergedMetadataListHelper.sortAndPage(items, 0, -1, "name:asc",
DEFAULT_QUERY_LIMIT);
+ assertEquals(total, all.getList().size());
+ assertEquals(-1, all.getPageSize());
+
+ PartialList<Metadata> allFromOffset =
MergedMetadataListHelper.sortAndPage(items, 10, -1, "name:asc",
DEFAULT_QUERY_LIMIT);
+ assertEquals(total - 10, allFromOffset.getList().size());
+
+ PartialList<Metadata> zero =
MergedMetadataListHelper.sortAndPage(items, 0, 0, "name:asc",
DEFAULT_QUERY_LIMIT);
+ assertEquals(0, zero.getList().size());
+ assertEquals(total, zero.getTotalSize());
+
+ PartialList<Metadata> largerThanTotal =
MergedMetadataListHelper.sortAndPage(items, 0, Integer.MAX_VALUE, "name:asc",
DEFAULT_QUERY_LIMIT);
+ assertEquals(total, largerThanTotal.getList().size());
+
+ PartialList<Metadata> pastEnd =
MergedMetadataListHelper.sortAndPage(items, 100, 2, "name:asc",
DEFAULT_QUERY_LIMIT);
+ assertEquals(0, pastEnd.getList().size());
+ assertEquals(total, pastEnd.getTotalSize());
+
+ PartialList<Metadata> secondPage =
MergedMetadataListHelper.sortAndPage(items, 2, 3, "name:asc",
DEFAULT_QUERY_LIMIT);
+ assertEquals(Arrays.asList("s02", "s03", "s04"),
metadataIds(secondPage));
+ assertEquals(2, secondPage.getOffset());
+ assertEquals(3, secondPage.getPageSize());
+ assertEquals(total, secondPage.getTotalSize());
+ assertEquals(PartialList.Relation.EQUAL,
secondPage.getTotalSizeRelation());
+
+ PartialList<Metadata> unsetFromOffset =
MergedMetadataListHelper.sortAndPage(items, 12, Integer.MIN_VALUE, "name:asc",
DEFAULT_QUERY_LIMIT);
+ assertEquals(Arrays.asList("s12", "s13", "s14"),
metadataIds(unsetFromOffset));
+ assertEquals(12, unsetFromOffset.getOffset());
+
+ PartialList<Metadata> negativeOffset
=MergedMetadataListHelper.sortAndPage(items, -3, 2, "name:asc",
DEFAULT_QUERY_LIMIT);
+ assertEquals(Arrays.asList("s00", "s01"), metadataIds(negativeOffset));
+ }
+
+ @Test
+ public void testConfiguredDefaultQueryLimitIsApplied() {
+ List<Segment> items = threeSegments();
+
+ PartialList<Metadata> unset =
MergedMetadataListHelper.sortAndPage(items, 0, Integer.MIN_VALUE, "name:asc",
2);
+ assertEquals(Arrays.asList("a", "b"), metadataIds(unset));
+ assertEquals(3, unset.getTotalSize());
+
+ PartialList<Metadata> all =
MergedMetadataListHelper.sortAndPage(items, 0, -1, "name:asc", 2);
+ assertEquals(Arrays.asList("a", "b", "c"), metadataIds(all));
+ }
+
+ @Test
+ public void testPagingThroughAllPagesReturnsEachItemOnce() {
+ List<Segment> items = Arrays.asList(
+ segment("a", "Alpha", daysAgo(1)),
+ segment("b", "Bravo", daysAgo(2)),
+ segment("c", "Charlie", daysAgo(3)),
+ segment("d", "Delta", daysAgo(4)),
+ segment("e", "Echo", daysAgo(5))
+ );
+
+ List<String> collected = new ArrayList<>();
+ int pageSize = 2;
+ for (int offset = 0; offset < items.size(); offset += pageSize) {
+ PartialList<Metadata> page =
MergedMetadataListHelper.sortAndPage(items, offset, pageSize, "name:asc",
DEFAULT_QUERY_LIMIT);
+ collected.addAll(metadataIds(page));
+ }
+ assertEquals(Arrays.asList("a", "b", "c", "d", "e"), collected);
+ }
+
+ private static List<Segment> threeSegments() {
+ return new ArrayList<>(Arrays.asList(
+ segment("c", "Charlie", daysAgo(3)),
+ segment("a", "Alpha", daysAgo(1)),
+ segment("b", "Bravo", daysAgo(2))
+ ));
+ }
+
+ private static Segment segment(String id, String name, Date creationDate) {
+ Segment segment = new Segment();
+ segment.setItemId(id);
+ Metadata metadata = new Metadata();
+ metadata.setId(id);
+ metadata.setName(name);
+ metadata.setScope("systemscope");
+ segment.setMetadata(metadata);
+ segment.setCreationDate(creationDate);
+ segment.setLastModificationDate(creationDate);
+ return segment;
+ }
+
+ private static Date daysAgo(int days) {
+ return new Date(System.currentTimeMillis() - days * 24L * 60L * 60L *
1000L);
+ }
+
+ private static List<String> ids(List<Segment> items) {
+ return
items.stream().map(Segment::getItemId).collect(Collectors.toList());
+ }
+
+ private static List<String> metadataIds(PartialList<Metadata> page) {
+ return
page.getList().stream().map(Metadata::getId).collect(Collectors.toList());
+ }
+}
diff --git
a/services/src/test/java/org/apache/unomi/services/impl/segments/SegmentServiceImplTest.java
b/services/src/test/java/org/apache/unomi/services/impl/segments/SegmentServiceImplTest.java
index c7564fa9d..a127beed7 100644
---
a/services/src/test/java/org/apache/unomi/services/impl/segments/SegmentServiceImplTest.java
+++
b/services/src/test/java/org/apache/unomi/services/impl/segments/SegmentServiceImplTest.java
@@ -24,7 +24,9 @@ import org.apache.unomi.api.actions.Action;
import org.apache.unomi.api.conditions.Condition;
import org.apache.unomi.api.conditions.ConditionType;
import org.apache.unomi.api.exceptions.BadSegmentConditionException;
+import org.apache.unomi.api.query.Query;
import org.apache.unomi.api.rules.Rule;
+import org.apache.unomi.api.segments.Scoring;
import org.apache.unomi.api.segments.Segment;
import org.apache.unomi.api.segments.SegmentsAndScores;
import org.apache.unomi.api.services.EventService;
@@ -239,6 +241,33 @@ public class SegmentServiceImplTest {
return segment;
}
+ private Scoring createTestScoring(String scoringId, String name) {
+ Scoring scoring = new Scoring();
+ scoring.setItemId(scoringId);
+
scoring.setTenantId(executionContextManager.getCurrentContext().getTenantId());
+
+ Metadata metadata = new Metadata();
+ metadata.setId(scoringId);
+ metadata.setName(name);
+ metadata.setScope("systemscope");
+ metadata.setEnabled(false);
+ scoring.setMetadata(metadata);
+ scoring.setElements(Collections.emptyList());
+ return scoring;
+ }
+
+ private Date daysAgo(int days) {
+ return new Date(System.currentTimeMillis() - days * 24L * 60L * 60L *
1000L);
+ }
+
+ private List<String> metadataIds(PartialList<Metadata> page) {
+ List<String> ids = new ArrayList<>();
+ for (Metadata metadata : page.getList()) {
+ ids.add(metadata.getId());
+ }
+ return ids;
+ }
+
private Segment createPastEventSegment(String segmentId, String name,
String eventType, int numberOfDays) {
Segment segment = new Segment();
segment.setItemId(segmentId);
@@ -1160,4 +1189,218 @@ public class SegmentServiceImplTest {
return null;
});
}
+
+ @Test
+ public void testMergedSegmentListSortByNameAndDate() {
+ executionContextManager.executeAsSystem(() -> {
+ Segment zebra = createTestSegment("sys-zebra", "Zebra");
+ zebra.setCreationDate(daysAgo(5));
+ Segment middle = createTestSegment("sys-middle", "Middle");
+ middle.setCreationDate(daysAgo(3));
+ segmentService.setSegmentDefinition(zebra);
+ segmentService.setSegmentDefinition(middle);
+ return null;
+ });
+
+ executionContextManager.executeAsTenant(TENANT_1, () -> {
+ Segment alpha = createTestSegment("tenant-alpha", "Alpha");
+ alpha.setCreationDate(daysAgo(1));
+ segmentService.setSegmentDefinition(alpha);
+ persistenceService.refresh();
+
+ PartialList<Metadata> byNameAsc =
TestHelper.retryQueryUntilAvailable(
+ () -> segmentService.getSegmentMetadatas(0, 10, "name:asc"),
+ 3
+ );
+ assertEquals(Arrays.asList("tenant-alpha", "sys-middle",
"sys-zebra"), metadataIds(byNameAsc));
+
+ PartialList<Metadata> byNameDesc =
segmentService.getSegmentMetadatas(0, 10, "name:desc");
+ assertEquals(Arrays.asList("sys-zebra", "sys-middle",
"tenant-alpha"), metadataIds(byNameDesc));
+
+ PartialList<Metadata> byDateAsc =
segmentService.getSegmentMetadatas(0, 10, "creationDate:asc");
+ assertEquals(Arrays.asList("sys-zebra", "sys-middle",
"tenant-alpha"), metadataIds(byDateAsc));
+
+ PartialList<Metadata> byDateDesc =
segmentService.getSegmentMetadatas(0, 10, "creationDate:desc");
+ assertEquals(Arrays.asList("tenant-alpha", "sys-middle",
"sys-zebra"), metadataIds(byDateDesc));
+ return null;
+ });
+ }
+
+ @Test
+ public void testMergedSegmentListPagingAndQueryLimits() {
+ executionContextManager.executeAsSystem(() -> {
+ segmentService.setSegmentDefinition(createTestSegment("sys-a",
"A-System"));
+ segmentService.setSegmentDefinition(createTestSegment("sys-b",
"B-System"));
+ return null;
+ });
+
+ executionContextManager.executeAsTenant(TENANT_1, () -> {
+ segmentService.setSegmentDefinition(createTestSegment("ten-c",
"C-Tenant"));
+ segmentService.setSegmentDefinition(createTestSegment("ten-d",
"D-Tenant"));
+ segmentService.setSegmentDefinition(createTestSegment("ten-e",
"E-Tenant"));
+ persistenceService.refresh();
+
+ TestHelper.retryQueryUntilAvailable(() ->
segmentService.getSegmentMetadatas(0, 10, "name:asc"), 5);
+
+ PartialList<Metadata> page1 =
segmentService.getSegmentMetadatas(0, 2, "name:asc");
+ PartialList<Metadata> page2 =
segmentService.getSegmentMetadatas(2, 2, "name:asc");
+ PartialList<Metadata> page3 =
segmentService.getSegmentMetadatas(4, 2, "name:asc");
+ List<String> allPages = new ArrayList<>();
+ allPages.addAll(metadataIds(page1));
+ allPages.addAll(metadataIds(page2));
+ allPages.addAll(metadataIds(page3));
+ assertEquals(Arrays.asList("sys-a", "sys-b", "ten-c", "ten-d",
"ten-e"), allPages);
+ assertEquals(5, page1.getTotalSize());
+
+ Query noLimit = new Query();
+ PartialList<Metadata> noLimitResult =
segmentService.getSegmentMetadatas(noLimit);
+ assertEquals(5, noLimitResult.getList().size());
+ assertEquals(5, noLimitResult.getTotalSize());
+
+ Query limitMinusOne = new Query();
+ limitMinusOne.setLimit(-1);
+ assertEquals(5,
segmentService.getSegmentMetadatas(limitMinusOne).getList().size());
+
+ Query limitZero = new Query();
+ limitZero.setLimit(0);
+ assertEquals(0,
segmentService.getSegmentMetadatas(limitZero).getList().size());
+
+ Query largerThanTotal = new Query();
+ largerThanTotal.setLimit(100);
+ largerThanTotal.setSortby("name:asc");
+ assertEquals(5,
segmentService.getSegmentMetadatas(largerThanTotal).getList().size());
+
+ Query pastEnd = new Query();
+ pastEnd.setOffset(20);
+ pastEnd.setLimit(5);
+ assertEquals(0,
segmentService.getSegmentMetadatas(pastEnd).getList().size());
+ assertEquals(5,
segmentService.getSegmentMetadatas(pastEnd).getTotalSize());
+
+ // Like the persistence services, unknown sort fields are ignored
rather than rejected
+ PartialList<Metadata> unknownSort =
segmentService.getSegmentMetadatas(0, 10, "notARealField:asc");
+ assertEquals(5, unknownSort.getList().size());
+ assertEquals(5, unknownSort.getTotalSize());
+ return null;
+ });
+ }
+
+ @Test
+ public void testMergedListsUseConfiguredDefaultQueryLimit() {
+ ((InMemoryPersistenceServiceImpl)
persistenceService).setDefaultQueryLimit(2);
+
+ executionContextManager.executeAsSystem(() -> {
+ segmentService.setSegmentDefinition(createTestSegment("sys-a",
"A-System"));
+
segmentService.setScoringDefinition(createTestScoring("sys-score-a",
"A-Score"));
+ return null;
+ });
+
+ executionContextManager.executeAsTenant(TENANT_1, () -> {
+ segmentService.setSegmentDefinition(createTestSegment("ten-b",
"B-Tenant"));
+ segmentService.setSegmentDefinition(createTestSegment("ten-c",
"C-Tenant"));
+ segmentService.setSegmentDefinition(createTestSegment("ten-d",
"D-Tenant"));
+
segmentService.setScoringDefinition(createTestScoring("ten-score-b",
"B-Score"));
+
segmentService.setScoringDefinition(createTestScoring("ten-score-c",
"C-Score"));
+
segmentService.setScoringDefinition(createTestScoring("ten-score-d",
"D-Score"));
+ persistenceService.refresh();
+
+ // More items per tenant than the limit: the limit must only apply
to the merged page
+ Query all = new Query();
+ all.setLimit(-1);
+ TestHelper.retryQueryUntilAvailable(() ->
segmentService.getSegmentMetadatas(all), 4);
+ TestHelper.retryQueryUntilAvailable(() ->
segmentService.getScoringMetadatas(all), 4);
+
+ Query noLimit = new Query();
+ noLimit.setSortby("metadata.name:asc");
+ PartialList<Metadata> segments =
segmentService.getSegmentMetadatas(noLimit);
+ assertEquals(Arrays.asList("sys-a", "ten-b"),
metadataIds(segments));
+ assertEquals(4, segments.getTotalSize());
+
+ PartialList<Metadata> scorings =
segmentService.getScoringMetadatas(noLimit);
+ assertEquals(Arrays.asList("sys-score-a", "ten-score-b"),
metadataIds(scorings));
+ assertEquals(4, scorings.getTotalSize());
+
+ assertEquals(2, segmentService.getSegmentMetadatas(0,
Integer.MIN_VALUE, "name:asc").getList().size());
+ assertEquals(4,
segmentService.getSegmentMetadatas(all).getList().size());
+ return null;
+ });
+ }
+
+ @Test
+ public void testMergedSegmentListTenantOverridesSystemItem() {
+ executionContextManager.executeAsSystem(() -> {
+ segmentService.setSegmentDefinition(createTestSegment("shared",
"System Version"));
+ segmentService.setSegmentDefinition(createTestSegment("sys-only",
"System Only"));
+ return null;
+ });
+
+ executionContextManager.executeAsTenant(TENANT_1, () -> {
+ segmentService.setSegmentDefinition(createTestSegment("shared",
"Tenant Version"));
+ persistenceService.refresh();
+
+ PartialList<Metadata> result = TestHelper.retryQueryUntilAvailable(
+ () -> segmentService.getSegmentMetadatas(0, 10, "name:asc"),
+ 2
+ );
+ assertEquals(Arrays.asList("sys-only", "shared"),
metadataIds(result));
+ assertEquals(2, result.getTotalSize());
+ assertEquals("Tenant Version", result.getList().get(1).getName());
+ return null;
+ });
+ }
+
+ @Test
+ public void testMergedScoringListSortPagingAndQueryLimits() {
+ executionContextManager.executeAsSystem(() -> {
+ Scoring late = createTestScoring("sys-late", "Late");
+ late.setCreationDate(daysAgo(4));
+ Scoring early = createTestScoring("sys-early", "Early");
+ early.setCreationDate(daysAgo(1));
+ segmentService.setScoringDefinition(late);
+ segmentService.setScoringDefinition(early);
+ return null;
+ });
+
+ executionContextManager.executeAsTenant(TENANT_1, () -> {
+ Scoring mid = createTestScoring("ten-mid", "Mid");
+ mid.setCreationDate(daysAgo(2));
+ segmentService.setScoringDefinition(mid);
+ persistenceService.refresh();
+
+ PartialList<Metadata> byNameAsc =
TestHelper.retryQueryUntilAvailable(
+ () -> segmentService.getScoringMetadatas(0, 10, "name:asc"),
+ 3
+ );
+ assertEquals(Arrays.asList("sys-early", "sys-late", "ten-mid"),
metadataIds(byNameAsc));
+
+ PartialList<Metadata> byDateDesc =
segmentService.getScoringMetadatas(0, 10, "creationDate:desc");
+ assertEquals(Arrays.asList("sys-early", "ten-mid", "sys-late"),
metadataIds(byDateDesc));
+
+ PartialList<Metadata> page1 =
segmentService.getScoringMetadatas(0, 2, "name:asc");
+ PartialList<Metadata> page2 =
segmentService.getScoringMetadatas(2, 2, "name:asc");
+ List<String> allPages = new ArrayList<>();
+ allPages.addAll(metadataIds(page1));
+ allPages.addAll(metadataIds(page2));
+ assertEquals(Arrays.asList("sys-early", "sys-late", "ten-mid"),
allPages);
+
+ Query noLimit = new Query();
+ assertEquals(3,
segmentService.getScoringMetadatas(noLimit).getList().size());
+
+ Query limitMinusOne = new Query();
+ limitMinusOne.setLimit(-1);
+ assertEquals(3,
segmentService.getScoringMetadatas(limitMinusOne).getList().size());
+
+ Query limitZero = new Query();
+ limitZero.setLimit(0);
+ assertEquals(0,
segmentService.getScoringMetadatas(limitZero).getList().size());
+
+ Query pastEnd = new Query();
+ pastEnd.setOffset(50);
+ pastEnd.setLimit(10);
+ assertEquals(0,
segmentService.getScoringMetadatas(pastEnd).getList().size());
+
+ assertEquals(3, segmentService.getScoringMetadatas(0, 10,
"properties.score:asc").getList().size());
+ return null;
+ });
+ }
}
+