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

mxm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git


The following commit(s) were added to refs/heads/main by this push:
     new c8d68218df Flink: Distribute rows by custom fields to DynamicWriter in 
DynamicSink (#18668)
c8d68218df is described below

commit c8d68218df68f96000e39387e6491736b23a25c2
Author: Swapna Marru <[email protected]>
AuthorDate: Mon Nov 9 07:49:27 2026 -0800

    Flink: Distribute rows by custom fields to DynamicWriter in DynamicSink 
(#18668)
---
 docs/docs/flink-writes.md                          |   3 +-
 .../iceberg/flink/sink/dynamic/DynamicRecord.java  |   9 +
 .../sink/dynamic/DynamicRecordWithConfig.java      |   5 +
 .../flink/sink/dynamic/DynamicSinkUtil.java        |  15 ++
 .../flink/sink/dynamic/HashKeyGenerator.java       |  83 +++++--
 .../flink/sink/dynamic/TestDynamicIcebergSink.java |  72 +++++-
 .../flink/sink/dynamic/TestHashKeyGenerator.java   | 261 +++++++++++++++++++++
 7 files changed, 427 insertions(+), 21 deletions(-)

diff --git a/docs/docs/flink-writes.md b/docs/docs/flink-writes.md
index deece55bb8..e9b7c4dcc5 100644
--- a/docs/docs/flink-writes.md
+++ b/docs/docs/flink-writes.md
@@ -516,6 +516,7 @@ We need the following information (DynamicRecord) for every 
record:
 | `Parallelism`      | The maximum number of parallel writers for a given 
table/branch/schema/spec (WriteTarget). |
 | `UpsertMode`       | Overrides this table's write.upsert.enabled (optional). 
                                  |
 | `EqualityFields`   | The equality fields for the table (optional). When 
unset, the dynamic sink falls back to the schema's identifier fields for both 
distribution (records sharing a key route to the same writer) and 
equality-delete inference. Identifier fields do not auto-enable upsert mode — 
that flag remains opt-in. |
+| `DistributionFields` | The fields to hash records by when the distribution 
mode is `HASH` (optional). When set, they replace the partition key and 
equality fields as the hash key. If equality fields are set, the distribution 
fields must be a subset of them. The same applies to the schema's identifier 
fields if equality fields are unset. Ignored for other distribution modes. |
 
 ### Schema Evolution
 
@@ -589,7 +590,7 @@ The `DistributionMode` set on each `DynamicRecord` controls 
how that record is r
 | Mode   | Behavior                                                            
                                                                                
                                         |
 
|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 | `NONE` | Records are distributed across writer subtasks in a round-robin 
fashion (or by equality fields if set).                                         
                                             |
-| `HASH` | Records are distributed by partition key (partitioned tables) or 
equality fields (unpartitioned tables). Ensures that records for the same 
partition are handled by the same writer subtask. |
+| `HASH` | Records are distributed by the distribution fields if set, 
otherwise by partition key (partitioned tables) or equality fields 
(unpartitioned tables). Without distribution fields, records for the same 
partition are handled by the same writer subtask. |
 | `null` | Forward mode: bypasses distribution entirely and sends records 
directly via a forward edge (see below).                                        
                                              |
 
 #### Forward Mode
diff --git 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecord.java
 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecord.java
index 6507a575c2..60bb3a272a 100644
--- 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecord.java
+++ 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecord.java
@@ -39,6 +39,7 @@ public class DynamicRecord {
   private int writeParallelism;
   private boolean upsertMode;
   @Nullable private Set<String> equalityFields;
+  @Nullable private Set<String> distributionFields;
 
   @Internal
   DynamicRecord() {}
@@ -165,4 +166,12 @@ public class DynamicRecord {
   public void setEqualityFields(Set<String> equalityFields) {
     this.equalityFields = equalityFields;
   }
+
+  public Set<String> distributionFields() {
+    return distributionFields;
+  }
+
+  public void setDistributionFields(Set<String> distributionFields) {
+    this.distributionFields = distributionFields;
+  }
 }
diff --git 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordWithConfig.java
 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordWithConfig.java
index 32716c3e4a..569bcf5c9a 100644
--- 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordWithConfig.java
+++ 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicRecordWithConfig.java
@@ -91,4 +91,9 @@ class DynamicRecordWithConfig extends DynamicRecord {
   public Set<String> equalityFields() {
     return wrapped.equalityFields();
   }
+
+  @Override
+  public Set<String> distributionFields() {
+    return wrapped.distributionFields();
+  }
 }
diff --git 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicSinkUtil.java
 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicSinkUtil.java
index dea135ddca..e278e13612 100644
--- 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicSinkUtil.java
+++ 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicSinkUtil.java
@@ -71,6 +71,21 @@ class DynamicSinkUtil {
     return schema.identifierFieldNames();
   }
 
+  static Set<Integer> distributionFieldIds(Set<String> distributionFields, 
Schema schema) {
+    Set<Integer> distributionFieldIds = 
Sets.newHashSetWithExpectedSize(distributionFields.size());
+    for (String distributionField : distributionFields) {
+      Types.NestedField field = schema.findField(distributionField);
+      Preconditions.checkNotNull(
+          field,
+          "Missing distribution field '%s'. Expected distribution field '%s' 
to exist in the table schema.",
+          distributionField,
+          distributionField);
+      distributionFieldIds.add(field.fieldId());
+    }
+
+    return distributionFieldIds;
+  }
+
   static int safeAbs(int input) {
     if (input >= 0) {
       return input;
diff --git 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/HashKeyGenerator.java
 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/HashKeyGenerator.java
index c60dd9b0ce..9cfc66f1fd 100644
--- 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/HashKeyGenerator.java
+++ 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/HashKeyGenerator.java
@@ -20,6 +20,7 @@ package org.apache.iceberg.flink.sink.dynamic;
 
 import static org.apache.iceberg.TableProperties.WRITE_DISTRIBUTION_MODE;
 
+import java.util.Collections;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -82,6 +83,8 @@ class HashKeyGenerator {
         
DynamicSinkUtil.resolveEqualityFieldNames(dynamicRecord.equalityFields(), 
effectiveSchema);
     DistributionMode effectiveDistributionMode =
         MoreObjects.firstNonNull(dynamicRecord.distributionMode(), 
DistributionMode.NONE);
+    Set<String> effectiveDistributionFields =
+        MoreObjects.firstNonNull(dynamicRecord.distributionFields(), 
Collections.emptySet());
     SelectorKey cacheKey =
         new SelectorKey(
             tableIdent,
@@ -92,7 +95,8 @@ class HashKeyGenerator {
             dynamicRecord.spec(),
             effectiveEqualityFields,
             effectiveDistributionMode,
-            Math.min(dynamicRecord.writeParallelism(), maxWriteParallelism));
+            Math.min(dynamicRecord.writeParallelism(), maxWriteParallelism),
+            effectiveDistributionFields);
     KeySelector<RowData, Integer> keySelector =
         keySelectorCache.computeIfAbsent(
             cacheKey,
@@ -103,7 +107,8 @@ class HashKeyGenerator {
                     MoreObjects.firstNonNull(tableSpec, dynamicRecord.spec()),
                     effectiveDistributionMode,
                     effectiveEqualityFields,
-                    Math.min(dynamicRecord.writeParallelism(), 
maxWriteParallelism)));
+                    Math.min(dynamicRecord.writeParallelism(), 
maxWriteParallelism),
+                    effectiveDistributionFields));
     try {
       return keySelector.getKey(
           overrideRowData != null ? overrideRowData : dynamicRecord.rowData());
@@ -118,23 +123,51 @@ class HashKeyGenerator {
       PartitionSpec spec,
       DistributionMode mode,
       Set<String> equalityFields,
-      int writeParallelism) {
+      int writeParallelism,
+      Set<String> distributionFields) {
     LOG.debug(
         "Creating new KeySelector for table '{}' with distribution mode '{}'", 
tableName, mode);
+
     switch (mode) {
       case NONE:
+        if (!distributionFields.isEmpty()) {
+          LOG.warn(
+              "{}: Distribution fields {} are ignored when distribution mode 
is set to NONE",
+              tableName,
+              distributionFields);
+        }
+
         if (equalityFields.isEmpty()) {
           return tableKeySelector(tableName, writeParallelism, 
maxWriteParallelism);
         } else {
           LOG.info(
               "{}: Distribute rows by equality fields, because there are 
equality fields set",
               tableName);
-          return equalityFieldKeySelector(
-              tableName, schema, equalityFields, writeParallelism, 
maxWriteParallelism);
+          return fieldKeySelector(
+              tableName,
+              schema,
+              DynamicSinkUtil.getEqualityFieldIds(equalityFields, schema),
+              writeParallelism,
+              maxWriteParallelism);
         }
 
       case HASH:
-        if (equalityFields.isEmpty()) {
+        if (!distributionFields.isEmpty()) {
+          Preconditions.checkArgument(
+              equalityFields.isEmpty() || 
equalityFields.containsAll(distributionFields),
+              "%s: Invalid distribution fields '%s'. Expected distribution 
fields to be a subset of "
+                  + "equality fields '%s'.",
+              tableName,
+              distributionFields,
+              equalityFields);
+          LOG.info("{}: Distribute rows by distribution fields {}", tableName, 
distributionFields);
+          return fieldKeySelector(
+              tableName,
+              schema,
+              DynamicSinkUtil.distributionFieldIds(distributionFields, schema),
+              writeParallelism,
+              maxWriteParallelism);
+        } else if (equalityFields.isEmpty()) {
           if (spec.isUnpartitioned()) {
             LOG.warn(
                 "{}: Fallback to use 'none' distribution mode, because there 
are no equality fields set "
@@ -151,8 +184,12 @@ class HashKeyGenerator {
                 "{}: Distribute rows by equality fields, because there are 
equality fields set "
                     + "and table is unpartitioned",
                 tableName);
-            return equalityFieldKeySelector(
-                tableName, schema, equalityFields, writeParallelism, 
maxWriteParallelism);
+            return fieldKeySelector(
+                tableName,
+                schema,
+                DynamicSinkUtil.getEqualityFieldIds(equalityFields, schema),
+                writeParallelism,
+                maxWriteParallelism);
           } else {
             // Compare by fully-qualified name: a partition source that is a 
nested field has a
             // simple name (e.g. "name") that differs from the dotted 
equality-field name (e.g.
@@ -188,8 +225,12 @@ class HashKeyGenerator {
                   + "and {}='range' is not supported yet in flink",
               tableName,
               WRITE_DISTRIBUTION_MODE);
-          return equalityFieldKeySelector(
-              tableName, schema, equalityFields, writeParallelism, 
maxWriteParallelism);
+          return fieldKeySelector(
+              tableName,
+              schema,
+              DynamicSinkUtil.getEqualityFieldIds(equalityFields, schema),
+              writeParallelism,
+              maxWriteParallelism);
         }
 
       default:
@@ -198,17 +239,14 @@ class HashKeyGenerator {
     }
   }
 
-  private static KeySelector<RowData, Integer> equalityFieldKeySelector(
+  private static KeySelector<RowData, Integer> fieldKeySelector(
       String tableName,
       Schema schema,
-      Set<String> equalityFields,
+      Set<Integer> fieldIds,
       int writeParallelism,
       int maxWriteParallelism) {
     return new TargetLimitedKeySelector(
-        new EqualityFieldKeySelector(
-            schema,
-            FlinkSchemaUtil.convert(schema),
-            DynamicSinkUtil.getEqualityFieldIds(equalityFields, schema)),
+        new EqualityFieldKeySelector(schema, FlinkSchemaUtil.convert(schema), 
fieldIds),
         tableName,
         writeParallelism,
         maxWriteParallelism);
@@ -331,6 +369,7 @@ class HashKeyGenerator {
     private final Set<String> equalityFields;
     private final DistributionMode distributionMode;
     private final int writeParallelism;
+    private final Set<String> distributionFields;
 
     SelectorKey(
         String tableName,
@@ -341,7 +380,8 @@ class HashKeyGenerator {
         PartitionSpec spec,
         Set<String> equalityFields,
         DistributionMode distributionMode,
-        int writeParallelism) {
+        int writeParallelism,
+        Set<String> distributionFields) {
       this.tableName = tableName;
       this.branch = branch;
       this.schemaId = tableSchemaId;
@@ -351,9 +391,11 @@ class HashKeyGenerator {
       this.equalityFields = equalityFields;
       this.distributionMode = distributionMode;
       this.writeParallelism = writeParallelism;
+      this.distributionFields = distributionFields;
     }
 
     @Override
+    @SuppressWarnings("checkstyle:CyclomaticComplexity")
     public boolean equals(Object other) {
       if (this == other) {
         return true;
@@ -372,7 +414,8 @@ class HashKeyGenerator {
           && Objects.equals(spec, that.spec)
           && Objects.equals(equalityFields, that.equalityFields)
           && distributionMode == that.distributionMode
-          && writeParallelism == that.writeParallelism;
+          && writeParallelism == that.writeParallelism
+          && Objects.equals(distributionFields, that.distributionFields);
     }
 
     @Override
@@ -386,7 +429,8 @@ class HashKeyGenerator {
           spec,
           equalityFields,
           distributionMode,
-          writeParallelism);
+          writeParallelism,
+          distributionFields);
     }
 
     @Override
@@ -401,6 +445,7 @@ class HashKeyGenerator {
           .add("equalityFields", equalityFields)
           .add("distributionMode", distributionMode)
           .add("writeParallelism", writeParallelism)
+          .add("distributionFields", distributionFields)
           .toString();
     }
   }
diff --git 
a/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicIcebergSink.java
 
b/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicIcebergSink.java
index 69730d9adb..1e7b1945ff 100644
--- 
a/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicIcebergSink.java
+++ 
b/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicIcebergSink.java
@@ -136,6 +136,7 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
     PartitionSpec partitionSpec;
     boolean upsertMode;
     Set<String> equalityFields;
+    Set<String> distributionFields;
     int writeParallelism;
 
     private DynamicIcebergDataImpl(
@@ -149,6 +150,7 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
           false,
           Collections.emptySet(),
           false,
+          Collections.emptySet(),
           10);
     }
 
@@ -167,6 +169,7 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
           false,
           Collections.emptySet(),
           false,
+          Collections.emptySet(),
           10);
     }
 
@@ -187,6 +190,26 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
           upsertMode,
           equalityFields,
           isDuplicate,
+          Collections.emptySet(),
+          10);
+    }
+
+    private DynamicIcebergDataImpl(
+        Schema schemaProvided,
+        String tableName,
+        String branch,
+        PartitionSpec partitionSpec,
+        Set<String> distributionFields) {
+      this(
+          schemaProvided,
+          schemaProvided,
+          tableName,
+          branch,
+          partitionSpec,
+          false,
+          Collections.emptySet(),
+          false,
+          distributionFields,
           10);
     }
 
@@ -199,6 +222,7 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
         boolean upsertMode,
         Set<String> equalityFields,
         boolean isDuplicate,
+        Set<String> distributionFields,
         int writeParallelism) {
       this.rowProvided = randomRow(schemaProvided, isDuplicate ? seed : 
++seed);
       this.rowExpected = isDuplicate ? null : rowProvided;
@@ -209,6 +233,7 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
       this.partitionSpec = partitionSpec;
       this.upsertMode = upsertMode;
       this.equalityFields = equalityFields;
+      this.distributionFields = distributionFields;
       this.writeParallelism = writeParallelism;
     }
   }
@@ -221,6 +246,11 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
       String branch = row.branch;
       Schema schema = row.schemaProvided;
       PartitionSpec spec = row.partitionSpec;
+      // Distribution fields are only read in HASH mode
+      DistributionMode distributionMode =
+          spec.isPartitioned() || !row.distributionFields.isEmpty()
+              ? DistributionMode.HASH
+              : DistributionMode.NONE;
       DynamicRecord dynamicRecord =
           new DynamicRecord(
               tableIdentifier,
@@ -228,10 +258,11 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
               schema,
               converter(schema).toInternal(row.rowProvided),
               spec,
-              spec.isPartitioned() ? DistributionMode.HASH : 
DistributionMode.NONE,
+              distributionMode,
               row.writeParallelism);
       dynamicRecord.setUpsertMode(row.upsertMode);
       dynamicRecord.setEqualityFields(row.equalityFields);
+      dynamicRecord.setDistributionFields(row.distributionFields);
       out.collect(dynamicRecord);
     }
   }
@@ -1366,6 +1397,43 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
     }
   }
 
+  @Test
+  void testDistributionFieldsWriteSameKeyToSingleDataFile() throws Exception {
+    int parallelism = 2;
+    List<DynamicIcebergDataImpl> rows = Lists.newArrayList();
+    for (int i = 0; i < 50; i++) {
+      DynamicIcebergDataImpl row =
+          new DynamicIcebergDataImpl(
+              SimpleDataUtil.SCHEMA,
+              "t1",
+              SnapshotRef.MAIN_BRANCH,
+              PartitionSpec.unpartitioned(),
+              Collections.singleton("id"));
+
+      row.rowProvided = Row.of(1, "data-" + i);
+      row.rowExpected = row.rowProvided;
+      rows.add(row);
+    }
+
+    executeDynamicSink(rows, this.env, true, parallelism, null);
+    verifyResults(rows);
+
+    Table table =
+        CATALOG_EXTENSION
+            .catalogLoader()
+            .loadCatalog()
+            .loadTable(TableIdentifier.of(DATABASE, "t1"));
+
+    List<Snapshot> dataSnapshots =
+        Lists.newArrayList(table.snapshots()).stream()
+            .filter(snapshot -> 
snapshot.addedDataFiles(table.io()).iterator().hasNext())
+            .collect(Collectors.toList());
+
+    assertThat(dataSnapshots)
+        .isNotEmpty()
+        .allSatisfy(snapshot -> 
assertThat(snapshot.addedDataFiles(table.io())).hasSize(1));
+  }
+
   @Test
   void testMultiFormatVersion() throws Exception {
     ImmutableMap<String, String> properties = 
ImmutableMap.of(TableProperties.FORMAT_VERSION, "3");
@@ -1942,6 +2010,7 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
                 false,
                 Collections.emptySet(),
                 false,
+                Collections.emptySet(),
                 -1),
             new DynamicIcebergDataImpl(
                 SimpleDataUtil.SCHEMA,
@@ -1952,6 +2021,7 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
                 false,
                 Collections.emptySet(),
                 false,
+                Collections.emptySet(),
                 0));
 
     runTest(
diff --git 
a/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestHashKeyGenerator.java
 
b/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestHashKeyGenerator.java
index 837de70d3d..de6fbddb82 100644
--- 
a/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestHashKeyGenerator.java
+++ 
b/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestHashKeyGenerator.java
@@ -38,6 +38,7 @@ import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.flink.FlinkWriteConf;
 import org.apache.iceberg.flink.FlinkWriteOptions;
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableSet;
 import org.apache.iceberg.relocated.com.google.common.collect.Lists;
 import org.apache.iceberg.relocated.com.google.common.collect.Sets;
 import org.apache.iceberg.types.Types;
@@ -800,6 +801,224 @@ class TestHashKeyGenerator {
     assertThat(keySelectorCache).hasSize(2);
   }
 
+  @Test
+  void testDistributionFieldsWithDistributionModeHash() throws Exception {
+    int writeParallelism = 2;
+    int maxWriteParallelism = 8;
+    HashKeyGenerator generator = new HashKeyGenerator(16, maxWriteParallelism);
+    PartitionSpec unpartitioned = PartitionSpec.unpartitioned();
+    Set<String> distributionFields = Collections.singleton("id");
+
+    GenericRowData row1 = GenericRowData.of(1, StringData.fromString("foo"));
+    GenericRowData row2 = GenericRowData.of(1, StringData.fromString("bar"));
+    GenericRowData row3 = GenericRowData.of(2, StringData.fromString("baz"));
+
+    int writeKey1 =
+        getWriteKey(
+            generator,
+            unpartitioned,
+            DistributionMode.HASH,
+            writeParallelism,
+            Collections.emptySet(),
+            distributionFields,
+            row1);
+    int writeKey2 =
+        getWriteKey(
+            generator,
+            unpartitioned,
+            DistributionMode.HASH,
+            writeParallelism,
+            Collections.emptySet(),
+            distributionFields,
+            row2);
+    int writeKey3 =
+        getWriteKey(
+            generator,
+            unpartitioned,
+            DistributionMode.HASH,
+            writeParallelism,
+            Collections.emptySet(),
+            distributionFields,
+            row3);
+
+    assertThat(writeKey1).isEqualTo(writeKey2);
+    assertThat(writeKey2).isNotEqualTo(writeKey3);
+
+    assertThat(getSubTaskId(writeKey1, writeParallelism, 
maxWriteParallelism)).isEqualTo(1);
+    assertThat(getSubTaskId(writeKey2, writeParallelism, 
maxWriteParallelism)).isEqualTo(1);
+    assertThat(getSubTaskId(writeKey3, writeParallelism, 
maxWriteParallelism)).isEqualTo(0);
+  }
+
+  @Test
+  void testDistributionFieldsIgnorePartitionSpec() throws Exception {
+    int writeParallelism = 4;
+    int maxWriteParallelism = 8;
+    HashKeyGenerator generator = new HashKeyGenerator(16, maxWriteParallelism);
+    PartitionSpec partitioned = 
PartitionSpec.builderFor(SCHEMA).identity("id").build();
+    Set<String> distributionFields = Collections.singleton("data");
+
+    // Same 'data' but different partition values must still land on the same 
writer
+    GenericRowData row1 = GenericRowData.of(1, StringData.fromString("x"));
+    GenericRowData row2 = GenericRowData.of(2, StringData.fromString("x"));
+
+    int writeKey1 =
+        getWriteKey(
+            generator,
+            partitioned,
+            DistributionMode.HASH,
+            writeParallelism,
+            Collections.emptySet(),
+            distributionFields,
+            row1);
+    int writeKey2 =
+        getWriteKey(
+            generator,
+            partitioned,
+            DistributionMode.HASH,
+            writeParallelism,
+            Collections.emptySet(),
+            distributionFields,
+            row2);
+    assertThat(writeKey1).isEqualTo(writeKey2);
+  }
+
+  @Test
+  void testDistributionFieldsMissingFromSchema() {
+    HashKeyGenerator generator = new HashKeyGenerator(16, 8);
+
+    assertThatThrownBy(
+            () ->
+                getWriteKey(
+                    generator,
+                    PartitionSpec.unpartitioned(),
+                    DistributionMode.HASH,
+                    2,
+                    Collections.emptySet(),
+                    Collections.singleton("does_not_exist"),
+                    GenericRowData.of(1, StringData.fromString("foo"))))
+        .isInstanceOf(NullPointerException.class)
+        .hasMessageContaining(
+            "Missing distribution field 'does_not_exist'. "
+                + "Expected distribution field 'does_not_exist' to exist in 
the table schema.");
+  }
+
+  @Test
+  void testDistributionFieldsSubsetOfEqualityFields() throws Exception {
+    HashKeyGenerator generator = new HashKeyGenerator(16, 8);
+    PartitionSpec unpartitioned = PartitionSpec.unpartitioned();
+    Set<String> equalityFields = ImmutableSet.of("id", "data");
+    Set<String> distributionFields = Collections.singleton("id");
+
+    int writeKey1 =
+        getWriteKey(
+            generator,
+            unpartitioned,
+            DistributionMode.HASH,
+            2,
+            equalityFields,
+            distributionFields,
+            GenericRowData.of(1, StringData.fromString("a")));
+    int writeKey2 =
+        getWriteKey(
+            generator,
+            unpartitioned,
+            DistributionMode.HASH,
+            2,
+            equalityFields,
+            distributionFields,
+            GenericRowData.of(1, StringData.fromString("b")));
+    int writeKey3 =
+        getWriteKey(
+            generator,
+            unpartitioned,
+            DistributionMode.HASH,
+            2,
+            equalityFields,
+            distributionFields,
+            GenericRowData.of(2, StringData.fromString("a")));
+
+    assertThat(writeKey1).isEqualTo(writeKey2);
+    assertThat(writeKey1).isNotEqualTo(writeKey3);
+  }
+
+  @Test
+  void testDistributionFieldsNotSubsetOfEqualityFields() {
+    HashKeyGenerator generator = new HashKeyGenerator(16, 8);
+
+    assertThatThrownBy(
+            () ->
+                getWriteKey(
+                    generator,
+                    PartitionSpec.unpartitioned(),
+                    DistributionMode.HASH,
+                    2,
+                    Collections.singleton("id"),
+                    Collections.singleton("data"),
+                    GenericRowData.of(1, StringData.fromString("foo"))))
+        .isInstanceOf(IllegalArgumentException.class)
+        .hasMessageContaining(
+            "Invalid distribution fields '[data]'. Expected distribution 
fields "
+                + "to be a subset of equality fields '[id]'.");
+  }
+
+  @Test
+  void testDistributionFieldsNotSubsetOfIdentifierFields() {
+    HashKeyGenerator generator = new HashKeyGenerator(16, 8);
+    Schema schemaWithIdentifier =
+        new Schema(
+            Lists.newArrayList(
+                Types.NestedField.required(1, "id", Types.IntegerType.get()),
+                Types.NestedField.required(2, "data", Types.StringType.get())),
+            Sets.newHashSet(1));
+
+    DynamicRecord record =
+        new DynamicRecord(
+            TABLE_IDENTIFIER,
+            BRANCH,
+            schemaWithIdentifier,
+            GenericRowData.of(1, StringData.fromString("foo")),
+            PartitionSpec.unpartitioned(),
+            DistributionMode.HASH,
+            2);
+    record.setDistributionFields(Collections.singleton("data"));
+
+    assertThatThrownBy(() -> generator.generateKey(record))
+        .isInstanceOf(IllegalArgumentException.class)
+        .hasMessageContaining(
+            "Invalid distribution fields '[data]'. Expected distribution 
fields "
+                + "to be a subset of equality fields '[id]'.");
+  }
+
+  @Test
+  void testCacheMissOnDistributionFieldsChange() throws Exception {
+    HashKeyGenerator generator = new HashKeyGenerator(10, 8);
+    Map<HashKeyGenerator.SelectorKey, KeySelector<RowData, Integer>> 
keySelectorCache =
+        generator.getKeySelectorCache();
+
+    PartitionSpec unpartitioned = PartitionSpec.unpartitioned();
+    GenericRowData row = GenericRowData.of(1, StringData.fromString("foo"));
+
+    getWriteKey(
+        generator,
+        unpartitioned,
+        DistributionMode.HASH,
+        2,
+        Collections.emptySet(),
+        Collections.singleton("id"),
+        row);
+    assertThat(keySelectorCache).hasSize(1);
+
+    getWriteKey(
+        generator,
+        unpartitioned,
+        DistributionMode.HASH,
+        2,
+        Collections.emptySet(),
+        Collections.singleton("data"),
+        row);
+    assertThat(keySelectorCache).hasSize(2);
+  }
+
   private static int getWriteKey(
       HashKeyGenerator generator,
       Schema schema,
@@ -843,12 +1062,54 @@ class TestHashKeyGenerator {
       DistributionMode mode,
       int writeParallelism,
       Set<String> equalityFields,
+      Set<String> distributionFields,
+      GenericRowData row)
+      throws Exception {
+    return getWriteKey(
+        generator,
+        spec,
+        mode,
+        writeParallelism,
+        equalityFields,
+        distributionFields,
+        row,
+        new FlinkWriteConf(Collections.emptyMap(), new Configuration()));
+  }
+
+  private static int getWriteKey(
+      HashKeyGenerator generator,
+      PartitionSpec spec,
+      DistributionMode mode,
+      int writeParallelism,
+      Set<String> equalityFields,
+      GenericRowData row,
+      FlinkWriteConf flinkWriteConf)
+      throws Exception {
+    return getWriteKey(
+        generator,
+        spec,
+        mode,
+        writeParallelism,
+        equalityFields,
+        Collections.emptySet(),
+        row,
+        flinkWriteConf);
+  }
+
+  private static int getWriteKey(
+      HashKeyGenerator generator,
+      PartitionSpec spec,
+      DistributionMode mode,
+      int writeParallelism,
+      Set<String> equalityFields,
+      Set<String> distributionFields,
       GenericRowData row,
       FlinkWriteConf flinkWriteConf)
       throws Exception {
     DynamicRecord inputRecord =
         new DynamicRecord(TABLE_IDENTIFIER, BRANCH, SCHEMA, row, spec, mode, 
writeParallelism);
     inputRecord.setEqualityFields(equalityFields);
+    inputRecord.setDistributionFields(distributionFields);
 
     DynamicRecordWithConfig dynamicRecordWithConfig = new 
DynamicRecordWithConfig(flinkWriteConf);
     return generator.generateKey(dynamicRecordWithConfig.wrap(inputRecord));

Reply via email to