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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 3a9249fa3a [core] Support count-limit for collect agg (#10540)
3a9249fa3a is described below

commit 3a9249fa3a0575576541500ca1b7a8aa339ee939
Author: tsreaper <[email protected]>
AuthorDate: Wed Oct 14 13:29:44 2026 +0800

    [core] Support count-limit for collect agg (#10540)
---
 .../primary-key-table/merge-engine/aggregation.mdx |  8 ++
 .../main/java/org/apache/paimon/CoreOptions.java   |  7 ++
 .../compact/aggregate/FieldCollectAgg.java         | 99 +++++++++++++---------
 .../compact/aggregate/FieldIgnoreRetractAgg.java   |  5 ++
 .../aggregate/factory/FieldCollectAggFactory.java  |  5 +-
 .../compact/aggregate/FieldAggregatorTest.java     | 45 ++++++++++
 6 files changed, 126 insertions(+), 43 deletions(-)

diff --git a/docs/docs/primary-key-table/merge-engine/aggregation.mdx 
b/docs/docs/primary-key-table/merge-engine/aggregation.mdx
index 3e81d1d52d..b0dbe03e00 100644
--- a/docs/docs/primary-key-table/merge-engine/aggregation.mdx
+++ b/docs/docs/primary-key-table/merge-engine/aggregation.mdx
@@ -181,6 +181,14 @@ count of distinct event IDs.
   The collect function collects elements into an Array. You can set 
`fields.<field-name>.distinct=true` to deduplicate elements.
   It only supports ARRAY type.
 
+  Use `fields.<field-name>.count-limit=<Integer>` to limit the number of 
collected elements. There is no limit by default.
+  A positive value `N` keeps the first `N` elements in aggregation order; a 
negative value `-N` keeps the last `N` elements.
+
+  With `distinct=true`, the limit counts distinct elements: positive limits 
retain the first occurrence of each element,
+  while negative limits retain the last occurrence. The retained elements keep 
their original order.
+  For example, collecting `[1, 2, 1, 3]` with `distinct=true` produces `[1, 
2]` for `count-limit=2`
+  and `[1, 3]` for `count-limit=-2`.
+
 ### merge_map
   The merge_map function merge input maps. It only supports MAP type.
 
diff --git a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java 
b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
index d44370d760..f9cb0914f6 100644
--- a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
+++ b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
@@ -3500,6 +3500,13 @@ public class CoreOptions implements Serializable {
                         .defaultValue(false));
     }
 
+    public int fieldCollectAggCountLimit(String fieldName) {
+        return options.get(
+                key(FIELDS_PREFIX + "." + fieldName + "." + COUNT_LIMIT)
+                        .intType()
+                        .defaultValue(Integer.MAX_VALUE));
+    }
+
     public String fieldListAggDelimiter(String fieldName) {
         return options.get(
                 key(FIELDS_PREFIX + "." + fieldName + "." + LIST_AGG_DELIMITER)
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/aggregate/FieldCollectAgg.java
 
b/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/aggregate/FieldCollectAgg.java
index 60f60e82ce..4559f07eb9 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/aggregate/FieldCollectAgg.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/aggregate/FieldCollectAgg.java
@@ -33,8 +33,8 @@ import javax.annotation.Nullable;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.function.BiFunction;
 
@@ -46,12 +46,14 @@ public class FieldCollectAgg extends FieldAggregator {
     private static final long serialVersionUID = 1L;
 
     private final boolean distinct;
+    private final int countLimit;
     private final InternalArray.ElementGetter elementGetter;
     @Nullable private final BiFunction<Object, Object, Boolean> equaliser;
 
-    public FieldCollectAgg(String name, ArrayType dataType, boolean distinct) {
+    public FieldCollectAgg(String name, ArrayType dataType, boolean distinct, 
int countLimit) {
         super(name, dataType);
         this.distinct = distinct;
+        this.countLimit = countLimit;
         this.elementGetter = 
InternalArray.createElementGetter(dataType.getElementType());
 
         // The equaliser is built from the element type alone: retract() needs 
it whether or not
@@ -95,69 +97,82 @@ public class FieldCollectAgg extends FieldAggregator {
 
     @Override
     public Object aggReversed(Object accumulator, Object inputField) {
-        // we don't need to actually do the reverse here for this agg
-        // because accumulator has been distinct, just let accumulator be 
accumulator will speed up
-        // distinct process
-        return agg(accumulator, inputField);
+        if (countLimit != Integer.MAX_VALUE) {
+            // order is important with limit
+            return agg(inputField, accumulator, true);
+        } else {
+            // we don't need to actually do the reverse here for this agg
+            // because accumulator has been distinct, just let accumulator be 
accumulator will speed
+            // up
+            // distinct process
+            return agg(accumulator, inputField, false);
+        }
     }
 
     @Override
     public Object agg(Object accumulator, Object inputField) {
-        if (accumulator == null && inputField == null) {
-            return null;
-        }
+        return agg(accumulator, inputField, false);
+    }
 
-        if ((accumulator == null || inputField == null) && !distinct) {
-            return accumulator == null ? inputField : accumulator;
-        }
+    private Object agg(Object accumulator, Object inputField, boolean 
accumulatorHasDuplicate) {
+        InternalArray accumulatorArray = (InternalArray) accumulator;
+        InternalArray inputArray = (InternalArray) inputField;
 
-        if (distinct && equaliser != null) {
-            List<Object> collection = new ArrayList<>();
-            // do not need to distinct accumulator, because the accumulator is 
always distinct, no
-            // need to distinct it every time
-            collect(collection, accumulator);
-            collectWithEqualiser(collection, inputField);
+        if (accumulatorArray == null && inputArray == null) {
+            return null;
+        } else if (inputArray == null
+                && !distinct
+                && accumulatorArray.size() <= Math.abs(countLimit)) {
+            return accumulatorArray;
+        } else if (accumulatorArray == null
+                && !distinct
+                && inputArray.size() <= Math.abs(countLimit)) {
+            return inputArray;
+        }
+
+        boolean removeDuplicate = distinct && equaliser != null;
+        Collection<Object> collection =
+                distinct && equaliser == null ? new LinkedHashSet<>() : new 
ArrayList<>();
+        if (countLimit >= 0) {
+            collect(collection, accumulatorArray, removeDuplicate && 
accumulatorHasDuplicate);
+            collect(collection, inputArray, removeDuplicate);
             return new GenericArray(collection.toArray());
         } else {
-            Collection<Object> collection = distinct ? new HashSet<>() : new 
ArrayList<>();
-            collect(collection, accumulator);
-            collect(collection, inputField);
-            return new GenericArray(collection.toArray());
+            collect(collection, inputArray, removeDuplicate);
+            collect(collection, accumulatorArray, removeDuplicate);
+            List<Object> result =
+                    collection instanceof List
+                            ? (List<Object>) collection
+                            : new ArrayList<>(collection);
+            Collections.reverse(result);
+            return new GenericArray(result.toArray());
         }
     }
 
-    private void collect(Collection<Object> collection, @Nullable Object data) 
{
-        if (data == null) {
+    private void collect(
+            Collection<Object> collection, @Nullable InternalArray array, 
boolean removeDuplicate) {
+        if (array == null) {
             return;
         }
 
-        InternalArray array = (InternalArray) data;
-        for (int i = 0; i < array.size(); i++) {
-            collection.add(elementGetter.getElementOrNull(array, i));
-        }
-    }
-
-    private void collectWithEqualiser(List<Object> list, Object data) {
-        if (data == null) {
-            return;
-        }
-
-        InternalArray array = (InternalArray) data;
-        for (int i = 0; i < array.size(); i++) {
-            Object element = elementGetter.getElementOrNull(array, i);
-            if (!contains(list, element)) {
-                list.add(element);
+        long limit = Math.abs((long) countLimit);
+        for (int i = 0; i < array.size() && collection.size() < limit; i++) {
+            Object element =
+                    elementGetter.getElementOrNull(
+                            array, countLimit < 0 ? array.size() - 1 - i : i);
+            if (!(removeDuplicate && contains(collection, element))) {
+                collection.add(element);
             }
         }
     }
 
-    private boolean contains(List<Object> list, @Nullable Object element) {
+    private boolean contains(Collection<Object> list, @Nullable Object 
element) {
         if (element == null) {
             return list.contains(null);
         }
 
         for (Object o : list) {
-            if (equaliser.apply(o, element)) {
+            if (equals(o, element)) {
                 return true;
             }
         }
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/aggregate/FieldIgnoreRetractAgg.java
 
b/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/aggregate/FieldIgnoreRetractAgg.java
index e98e64852b..341bb19d8e 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/aggregate/FieldIgnoreRetractAgg.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/aggregate/FieldIgnoreRetractAgg.java
@@ -35,6 +35,11 @@ public class FieldIgnoreRetractAgg extends FieldAggregator {
         return aggregator.agg(accumulator, inputField);
     }
 
+    @Override
+    public Object aggReversed(Object accumulator, Object inputField) {
+        return aggregator.aggReversed(accumulator, inputField);
+    }
+
     @Override
     public void reset() {
         aggregator.reset();
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/aggregate/factory/FieldCollectAggFactory.java
 
b/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/aggregate/factory/FieldCollectAggFactory.java
index a4325d165b..6b656073e6 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/aggregate/factory/FieldCollectAggFactory.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/aggregate/factory/FieldCollectAggFactory.java
@@ -37,7 +37,10 @@ public class FieldCollectAggFactory implements 
FieldAggregatorFactory {
                 "Data type for collect column must be 'Array' but was '%s'.",
                 fieldType);
         return new FieldCollectAgg(
-                identifier(), (ArrayType) fieldType, 
options.fieldCollectAggDistinct(field));
+                identifier(),
+                (ArrayType) fieldType,
+                options.fieldCollectAggDistinct(field),
+                options.fieldCollectAggCountLimit(field));
     }
 
     @Override
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/mergetree/compact/aggregate/FieldAggregatorTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/mergetree/compact/aggregate/FieldAggregatorTest.java
index b915866e7f..4e9e2852af 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/mergetree/compact/aggregate/FieldAggregatorTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/mergetree/compact/aggregate/FieldAggregatorTest.java
@@ -2139,6 +2139,51 @@ public class FieldAggregatorTest {
         return GenericRow.of(k0, k1, BinaryString.fromString(v), seq, ts);
     }
 
+    @Test
+    public void testFieldCollectAggWithCountLimit() {
+        DataType[] elementTypes = {DataTypes.INT(), 
RowType.of(DataTypes.INT())};
+        for (DataType elementType : elementTypes) {
+            assertCollectCountLimit(elementType, false, 3, 2, 1, 2);
+            assertCollectCountLimit(elementType, true, 3, 2, 1, 3);
+            assertCollectCountLimit(elementType, false, -3, 3, 3, 1);
+            assertCollectCountLimit(elementType, true, -3, 2, 3, 1);
+            assertCollectCountLimit(elementType, false, 0);
+            assertCollectCountLimit(elementType, true, 0);
+        }
+    }
+
+    private void assertCollectCountLimit(
+            DataType elementType, boolean distinct, int countLimit, int... 
expected) {
+        Map<String, String> config = new HashMap<>();
+        config.put("fields.fieldName.distinct", Boolean.toString(distinct));
+        config.put("fields.fieldName.count-limit", 
Integer.toString(countLimit));
+        CoreOptions options = CoreOptions.fromMap(config);
+        ArrayType arrayType = DataTypes.ARRAY(elementType);
+        FieldCollectAgg agg = new FieldCollectAggFactory().create(arrayType, 
options, "fieldName");
+        InternalArray.ElementGetter elementGetter = 
InternalArray.createElementGetter(elementType);
+        Object[] expectedElements = makeArray(elementType, 
expected).toObjectArray();
+
+        InternalArray input = makeArray(elementType, 2, 1, 2, 3, 3, 1);
+        InternalArray single = (InternalArray) agg.agg(null, input);
+        assertThat(unnest(single, 
elementGetter)).containsExactly(expectedElements);
+
+        InternalArray older = makeArray(elementType, 2, 1, 2);
+        InternalArray newer = makeArray(elementType, 3, 3, 1);
+        InternalArray forward = (InternalArray) agg.agg(agg.agg(null, older), 
newer);
+        InternalArray reversed =
+                (InternalArray) agg.aggReversed(agg.aggReversed(null, newer), 
older);
+        assertThat(unnest(forward, 
elementGetter)).containsExactly(expectedElements);
+        assertThat(unnest(reversed, 
elementGetter)).containsExactly(expectedElements);
+    }
+
+    private GenericArray makeArray(DataType elementType, int... values) {
+        Object[] elements = new Object[values.length];
+        for (int i = 0; i < values.length; i++) {
+            elements[i] = elementType instanceof RowType ? 
GenericRow.of(values[i]) : values[i];
+        }
+        return new GenericArray(elements);
+    }
+
     @Test
     public void testFieldCollectAggWithDistinct() {
         FieldCollectAgg agg =

Reply via email to