mxm commented on code in PR #18668:
URL: https://github.com/apache/iceberg/pull/18668#discussion_r4426212007


##########
flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestHashKeyGenerator.java:
##########
@@ -754,6 +754,172 @@ void testCacheMissOnDistributionModeChange() throws 
Exception {
     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 =
+        distributionFieldsWriteKey(
+            generator, unpartitioned, writeParallelism, distributionFields, 
row1);
+    int writeKey2 =
+        distributionFieldsWriteKey(
+            generator, unpartitioned, writeParallelism, distributionFields, 
row2);
+    int writeKey3 =
+        distributionFieldsWriteKey(
+            generator, unpartitioned, writeParallelism, 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"));
+
+    assertThat(
+            distributionFieldsWriteKey(
+                generator, partitioned, writeParallelism, distributionFields, 
row1))
+        .isEqualTo(
+            distributionFieldsWriteKey(
+                generator, partitioned, writeParallelism, distributionFields, 
row2));
+
+    // Distinct 'data' values must not all collapse onto a single writer
+    Set<Integer> writeKeys = Sets.newHashSet();
+    for (int i = 0; i < 8; i++) {
+      writeKeys.add(
+          distributionFieldsWriteKey(
+              generator,
+              partitioned,
+              writeParallelism,
+              distributionFields,
+              GenericRowData.of(i, StringData.fromString("value-" + i))));
+    }
+
+    assertThat(writeKeys).hasSizeGreaterThan(1);
+  }
+
+  @Test
+  void testDistributionFieldsMissingFromSchema() {
+    HashKeyGenerator generator = new HashKeyGenerator(16, 8);
+    DynamicRecord record =
+        distributionFieldsRecord(
+            PartitionSpec.unpartitioned(),
+            DistributionMode.HASH,
+            Collections.singleton("does_not_exist"));
+
+    assertThatThrownBy(() -> generator.generateKey(record))
+        .isInstanceOf(NullPointerException.class)
+        .hasMessageContaining(
+            "Missing distribution field 'does_not_exist'. "
+                + "Expected distribution field 'does_not_exist' to exist in 
the table schema.");
+  }
+
+  @Test
+  void testDistributionFieldsNotSubsetOfEqualityFields() {

Review Comment:
   Could we also test the accepted case, e.g. equality fields `{id, data}` and 
distribution field `{id}`? Rows `(1, "a")` and `(1, "b")` should then get the 
same writer key.



##########
flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/HashKeyGenerator.java:
##########
@@ -117,23 +122,52 @@ private KeySelector<RowData, Integer> getKeySelector(
       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 '%s' to be a subset of "
+                  + "equality fields '%s'.",

Review Comment:
   NIT: This prints the distribution fields twice. Could we shorten it, e.g. 
`"%s: Distribution fields %s must be a subset of equality fields %s"`?



##########
flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicIcebergSink.java:
##########
@@ -1184,6 +1215,43 @@ void testUpsertV3(boolean immediateUpdate) throws 
Exception {
     }
   }
 
+  @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();
+    for (Snapshot snapshot : dataSnapshots) {
+      assertThat(snapshot.addedDataFiles(table.io())).hasSize(1);
+    }

Review Comment:
   NIT: I'd prefer not to use loops for assertions. Makes test failures easier 
to reason about.
   
   ```suggestion
       assertThat(dataSnapshots)
           .isNotEmpty()
           .allSatisfy(snapshot -> 
assertThat(snapshot.addedDataFiles(table.io())).hasSize(1));
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to