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 6890694bd3 Flink: Backport: Remap equality field ids on table update 
(v1.20, v2.2) (#18542)
6890694bd3 is described below

commit 6890694bd326648c3ab5930b8c97986118bbc1e5
Author: Sergei Nikolaev <[email protected]>
AuthorDate: Mon Oct 19 17:59:51 2026 +0200

    Flink: Backport: Remap equality field ids on table update (v1.20, v2.2) 
(#18542)
---
 .../sink/dynamic/DynamicTableUpdateOperator.java   |  26 +++-
 .../flink/sink/dynamic/TestDynamicIcebergSink.java | 133 ++++++++++++++-------
 .../dynamic/TestDynamicTableUpdateOperator.java    |  38 ++++++
 .../sink/dynamic/DynamicTableUpdateOperator.java   |  26 +++-
 .../flink/sink/dynamic/TestDynamicIcebergSink.java | 133 ++++++++++++++-------
 .../dynamic/TestDynamicTableUpdateOperator.java    |  38 ++++++
 6 files changed, 312 insertions(+), 82 deletions(-)

diff --git 
a/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicTableUpdateOperator.java
 
b/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicTableUpdateOperator.java
index 93c268ff86..b6795bf79b 100644
--- 
a/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicTableUpdateOperator.java
+++ 
b/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicTableUpdateOperator.java
@@ -18,15 +18,20 @@
  */
 package org.apache.iceberg.flink.sink.dynamic;
 
+import java.util.Set;
 import org.apache.flink.annotation.Internal;
 import org.apache.flink.api.common.functions.OpenContext;
 import org.apache.flink.api.common.functions.RichMapFunction;
 import org.apache.flink.api.java.tuple.Tuple2;
 import org.apache.flink.table.data.RowData;
 import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
 import org.apache.iceberg.catalog.Catalog;
 import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.flink.CatalogLoader;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.Types;
 
 /**
  * An optional operator to perform table updates for tables (e.g. schema 
update) in a non-concurrent
@@ -87,8 +92,10 @@ class DynamicTableUpdateOperator
             data.spec(),
             tableCreator);
     TableMetadataCache.ResolvedSchemaInfo compareInfo = newData.f0;
+    Schema tableSchema = compareInfo.resolvedTableSchema();
 
-    data.setSchema(compareInfo.resolvedTableSchema());
+    data.setEqualityFieldIds(remapFieldIds(data.equalityFields(), 
data.schema(), tableSchema));
+    data.setSchema(tableSchema);
     data.setSpec(newData.f1);
 
     RowData newRowData = (RowData) 
newData.f0.recordConverter().convert(data.rowData());
@@ -96,4 +103,21 @@ class DynamicTableUpdateOperator
 
     return data;
   }
+
+  private Set<Integer> remapFieldIds(Set<Integer> fieldIds, Schema fromSchema, 
Schema toSchema) {
+    if (fieldIds == null || fieldIds.isEmpty()) {
+      return fieldIds;
+    }
+
+    Set<Integer> remappedIds = 
Sets.newHashSetWithExpectedSize(fieldIds.size());
+    for (Integer fieldId : fieldIds) {
+      String name = fromSchema.findColumnName(fieldId);
+      Types.NestedField field =
+          caseSensitive ? toSchema.findField(name) : 
toSchema.caseInsensitiveFindField(name);
+      Preconditions.checkNotNull(field, "Equality field %s does not exist in 
schema", name);
+      remappedIds.add(field.fieldId());
+    }
+
+    return remappedIds;
+  }
 }
diff --git 
a/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicIcebergSink.java
 
b/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicIcebergSink.java
index bb896d734a..873ef2475f 100644
--- 
a/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicIcebergSink.java
+++ 
b/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicIcebergSink.java
@@ -76,6 +76,7 @@ import org.apache.iceberg.Snapshot;
 import org.apache.iceberg.SnapshotRef;
 import org.apache.iceberg.Table;
 import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TableUtil;
 import org.apache.iceberg.catalog.Catalog;
 import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.data.IcebergGenerics;
@@ -842,51 +843,64 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
     runTest(rows);
   }
 
-  @Test
-  void testUpsert() throws Exception {
+  @ParameterizedTest
+  @ValueSource(booleans = {false, true})
+  void testUpsert(boolean immediateUpdate) throws Exception {
+    // A restart replays the records against the already created table, which 
hides failures
+    Configuration configuration = new Configuration();
+    configuration.set(RestartStrategyOptions.RESTART_STRATEGY, "none");
+    env.configure(configuration);
+
+    // Field IDs differ from the ones the catalog assigns on table creation
+    Schema schema =
+        new Schema(
+            Types.NestedField.optional(0, "id", Types.IntegerType.get()),
+            Types.NestedField.optional(1, "data", Types.StringType.get()));
+    Types.NestedField equalityField = schema.findField("id");
     List<DynamicIcebergDataImpl> rows =
         Lists.newArrayList(
             // Insert one rows
             new DynamicIcebergDataImpl(
-                SimpleDataUtil.SCHEMA,
+                schema,
                 "t1",
                 SnapshotRef.MAIN_BRANCH,
                 PartitionSpec.unpartitioned(),
                 true,
-                Sets.newHashSet("id"),
+                Sets.newHashSet(equalityField.name()),
                 false),
             // Remaining rows are duplicates
             new DynamicIcebergDataImpl(
-                SimpleDataUtil.SCHEMA,
+                schema,
                 "t1",
                 SnapshotRef.MAIN_BRANCH,
                 PartitionSpec.unpartitioned(),
                 true,
-                Sets.newHashSet("id"),
+                Sets.newHashSet(equalityField.name()),
                 true),
             new DynamicIcebergDataImpl(
-                SimpleDataUtil.SCHEMA,
+                schema,
                 "t1",
                 SnapshotRef.MAIN_BRANCH,
                 PartitionSpec.unpartitioned(),
                 true,
-                Sets.newHashSet("id"),
+                Sets.newHashSet(equalityField.name()),
                 true),
             new DynamicIcebergDataImpl(
-                SimpleDataUtil.SCHEMA,
+                schema,
                 "t1",
                 SnapshotRef.MAIN_BRANCH,
                 PartitionSpec.unpartitioned(),
                 true,
-                Sets.newHashSet("id"),
+                Sets.newHashSet(equalityField.name()),
                 true));
 
-    executeDynamicSink(rows, env, true, 1, null);
+    executeDynamicSink(rows, env, immediateUpdate, 1, null);
 
-    try (CloseableIterable<Record> iterable =
-        IcebergGenerics.read(
-                
CATALOG_EXTENSION.catalog().loadTable(TableIdentifier.of("default", "t1")))
-            .build()) {
+    Table table = 
CATALOG_EXTENSION.catalog().loadTable(TableIdentifier.of("default", "t1"));
+    // The record's equality field ID doesn't exist in the created table, so 
with immediateUpdate
+    // disabled the write fails unless equality field IDs are remapped to the 
table schema.
+    
assertThat(table.schema().findColumnName(equalityField.fieldId())).isNull();
+    try (CloseableIterable<Record> iterable = 
IcebergGenerics.read(table).build()) {
       List<Record> records = Lists.newArrayList();
       for (Record record : iterable) {
         records.add(record);
@@ -901,61 +915,70 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
     }
   }
 
-  @Test
-  void testUpsertV3() throws Exception {
-    ImmutableMap<String, String> properties = 
ImmutableMap.of(TableProperties.FORMAT_VERSION, "3");
-    CATALOG_EXTENSION
-        .catalog()
-        .createTable(
-            TableIdentifier.of(DATABASE, "t1"),
-            SimpleDataUtil.SCHEMA,
-            PartitionSpec.unpartitioned(),
-            null,
-            properties);
+  @ParameterizedTest
+  @ValueSource(booleans = {false, true})
+  void testUpsertV3(boolean immediateUpdate) throws Exception {
+    // A restart replays the records against the already created table, which 
hides failures
+    Configuration configuration = new Configuration();
+    configuration.set(RestartStrategyOptions.RESTART_STRATEGY, "none");
+    env.configure(configuration);
+
+    Map<String, String> tableProps = 
ImmutableMap.of(TableProperties.FORMAT_VERSION, "3");
+    TableCreator tableCreator =
+        (catalog, identifier, tableSchema, spec) ->
+            catalog.createTable(identifier, tableSchema, spec, null, 
tableProps);
 
+    // Field IDs differ from the ones the catalog assigns on table creation
+    Schema schema =
+        new Schema(
+            Types.NestedField.optional(0, "id", Types.IntegerType.get()),
+            Types.NestedField.optional(1, "data", Types.StringType.get()));
+    Types.NestedField equalityField = schema.findField("id");
     List<DynamicIcebergDataImpl> rows =
         Lists.newArrayList(
             // Insert one rows
             new DynamicIcebergDataImpl(
-                SimpleDataUtil.SCHEMA,
+                schema,
                 "t1",
                 SnapshotRef.MAIN_BRANCH,
                 PartitionSpec.unpartitioned(),
                 true,
-                Sets.newHashSet("id"),
+                Sets.newHashSet(equalityField.name()),
                 false),
             // Remaining rows are duplicates
             new DynamicIcebergDataImpl(
-                SimpleDataUtil.SCHEMA,
+                schema,
                 "t1",
                 SnapshotRef.MAIN_BRANCH,
                 PartitionSpec.unpartitioned(),
                 true,
-                Sets.newHashSet("id"),
+                Sets.newHashSet(equalityField.name()),
                 true),
             new DynamicIcebergDataImpl(
-                SimpleDataUtil.SCHEMA,
+                schema,
                 "t1",
                 SnapshotRef.MAIN_BRANCH,
                 PartitionSpec.unpartitioned(),
                 true,
-                Sets.newHashSet("id"),
+                Sets.newHashSet(equalityField.name()),
                 true),
             new DynamicIcebergDataImpl(
-                SimpleDataUtil.SCHEMA,
+                schema,
                 "t1",
                 SnapshotRef.MAIN_BRANCH,
                 PartitionSpec.unpartitioned(),
                 true,
-                Sets.newHashSet("id"),
+                Sets.newHashSet(equalityField.name()),
                 true));
 
-    executeDynamicSink(rows, env, true, 1, null);
+    executeDynamicSink(rows, env, immediateUpdate, 1, null, false, 
Maps.newHashMap(), tableCreator);
 
-    try (CloseableIterable<Record> iterable =
-        IcebergGenerics.read(
-                
CATALOG_EXTENSION.catalog().loadTable(TableIdentifier.of("default", "t1")))
-            .build()) {
+    Table table = 
CATALOG_EXTENSION.catalog().loadTable(TableIdentifier.of("default", "t1"));
+    assertThat(TableUtil.formatVersion(table)).isEqualTo(3);
+    // The record's equality field ID doesn't exist in the created table, so 
with immediateUpdate
+    // disabled the write fails unless equality field IDs are remapped to the 
table schema.
+    
assertThat(table.schema().findColumnName(equalityField.fieldId())).isNull();
+    try (CloseableIterable<Record> iterable = 
IcebergGenerics.read(table).build()) {
       List<Record> records = Lists.newArrayList();
       for (Record record : iterable) {
         records.add(record);
@@ -1740,7 +1763,14 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
       Map<String, String> writeProperties)
       throws Exception {
     executeDynamicSink(
-        dynamicData, env, immediateUpdate, parallelism, null, false, 
writeProperties);
+        dynamicData,
+        env,
+        immediateUpdate,
+        parallelism,
+        null,
+        false,
+        writeProperties,
+        TableCreator.DEFAULT);
     verifyResults(dynamicData, writeProperties);
   }
 
@@ -1776,6 +1806,27 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
       boolean overwrite,
       Map<String, String> writeProperties)
       throws Exception {
+    executeDynamicSink(
+        dynamicData,
+        env,
+        immediateUpdate,
+        parallelism,
+        commitHook,
+        overwrite,
+        writeProperties,
+        TableCreator.DEFAULT);
+  }
+
+  private void executeDynamicSink(
+      List<DynamicIcebergDataImpl> dynamicData,
+      StreamExecutionEnvironment env,
+      boolean immediateUpdate,
+      int parallelism,
+      @Nullable CommitHook commitHook,
+      boolean overwrite,
+      Map<String, String> writeProperties,
+      TableCreator tableCreator)
+      throws Exception {
     DataStream<DynamicIcebergDataImpl> dataStream =
         env.fromData(dynamicData, TypeInformation.of(new TypeHint<>() {}));
     env.setParallelism(parallelism);
@@ -1787,6 +1838,7 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
           .catalogLoader(CATALOG_EXTENSION.catalogLoader())
           .writeParallelism(parallelism)
           .immediateTableUpdate(immediateUpdate)
+          .tableCreator(tableCreator)
           .setSnapshotProperty("commit.retry.num-retries", "0")
           .overwrite(overwrite)
           .setAll(writeProperties)
@@ -1797,6 +1849,7 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
           .catalogLoader(CATALOG_EXTENSION.catalogLoader())
           .writeParallelism(parallelism)
           .immediateTableUpdate(immediateUpdate)
+          .tableCreator(tableCreator)
           .overwrite(overwrite)
           .setAll(writeProperties)
           .append();
diff --git 
a/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicTableUpdateOperator.java
 
b/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicTableUpdateOperator.java
index fdc1295126..4ee75ca54c 100644
--- 
a/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicTableUpdateOperator.java
+++ 
b/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicTableUpdateOperator.java
@@ -26,6 +26,7 @@ import java.util.Collections;
 import org.apache.flink.api.common.functions.OpenContext;
 import org.apache.flink.configuration.Configuration;
 import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.StringData;
 import org.apache.iceberg.PartitionSpec;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.catalog.Catalog;
@@ -121,6 +122,43 @@ class TestDynamicTableUpdateOperator {
     
assertThat(catalog.loadTable(table).schema().schemaId()).isEqualTo(output.schema().schemaId());
   }
 
+  @Test
+  void testEqualityFieldIdsRemappedToTableSchema() throws Exception {
+    Catalog catalog = CATALOG_EXTENSION.catalog();
+    TableIdentifier table = TableIdentifier.of(TABLE);
+
+    DynamicTableUpdateOperator operator =
+        new DynamicTableUpdateOperator(
+            CATALOG_EXTENSION.catalogLoader(),
+            TableCreator.DEFAULT,
+            flinkDynamicSinkConfiguration(CASE_SENSITIVE, PRESERVE_COLUMNS));
+    operator.open((OpenContext) null);
+
+    catalog.createTable(table, SCHEMA1);
+    Schema inputSchema =
+        new Schema(
+            Types.NestedField.required(2, "id", Types.IntegerType.get()),
+            Types.NestedField.optional(1, "data", Types.StringType.get()));
+    Types.NestedField equalityField = inputSchema.findField("id");
+    DynamicRecordInternal input =
+        new DynamicRecordInternal(
+            TABLE,
+            "branch",
+            inputSchema,
+            GenericRowData.of(1, StringData.fromString("test")),
+            PartitionSpec.unpartitioned(),
+            42,
+            true,
+            Collections.singleton(equalityField.fieldId()));
+    DynamicRecordInternal output = operator.map(input);
+
+    assertThat(output.schema().findColumnName(equalityField.fieldId()))
+        .isNotEqualTo(equalityField.name());
+    assertThat(output.equalityFields())
+        .map(output.schema()::findColumnName)
+        .containsExactly(equalityField.name());
+  }
+
   @ParameterizedTest
   @ValueSource(booleans = {true, false})
   void testCaseInSensitivity(boolean caseSensitive) throws Exception {
diff --git 
a/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicTableUpdateOperator.java
 
b/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicTableUpdateOperator.java
index 93c268ff86..b6795bf79b 100644
--- 
a/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicTableUpdateOperator.java
+++ 
b/flink/v2.2/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicTableUpdateOperator.java
@@ -18,15 +18,20 @@
  */
 package org.apache.iceberg.flink.sink.dynamic;
 
+import java.util.Set;
 import org.apache.flink.annotation.Internal;
 import org.apache.flink.api.common.functions.OpenContext;
 import org.apache.flink.api.common.functions.RichMapFunction;
 import org.apache.flink.api.java.tuple.Tuple2;
 import org.apache.flink.table.data.RowData;
 import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
 import org.apache.iceberg.catalog.Catalog;
 import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.flink.CatalogLoader;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
+import org.apache.iceberg.types.Types;
 
 /**
  * An optional operator to perform table updates for tables (e.g. schema 
update) in a non-concurrent
@@ -87,8 +92,10 @@ class DynamicTableUpdateOperator
             data.spec(),
             tableCreator);
     TableMetadataCache.ResolvedSchemaInfo compareInfo = newData.f0;
+    Schema tableSchema = compareInfo.resolvedTableSchema();
 
-    data.setSchema(compareInfo.resolvedTableSchema());
+    data.setEqualityFieldIds(remapFieldIds(data.equalityFields(), 
data.schema(), tableSchema));
+    data.setSchema(tableSchema);
     data.setSpec(newData.f1);
 
     RowData newRowData = (RowData) 
newData.f0.recordConverter().convert(data.rowData());
@@ -96,4 +103,21 @@ class DynamicTableUpdateOperator
 
     return data;
   }
+
+  private Set<Integer> remapFieldIds(Set<Integer> fieldIds, Schema fromSchema, 
Schema toSchema) {
+    if (fieldIds == null || fieldIds.isEmpty()) {
+      return fieldIds;
+    }
+
+    Set<Integer> remappedIds = 
Sets.newHashSetWithExpectedSize(fieldIds.size());
+    for (Integer fieldId : fieldIds) {
+      String name = fromSchema.findColumnName(fieldId);
+      Types.NestedField field =
+          caseSensitive ? toSchema.findField(name) : 
toSchema.caseInsensitiveFindField(name);
+      Preconditions.checkNotNull(field, "Equality field %s does not exist in 
schema", name);
+      remappedIds.add(field.fieldId());
+    }
+
+    return remappedIds;
+  }
 }
diff --git 
a/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicIcebergSink.java
 
b/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicIcebergSink.java
index 34a74cdf1b..9e2f00bca0 100644
--- 
a/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicIcebergSink.java
+++ 
b/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicIcebergSink.java
@@ -77,6 +77,7 @@ import org.apache.iceberg.Snapshot;
 import org.apache.iceberg.SnapshotRef;
 import org.apache.iceberg.Table;
 import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TableUtil;
 import org.apache.iceberg.catalog.Catalog;
 import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.data.IcebergGenerics;
@@ -1034,51 +1035,64 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
     runTest(rows);
   }
 
-  @Test
-  void testUpsert() throws Exception {
+  @ParameterizedTest
+  @ValueSource(booleans = {false, true})
+  void testUpsert(boolean immediateUpdate) throws Exception {
+    // A restart replays the records against the already created table, which 
hides failures
+    Configuration configuration = new Configuration();
+    configuration.set(RestartStrategyOptions.RESTART_STRATEGY, "none");
+    env.configure(configuration);
+
+    // Field IDs differ from the ones the catalog assigns on table creation
+    Schema schema =
+        new Schema(
+            Types.NestedField.optional(0, "id", Types.IntegerType.get()),
+            Types.NestedField.optional(1, "data", Types.StringType.get()));
+    Types.NestedField equalityField = schema.findField("id");
     List<DynamicIcebergDataImpl> rows =
         Lists.newArrayList(
             // Insert one rows
             new DynamicIcebergDataImpl(
-                SimpleDataUtil.SCHEMA,
+                schema,
                 "t1",
                 SnapshotRef.MAIN_BRANCH,
                 PartitionSpec.unpartitioned(),
                 true,
-                Sets.newHashSet("id"),
+                Sets.newHashSet(equalityField.name()),
                 false),
             // Remaining rows are duplicates
             new DynamicIcebergDataImpl(
-                SimpleDataUtil.SCHEMA,
+                schema,
                 "t1",
                 SnapshotRef.MAIN_BRANCH,
                 PartitionSpec.unpartitioned(),
                 true,
-                Sets.newHashSet("id"),
+                Sets.newHashSet(equalityField.name()),
                 true),
             new DynamicIcebergDataImpl(
-                SimpleDataUtil.SCHEMA,
+                schema,
                 "t1",
                 SnapshotRef.MAIN_BRANCH,
                 PartitionSpec.unpartitioned(),
                 true,
-                Sets.newHashSet("id"),
+                Sets.newHashSet(equalityField.name()),
                 true),
             new DynamicIcebergDataImpl(
-                SimpleDataUtil.SCHEMA,
+                schema,
                 "t1",
                 SnapshotRef.MAIN_BRANCH,
                 PartitionSpec.unpartitioned(),
                 true,
-                Sets.newHashSet("id"),
+                Sets.newHashSet(equalityField.name()),
                 true));
 
-    executeDynamicSink(rows, env, true, 1, null);
+    executeDynamicSink(rows, env, immediateUpdate, 1, null);
 
-    try (CloseableIterable<Record> iterable =
-        IcebergGenerics.read(
-                
CATALOG_EXTENSION.catalog().loadTable(TableIdentifier.of("default", "t1")))
-            .build()) {
+    Table table = 
CATALOG_EXTENSION.catalog().loadTable(TableIdentifier.of("default", "t1"));
+    // The record's equality field ID doesn't exist in the created table, so 
with immediateUpdate
+    // disabled the write fails unless equality field IDs are remapped to the 
table schema.
+    
assertThat(table.schema().findColumnName(equalityField.fieldId())).isNull();
+    try (CloseableIterable<Record> iterable = 
IcebergGenerics.read(table).build()) {
       List<Record> records = Lists.newArrayList();
       for (Record record : iterable) {
         records.add(record);
@@ -1093,61 +1107,70 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
     }
   }
 
-  @Test
-  void testUpsertV3() throws Exception {
-    ImmutableMap<String, String> properties = 
ImmutableMap.of(TableProperties.FORMAT_VERSION, "3");
-    CATALOG_EXTENSION
-        .catalog()
-        .createTable(
-            TableIdentifier.of(DATABASE, "t1"),
-            SimpleDataUtil.SCHEMA,
-            PartitionSpec.unpartitioned(),
-            null,
-            properties);
+  @ParameterizedTest
+  @ValueSource(booleans = {false, true})
+  void testUpsertV3(boolean immediateUpdate) throws Exception {
+    // A restart replays the records against the already created table, which 
hides failures
+    Configuration configuration = new Configuration();
+    configuration.set(RestartStrategyOptions.RESTART_STRATEGY, "none");
+    env.configure(configuration);
+
+    Map<String, String> tableProps = 
ImmutableMap.of(TableProperties.FORMAT_VERSION, "3");
+    TableCreator tableCreator =
+        (catalog, identifier, tableSchema, spec) ->
+            catalog.createTable(identifier, tableSchema, spec, null, 
tableProps);
 
+    // Field IDs differ from the ones the catalog assigns on table creation
+    Schema schema =
+        new Schema(
+            Types.NestedField.optional(0, "id", Types.IntegerType.get()),
+            Types.NestedField.optional(1, "data", Types.StringType.get()));
+    Types.NestedField equalityField = schema.findField("id");
     List<DynamicIcebergDataImpl> rows =
         Lists.newArrayList(
             // Insert one rows
             new DynamicIcebergDataImpl(
-                SimpleDataUtil.SCHEMA,
+                schema,
                 "t1",
                 SnapshotRef.MAIN_BRANCH,
                 PartitionSpec.unpartitioned(),
                 true,
-                Sets.newHashSet("id"),
+                Sets.newHashSet(equalityField.name()),
                 false),
             // Remaining rows are duplicates
             new DynamicIcebergDataImpl(
-                SimpleDataUtil.SCHEMA,
+                schema,
                 "t1",
                 SnapshotRef.MAIN_BRANCH,
                 PartitionSpec.unpartitioned(),
                 true,
-                Sets.newHashSet("id"),
+                Sets.newHashSet(equalityField.name()),
                 true),
             new DynamicIcebergDataImpl(
-                SimpleDataUtil.SCHEMA,
+                schema,
                 "t1",
                 SnapshotRef.MAIN_BRANCH,
                 PartitionSpec.unpartitioned(),
                 true,
-                Sets.newHashSet("id"),
+                Sets.newHashSet(equalityField.name()),
                 true),
             new DynamicIcebergDataImpl(
-                SimpleDataUtil.SCHEMA,
+                schema,
                 "t1",
                 SnapshotRef.MAIN_BRANCH,
                 PartitionSpec.unpartitioned(),
                 true,
-                Sets.newHashSet("id"),
+                Sets.newHashSet(equalityField.name()),
                 true));
 
-    executeDynamicSink(rows, env, true, 1, null);
+    executeDynamicSink(rows, env, immediateUpdate, 1, null, false, 
Maps.newHashMap(), tableCreator);
 
-    try (CloseableIterable<Record> iterable =
-        IcebergGenerics.read(
-                
CATALOG_EXTENSION.catalog().loadTable(TableIdentifier.of("default", "t1")))
-            .build()) {
+    Table table = 
CATALOG_EXTENSION.catalog().loadTable(TableIdentifier.of("default", "t1"));
+    assertThat(TableUtil.formatVersion(table)).isEqualTo(3);
+    // The record's equality field ID doesn't exist in the created table, so 
with immediateUpdate
+    // disabled the write fails unless equality field IDs are remapped to the 
table schema.
+    
assertThat(table.schema().findColumnName(equalityField.fieldId())).isNull();
+    try (CloseableIterable<Record> iterable = 
IcebergGenerics.read(table).build()) {
       List<Record> records = Lists.newArrayList();
       for (Record record : iterable) {
         records.add(record);
@@ -1932,7 +1955,14 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
       Map<String, String> writeProperties)
       throws Exception {
     executeDynamicSink(
-        dynamicData, env, immediateUpdate, parallelism, null, false, 
writeProperties);
+        dynamicData,
+        env,
+        immediateUpdate,
+        parallelism,
+        null,
+        false,
+        writeProperties,
+        TableCreator.DEFAULT);
     verifyResults(dynamicData, writeProperties);
   }
 
@@ -1968,6 +1998,27 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
       boolean overwrite,
       Map<String, String> writeProperties)
       throws Exception {
+    executeDynamicSink(
+        dynamicData,
+        env,
+        immediateUpdate,
+        parallelism,
+        commitHook,
+        overwrite,
+        writeProperties,
+        TableCreator.DEFAULT);
+  }
+
+  private void executeDynamicSink(
+      List<DynamicIcebergDataImpl> dynamicData,
+      StreamExecutionEnvironment env,
+      boolean immediateUpdate,
+      int parallelism,
+      @Nullable CommitHook commitHook,
+      boolean overwrite,
+      Map<String, String> writeProperties,
+      TableCreator tableCreator)
+      throws Exception {
     DataStream<DynamicIcebergDataImpl> dataStream =
         env.fromData(dynamicData, TypeInformation.of(new TypeHint<>() {}));
     env.setParallelism(parallelism);
@@ -1979,6 +2030,7 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
           .catalogLoader(CATALOG_EXTENSION.catalogLoader())
           .writeParallelism(parallelism)
           .immediateTableUpdate(immediateUpdate)
+          .tableCreator(tableCreator)
           .setSnapshotProperty("commit.retry.num-retries", "0")
           .overwrite(overwrite)
           .setAll(writeProperties)
@@ -1989,6 +2041,7 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
           .catalogLoader(CATALOG_EXTENSION.catalogLoader())
           .writeParallelism(parallelism)
           .immediateTableUpdate(immediateUpdate)
+          .tableCreator(tableCreator)
           .overwrite(overwrite)
           .setAll(writeProperties)
           .append();
diff --git 
a/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicTableUpdateOperator.java
 
b/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicTableUpdateOperator.java
index f6b2b368c2..80f0dc2bcb 100644
--- 
a/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicTableUpdateOperator.java
+++ 
b/flink/v2.2/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicTableUpdateOperator.java
@@ -25,6 +25,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import java.util.Collections;
 import org.apache.flink.configuration.Configuration;
 import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.StringData;
 import org.apache.iceberg.PartitionSpec;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.catalog.Catalog;
@@ -120,6 +121,43 @@ class TestDynamicTableUpdateOperator {
     
assertThat(catalog.loadTable(table).schema().schemaId()).isEqualTo(output.schema().schemaId());
   }
 
+  @Test
+  void testEqualityFieldIdsRemappedToTableSchema() throws Exception {
+    Catalog catalog = CATALOG_EXTENSION.catalog();
+    TableIdentifier table = TableIdentifier.of(TABLE);
+
+    DynamicTableUpdateOperator operator =
+        new DynamicTableUpdateOperator(
+            CATALOG_EXTENSION.catalogLoader(),
+            TableCreator.DEFAULT,
+            flinkDynamicSinkConfiguration(CASE_SENSITIVE, PRESERVE_COLUMNS));
+    operator.open(null);
+
+    catalog.createTable(table, SCHEMA1);
+    Schema inputSchema =
+        new Schema(
+            Types.NestedField.required(2, "id", Types.IntegerType.get()),
+            Types.NestedField.optional(1, "data", Types.StringType.get()));
+    Types.NestedField equalityField = inputSchema.findField("id");
+    DynamicRecordInternal input =
+        new DynamicRecordInternal(
+            TABLE,
+            "branch",
+            inputSchema,
+            GenericRowData.of(1, StringData.fromString("test")),
+            PartitionSpec.unpartitioned(),
+            42,
+            true,
+            Collections.singleton(equalityField.fieldId()));
+    DynamicRecordInternal output = operator.map(input);
+
+    assertThat(output.schema().findColumnName(equalityField.fieldId()))
+        .isNotEqualTo(equalityField.name());
+    assertThat(output.equalityFields())
+        .map(output.schema()::findColumnName)
+        .containsExactly(equalityField.name());
+  }
+
   @ParameterizedTest
   @ValueSource(booleans = {true, false})
   void testCaseInSensitivity(boolean caseSensitive) throws Exception {

Reply via email to