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 ce9817b9e8 [core] Support nested-key-null-strategy in 
FieldNestedPartialUpdateAgg operator. (#8461)
ce9817b9e8 is described below

commit ce9817b9e86079f7cd236c9c87c320dedc93e8d3
Author: AuroraVoyage <[email protected]>
AuthorDate: Sat Jul 4 22:27:07 2026 +0800

    [core] Support nested-key-null-strategy in FieldNestedPartialUpdateAgg 
operator. (#8461)
---
 .../primary-key-table/merge-engine/aggregation.mdx |  7 ++
 .../aggregate/FieldNestedPartialUpdateAgg.java     | 51 +++++++++++--
 .../FieldNestedPartialUpdateAggFactory.java        | 11 ++-
 .../compact/aggregate/FieldAggregatorTest.java     | 89 +++++++++++++++++++++-
 4 files changed, 148 insertions(+), 10 deletions(-)

diff --git a/docs/docs/primary-key-table/merge-engine/aggregation.mdx 
b/docs/docs/primary-key-table/merge-engine/aggregation.mdx
index 1f3cab9340..823ae7b016 100644
--- a/docs/docs/primary-key-table/merge-engine/aggregation.mdx
+++ b/docs/docs/primary-key-table/merge-engine/aggregation.mdx
@@ -392,6 +392,13 @@ public static class BitmapContainsUDF extends 
ScalarFunction {
   ARRAY\<ROW\> data types. You need to use 
`fields.<field-name>.nested-key=pk0,pk1,...` to specify the primary keys of the
   nested table. The values in each row are written by partial updating some 
columns.
 
+  Use `fields.<field-name>.nested-key-null-strategy=<merge|ignore|error>` to 
specify how rows are handled when the nested-key does not satisfy primary key 
semantics (e.g., primary key fields contain null values).
+
+   - `merge` (default): Merge rows even if the nested-key does not satisfy 
primary key semantics. This is the same behavior as when this option is not 
configured.
+   - `ignore`: Ignore rows whose nested-key contains null values because they 
do not satisfy primary key semantics.
+   - `error`: Throw an exception if the nested-key contains null values, 
because primary key fields must not be null.
+
+
 ### collect
   The collect function collects elements into an Array. You can set 
`fields.<field-name>.distinct=true` to deduplicate elements.
   It only supports ARRAY type.
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/aggregate/FieldNestedPartialUpdateAgg.java
 
b/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/aggregate/FieldNestedPartialUpdateAgg.java
index d8e058e839..06e9e64a2f 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/aggregate/FieldNestedPartialUpdateAgg.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/aggregate/FieldNestedPartialUpdateAgg.java
@@ -18,6 +18,7 @@
 
 package org.apache.paimon.mergetree.compact.aggregate;
 
+import org.apache.paimon.CoreOptions;
 import org.apache.paimon.codegen.Projection;
 import org.apache.paimon.data.BinaryRow;
 import org.apache.paimon.data.GenericArray;
@@ -29,6 +30,7 @@ import org.apache.paimon.types.ArrayType;
 import org.apache.paimon.types.RowType;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -47,8 +49,13 @@ public class FieldNestedPartialUpdateAgg extends 
FieldAggregator {
     private final int nestedFields;
     private final Projection keyProjection;
     private final FieldGetter[] fieldGetters;
+    private final CoreOptions.NestedKeyNullStrategy nestedKeyNullStrategy;
 
-    public FieldNestedPartialUpdateAgg(String name, ArrayType dataType, 
List<String> nestedKey) {
+    public FieldNestedPartialUpdateAgg(
+            String name,
+            ArrayType dataType,
+            List<String> nestedKey,
+            CoreOptions.NestedKeyNullStrategy nestedKeyNullStrategy) {
         super(name, dataType);
         RowType nestedType = (RowType) dataType.getElementType();
         this.nestedFields = nestedType.getFieldCount();
@@ -58,25 +65,34 @@ public class FieldNestedPartialUpdateAgg extends 
FieldAggregator {
         for (int i = 0; i < nestedFields; i++) {
             fieldGetters[i] = 
InternalRow.createFieldGetter(nestedType.getTypeAt(i), i);
         }
+        this.nestedKeyNullStrategy = nestedKeyNullStrategy;
     }
 
     @Override
     public Object agg(Object accumulator, Object inputField) {
-        if (accumulator == null || inputField == null) {
-            return accumulator == null ? inputField : accumulator;
+        if (inputField == null) {
+            return accumulator;
         }
 
-        InternalArray acc = (InternalArray) accumulator;
         InternalArray input = (InternalArray) inputField;
 
-        List<InternalRow> rows = new ArrayList<>(acc.size() + input.size());
-        addNonNullRows(acc, rows);
+        List<InternalRow> rows;
+        if (accumulator == null) {
+            rows = new ArrayList<>(input.size());
+        } else {
+            InternalArray acc = (InternalArray) accumulator;
+            rows = new ArrayList<>(acc.size() + input.size());
+            addNonNullRows(acc, rows);
+        }
         addNonNullRows(input, rows);
 
         if (keyProjection != null) {
             Map<BinaryRow, GenericRow> map = new HashMap<>();
             for (InternalRow row : rows) {
                 BinaryRow key = keyProjection.apply(row).copy();
+                if (!applyNestedKeyNullStrategy(key)) {
+                    continue;
+                }
                 GenericRow toUpdate = map.computeIfAbsent(key, k -> new 
GenericRow(nestedFields));
                 partialUpdate(toUpdate, row);
             }
@@ -105,4 +121,27 @@ public class FieldNestedPartialUpdateAgg extends 
FieldAggregator {
             }
         }
     }
+
+    private boolean applyNestedKeyNullStrategy(BinaryRow key) {
+        if (!key.anyNull()) {
+            // The nested-key satisfies primary key semantics.
+            return true;
+        }
+        switch (nestedKeyNullStrategy) {
+            case MERGE:
+                // Preserve the previous behavior.
+                return true;
+            case IGNORE:
+                return false;
+            case ERROR:
+                throw new IllegalArgumentException(
+                        "Nested key contains null values. Primary key fields 
must not be null.");
+            default:
+                throw new UnsupportedOperationException(
+                        String.format(
+                                "Unsupported nested-key-null-strategy '%s'. 
Supported values are: %s.",
+                                nestedKeyNullStrategy,
+                                
Arrays.toString(CoreOptions.NestedKeyNullStrategy.values())));
+        }
+    }
 }
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/aggregate/factory/FieldNestedPartialUpdateAggFactory.java
 
b/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/aggregate/factory/FieldNestedPartialUpdateAggFactory.java
index 80b899a8b2..9a22dc5cd8 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/aggregate/factory/FieldNestedPartialUpdateAggFactory.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/aggregate/factory/FieldNestedPartialUpdateAggFactory.java
@@ -37,7 +37,9 @@ public class FieldNestedPartialUpdateAggFactory implements 
FieldAggregatorFactor
     public FieldNestedPartialUpdateAgg create(
             DataType fieldType, CoreOptions options, String field) {
         return createFieldNestedPartialUpdateAgg(
-                fieldType, options.fieldNestedUpdateAggNestedKey(field));
+                fieldType,
+                options.fieldNestedUpdateAggNestedKey(field),
+                options.fieldNestedUpdateAggNestedKeyNullStrategy(field));
     }
 
     @Override
@@ -46,13 +48,16 @@ public class FieldNestedPartialUpdateAggFactory implements 
FieldAggregatorFactor
     }
 
     private FieldNestedPartialUpdateAgg createFieldNestedPartialUpdateAgg(
-            DataType fieldType, List<String> nestedKey) {
+            DataType fieldType,
+            List<String> nestedKey,
+            CoreOptions.NestedKeyNullStrategy nestedKeyNullStrategy) {
         checkArgument(!nestedKey.isEmpty());
         String typeErrorMsg =
                 "Data type for nested table column must be 'Array<Row>' but 
was '%s'.";
         checkArgument(fieldType instanceof ArrayType, typeErrorMsg, fieldType);
         ArrayType arrayType = (ArrayType) fieldType;
         checkArgument(arrayType.getElementType() instanceof RowType, 
typeErrorMsg, fieldType);
-        return new FieldNestedPartialUpdateAgg(identifier(), arrayType, 
nestedKey);
+        return new FieldNestedPartialUpdateAgg(
+                identifier(), arrayType, nestedKey, nestedKeyNullStrategy);
     }
 }
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 77cc470cfb..183a1b63ef 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
@@ -2423,7 +2423,8 @@ public class FieldAggregatorTest {
                                         DataTypes.FIELD(0, "k", 
DataTypes.INT()),
                                         DataTypes.FIELD(1, "v1", 
DataTypes.INT()),
                                         DataTypes.FIELD(2, "v2", 
DataTypes.STRING()))),
-                        Collections.singletonList("k"));
+                        Collections.singletonList("k"),
+                        CoreOptions.NestedKeyNullStrategy.MERGE);
 
         InternalArray accumulator;
         InternalArray.ElementGetter elementGetter =
@@ -2448,6 +2449,92 @@ public class FieldAggregatorTest {
         accumulator = (InternalArray) agg.agg(accumulator, 
singletonArray(current));
         assertThat(unnest(accumulator, elementGetter))
                 .containsExactlyInAnyOrderElementsOf(Arrays.asList(row(0, 1, 
"B"), row(1, 2, "C")));
+
+        // Verify MERGE strategy keeps rows with null nested keys.
+        current = row(null, 0, "D");
+        accumulator = (InternalArray) agg.agg(accumulator, 
singletonArray(current));
+        assertThat(unnest(accumulator, elementGetter))
+                .containsExactlyInAnyOrderElementsOf(
+                        Arrays.asList(row(0, 1, "B"), row(1, 2, "C"), 
row(null, 0, "D")));
+    }
+
+    @Test
+    public void 
testFieldNestedPartialUpdateAggWithNestedKeyNullUseIgnoreStrategy() {
+        DataType elementRowType =
+                DataTypes.ROW(
+                        DataTypes.FIELD(0, "k", DataTypes.INT()),
+                        DataTypes.FIELD(1, "v1", DataTypes.INT()),
+                        DataTypes.FIELD(2, "v2", DataTypes.STRING()));
+        FieldNestedPartialUpdateAgg agg =
+                new FieldNestedPartialUpdateAgg(
+                        FieldNestedPartialUpdateAggFactory.NAME,
+                        DataTypes.ARRAY(
+                                DataTypes.ROW(
+                                        DataTypes.FIELD(0, "k", 
DataTypes.INT()),
+                                        DataTypes.FIELD(1, "v1", 
DataTypes.INT()),
+                                        DataTypes.FIELD(2, "v2", 
DataTypes.STRING()))),
+                        Collections.singletonList("k"),
+                        CoreOptions.NestedKeyNullStrategy.IGNORE);
+
+        InternalArray accumulator;
+        InternalArray.ElementGetter elementGetter =
+                InternalArray.createElementGetter(elementRowType);
+
+        InternalRow current = row(0, 0, null);
+        accumulator = (InternalArray) agg.agg(null, singletonArray(current));
+        assertThat(unnest(accumulator, elementGetter))
+                
.containsExactlyInAnyOrderElementsOf(Collections.singletonList(current));
+
+        // Verify rows with null nested keys are ignored.
+        current = row(null, null, "A_ignore");
+        accumulator = (InternalArray) agg.agg(accumulator, 
singletonArray(current));
+        assertThat(unnest(accumulator, elementGetter))
+                
.containsExactlyInAnyOrderElementsOf(Collections.singletonList(row(0, 0, 
null)));
+
+        // Verify IGNORE strategy is also applied during the first aggregation.
+        current = row(null, null, "FirstInput");
+        InternalArray result = (InternalArray) agg.agg(null, 
singletonArray(current));
+        assertThat(unnest(result, elementGetter)).isEmpty();
+    }
+
+    @Test
+    public void 
testFieldNestedPartialUpdateAggWithNestedKeyNullUseThrowErrorStrategy() {
+        DataType elementRowType =
+                DataTypes.ROW(
+                        DataTypes.FIELD(0, "k", DataTypes.INT()),
+                        DataTypes.FIELD(1, "v1", DataTypes.INT()),
+                        DataTypes.FIELD(2, "v2", DataTypes.STRING()));
+        FieldNestedPartialUpdateAgg agg =
+                new FieldNestedPartialUpdateAgg(
+                        FieldNestedPartialUpdateAggFactory.NAME,
+                        DataTypes.ARRAY(
+                                DataTypes.ROW(
+                                        DataTypes.FIELD(0, "k", 
DataTypes.INT()),
+                                        DataTypes.FIELD(1, "v1", 
DataTypes.INT()),
+                                        DataTypes.FIELD(2, "v2", 
DataTypes.STRING()))),
+                        Collections.singletonList("k"),
+                        CoreOptions.NestedKeyNullStrategy.ERROR);
+
+        InternalArray accumulator;
+        InternalArray.ElementGetter elementGetter =
+                InternalArray.createElementGetter(elementRowType);
+
+        InternalRow current = row(0, 0, null);
+        accumulator = (InternalArray) agg.agg(null, singletonArray(current));
+        assertThat(unnest(accumulator, elementGetter))
+                
.containsExactlyInAnyOrderElementsOf(Collections.singletonList(current));
+
+        // Verify ERROR strategy rejects rows with null nested keys.
+        assertThatThrownBy(() -> agg.agg(accumulator, singletonArray(row(null, 
0, "A", 2))))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage(
+                        "Nested key contains null values. Primary key fields 
must not be null.");
+
+        // Verify ERROR strategy is also applied during the first aggregation.
+        assertThatThrownBy(() -> agg.agg(null, singletonArray(row(null, null, 
"FirstInput"))))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage(
+                        "Nested key contains null values. Primary key fields 
must not be null.");
     }
 
     private Map<Object, Object> toMap(Object... kvs) {

Reply via email to