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 753c82df65 Flink: Allow configuring a branch for rewrite data files 
action (#18661)
753c82df65 is described below

commit 753c82df656bacf516c17ccddc0792a62933aa17
Author: Swapna Marru <[email protected]>
AuthorDate: Thu Nov 5 08:20:28 2026 -0800

    Flink: Allow configuring a branch for rewrite data files action (#18661)
---
 docs/docs/flink-maintenance.md                     |  1 +
 .../flink/maintenance/api/RewriteDataFiles.java    |  4 +
 .../maintenance/api/RewriteDataFilesConfig.java    | 17 +++++
 .../org/apache/iceberg/flink/sink/IcebergSink.java | 39 ++++++++--
 .../maintenance/api/TestRewriteDataFiles.java      | 35 ++++++++-
 .../api/TestRewriteDataFilesConfig.java            |  2 +
 .../sink/TestIcebergSinkTableMaintenance.java      | 86 ++++++++++++++++++++++
 7 files changed, 173 insertions(+), 11 deletions(-)

diff --git a/docs/docs/flink-maintenance.md b/docs/docs/flink-maintenance.md
index eac8d84a08..c7b24f2e92 100644
--- a/docs/docs/flink-maintenance.md
+++ b/docs/docs/flink-maintenance.md
@@ -511,6 +511,7 @@ These keys are used in SQL (SET or table WITH options) or 
via `IcebergSink.Build
 | `flink-maintenance.rewrite.schedule.data-file-count` | Trigger after N data 
files | `1000` |
 | `flink-maintenance.rewrite.schedule.data-file-size` | Trigger after total 
data file size (bytes) | `107374182400` (100GB) |
 | `flink-maintenance.rewrite.schedule.interval-second` | Trigger after time 
interval (seconds) | `600` |
+| `flink-maintenance.rewrite.branch` | Branch to compact. Defaults to the 
sink's write branch, or to the convert equality deletes target branch when 
convert equality deletes is also enabled | Not set |
 | `flink-maintenance.rewrite.max-bytes` | Maximum bytes to rewrite per 
execution | `Long.MAX_VALUE` |
 | `flink-maintenance.rewrite.partial-progress.enabled` | Enable partial 
progress commits | `false` |
 | `flink-maintenance.rewrite.partial-progress.max-commits` | Maximum commits 
for partial progress | `10` |
diff --git 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/RewriteDataFiles.java
 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/RewriteDataFiles.java
index 8d21dc0b6b..2a72cb83f6 100644
--- 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/RewriteDataFiles.java
+++ 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/RewriteDataFiles.java
@@ -260,6 +260,10 @@ public class RewriteDataFiles {
           .scheduleOnInterval(
               
Duration.ofSeconds(rewriteDataFilesConfig.scheduleOnIntervalSecond()));
 
+      if (rewriteDataFilesConfig.branch() != null) {
+        this.branch(rewriteDataFilesConfig.branch());
+      }
+
       // override the rewrite options
       this.rewriteOptions.putAll(rewriteDataFilesConfig.properties());
 
diff --git 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/RewriteDataFilesConfig.java
 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/RewriteDataFilesConfig.java
index 1a4645cb2b..2d8722f38a 100644
--- 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/RewriteDataFilesConfig.java
+++ 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/RewriteDataFilesConfig.java
@@ -30,6 +30,15 @@ import org.apache.iceberg.util.PropertyUtil;
 public class RewriteDataFilesConfig {
   public static final String PREFIX = FlinkMaintenanceConfig.PREFIX + 
"rewrite.";
 
+  public static final String BRANCH = PREFIX + "branch";
+  public static final ConfigOption<String> BRANCH_OPTION =
+      ConfigOptions.key(BRANCH)
+          .stringType()
+          .noDefaultValue()
+          .withDescription(
+              "The branch to compact. Defaults to the sink's write branch, or 
to the convert "
+                  + "equality deletes target branch when convert equality 
deletes is also enabled.");
+
   public static final String MAX_BYTES = PREFIX + "max-bytes";
   public static final ConfigOption<Long> MAX_BYTES_OPTION =
       ConfigOptions.key(MAX_BYTES)
@@ -170,6 +179,14 @@ public class RewriteDataFilesConfig {
         .parse();
   }
 
+  /**
+   * Gets the branch to compact, or null to default to the sink's write 
branch, or to the convert
+   * equality deletes target branch when convert equality deletes is also 
enabled.
+   */
+  public String branch() {
+    return 
confParser.stringConf().option(BRANCH).flinkConfig(BRANCH_OPTION).parseOptional();
+  }
+
   public Map<String, String> properties() {
     return PropertyUtil.propertiesWithPrefix(writeProperties, PREFIX);
   }
diff --git 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java
index 789bb00082..b436c969db 100644
--- 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java
+++ 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java
@@ -803,10 +803,17 @@ public class IcebergSink
       if (flinkWriteConf.compactMode()) {
         RewriteDataFilesConfig rewriteDataFilesConfig =
             flinkMaintenanceConfig.createRewriteDataFilesConfig();
+
+        String defaultRewriteBranch = flinkWriteConf.branch();
+        if (flinkWriteConf.convertEqualityDeletesMode()) {
+          defaultRewriteBranch =
+              convertTargetBranch(
+                  flinkWriteConf, 
flinkMaintenanceConfig.createConvertEqualityDeletesConfig());
+          validateRewriteBranch(defaultRewriteBranch, rewriteDataFilesConfig);
+        }
+
         maintenanceTasks.add(
-            RewriteDataFiles.builder()
-                .branch(flinkWriteConf.branch())
-                .config(rewriteDataFilesConfig));
+            
RewriteDataFiles.builder().branch(defaultRewriteBranch).config(rewriteDataFilesConfig));
       }
 
       if (flinkWriteConf.expireSnapshotsMode()) {
@@ -856,10 +863,8 @@ public class IcebergSink
           "Equality field columns must be set to convert equality deletes to 
deletion vectors.");
       ConvertEqualityDeletesConfig convertEqualityDeletesConfig =
           flinkMaintenanceConfig.createConvertEqualityDeletesConfig();
-      // The sink writes equality deletes to its write branch, so that becomes 
the staging branch.
-      // The target defaults to the same branch, i.e. an in-place conversion.
-      String targetBranch = convertEqualityDeletesConfig.targetBranch();
-      String convertTargetBranch = targetBranch != null ? targetBranch : 
flinkWriteConf.branch();
+      String convertTargetBranch =
+          convertTargetBranch(flinkWriteConf, convertEqualityDeletesConfig);
       List<String> convertEqualityFieldColumns = Lists.newArrayList();
       for (int fieldId : equalityFieldIds) {
         
convertEqualityFieldColumns.add(table.schema().findColumnName(fieldId));
@@ -873,6 +878,26 @@ public class IcebergSink
               .config(convertEqualityDeletesConfig));
     }
 
+    private String convertTargetBranch(
+        FlinkWriteConf flinkWriteConf, ConvertEqualityDeletesConfig 
convertEqualityDeletesConfig) {
+      // The sink writes equality deletes to its write branch, so that becomes 
the staging branch.
+      // The target defaults to the same branch, i.e. an in-place conversion.
+      String targetBranch = convertEqualityDeletesConfig.targetBranch();
+      return targetBranch != null ? targetBranch : flinkWriteConf.branch();
+    }
+
+    private void validateRewriteBranch(
+        String convertTargetBranch, RewriteDataFilesConfig 
rewriteDataFilesConfig) {
+      String rewriteBranch = rewriteDataFilesConfig.branch();
+      Preconditions.checkArgument(
+          rewriteBranch == null || rewriteBranch.equals(convertTargetBranch),
+          "Invalid rewrite branch %s. Expected rewrite branch %s, "
+              + "the convert equality deletes target branch. Unset config %s 
to default to it.",
+          rewriteBranch,
+          convertTargetBranch,
+          RewriteDataFilesConfig.BRANCH);
+    }
+
     /**
      * Append the iceberg sink operators to write records to iceberg table.
      *
diff --git 
a/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/maintenance/api/TestRewriteDataFiles.java
 
b/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/maintenance/api/TestRewriteDataFiles.java
index 7cb389b663..3ea0c3e2f6 100644
--- 
a/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/maintenance/api/TestRewriteDataFiles.java
+++ 
b/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/maintenance/api/TestRewriteDataFiles.java
@@ -33,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import java.time.Instant;
 import java.util.List;
 import java.util.stream.StreamSupport;
+import org.apache.flink.configuration.Configuration;
 import org.apache.flink.streaming.api.graph.StreamGraphGenerator;
 import org.apache.iceberg.FileFormat;
 import org.apache.iceberg.ManifestFiles;
@@ -50,6 +51,7 @@ import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.FieldSource;
 
 class TestRewriteDataFiles extends MaintenanceTaskTestBase {
+  private static final String TEST_BRANCH = "test-branch";
 
   private static final FileFormat[] FILE_FORMATS =
       new FileFormat[] {FileFormat.AVRO, FileFormat.PARQUET, FileFormat.ORC};
@@ -619,18 +621,43 @@ class TestRewriteDataFiles extends 
MaintenanceTaskTestBase {
 
   @Test
   void testBranch() throws Exception {
+    Table table = createTableWithBranch(TEST_BRANCH);
+
+    rewriteAndAssertBranchCompacted(
+        table, 
RewriteDataFiles.builder().rewriteAll(true).branch(TEST_BRANCH));
+  }
+
+  @Test
+  void testRewriteOnCustomBranch() throws Exception {
+    Table table = createTableWithBranch(TEST_BRANCH);
+    RewriteDataFilesConfig config =
+        new RewriteDataFilesConfig(
+            table,
+            ImmutableMap.of(RewriteDataFilesConfig.BRANCH, TEST_BRANCH),
+            new Configuration());
+
+    rewriteAndAssertBranchCompacted(
+        table,
+        
RewriteDataFiles.builder().rewriteAll(true).branch(SnapshotRef.MAIN_BRANCH).config(config));
+  }
+
+  /** Creates a table with a branch at two data files, while main gets a third 
one. */
+  private Table createTableWithBranch(String branchName) throws Exception {
     Table table = createTable();
     insert(table, 1, "a");
     insert(table, 2, "b");
 
     // Create branch based on above inserts
-    String branchName = "test-branch";
     table.manageSnapshots().createBranch(branchName).commit();
 
     // Insert another file on main only (main has 3 files, branch stays at 2)
     insert(table, 3, "c");
+    return table;
+  }
 
-    
appendRewriteDataFiles(RewriteDataFiles.builder().rewriteAll(true).branch(branchName));
+  private void rewriteAndAssertBranchCompacted(Table table, 
RewriteDataFiles.Builder builder)
+      throws Exception {
+    appendRewriteDataFiles(builder);
 
     runAndWaitForSuccess(infra.env(), infra.source(), infra.sink());
 
@@ -638,7 +665,7 @@ class TestRewriteDataFiles extends MaintenanceTaskTestBase {
 
     // Branch should be compacted from 2 files to 1
     assertThat(
-            table.snapshot(branchName).dataManifests(table.io()).stream()
+            table.snapshot(TEST_BRANCH).dataManifests(table.io()).stream()
                 .flatMap(
                     m ->
                         StreamSupport.stream(
@@ -646,7 +673,7 @@ class TestRewriteDataFiles extends MaintenanceTaskTestBase {
                 .count())
         .isEqualTo(1);
     SimpleDataUtil.assertTableRecords(
-        table, ImmutableList.of(createRecord(1, "a"), createRecord(2, "b")), 
branchName);
+        table, ImmutableList.of(createRecord(1, "a"), createRecord(2, "b")), 
TEST_BRANCH);
 
     // Main should be untouched with 3 files
     assertFileNum(table, 3, 0);
diff --git 
a/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/maintenance/api/TestRewriteDataFilesConfig.java
 
b/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/maintenance/api/TestRewriteDataFilesConfig.java
index 665a82ea15..73cfd4eb17 100644
--- 
a/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/maintenance/api/TestRewriteDataFilesConfig.java
+++ 
b/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/maintenance/api/TestRewriteDataFilesConfig.java
@@ -50,6 +50,7 @@ public class TestRewriteDataFilesConfig extends 
OperatorTestBase {
     input.put(RewriteDataFilesConfig.SCHEDULE_ON_DATA_FILE_COUNT, "20");
     input.put(RewriteDataFilesConfig.SCHEDULE_ON_DATA_FILE_SIZE, "30");
     input.put(RewriteDataFilesConfig.SCHEDULE_ON_INTERVAL_SECOND, "60");
+    input.put(RewriteDataFilesConfig.BRANCH, "custom");
     input.put("other.config", "should-be-ignored");
   }
 
@@ -100,6 +101,7 @@ public class TestRewriteDataFilesConfig extends 
OperatorTestBase {
     assertThat(config.partialProgressEnable()).isTrue();
     assertThat(config.partialProgressMaxCommits()).isEqualTo(5);
     assertThat(config.maxRewriteBytes()).isEqualTo(1024L);
+    assertThat(config.branch()).isEqualTo("custom");
 
     // check the config about the schedule
     assertThat(config.scheduleOnCommitCount()).isEqualTo(10);
diff --git 
a/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/TestIcebergSinkTableMaintenance.java
 
b/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/TestIcebergSinkTableMaintenance.java
index 049589f3a4..af21f34937 100644
--- 
a/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/TestIcebergSinkTableMaintenance.java
+++ 
b/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/TestIcebergSinkTableMaintenance.java
@@ -42,6 +42,7 @@ import org.apache.iceberg.ManifestFiles;
 import org.apache.iceberg.ManifestReader;
 import org.apache.iceberg.PartitionSpec;
 import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
 import org.apache.iceberg.Table;
 import org.apache.iceberg.TableProperties;
 import org.apache.iceberg.actions.SizeBasedFileRewritePlanner;
@@ -50,10 +51,13 @@ import org.apache.iceberg.flink.FlinkWriteOptions;
 import org.apache.iceberg.flink.MiniFlinkClusterExtension;
 import org.apache.iceberg.flink.SimpleDataUtil;
 import org.apache.iceberg.flink.TestFixtures;
+import org.apache.iceberg.flink.maintenance.api.ConvertEqualityDeletesConfig;
 import org.apache.iceberg.flink.maintenance.api.DeleteOrphanFilesConfig;
 import org.apache.iceberg.flink.maintenance.api.ExpireSnapshotsConfig;
+import org.apache.iceberg.flink.maintenance.api.FlinkMaintenanceConfig;
 import org.apache.iceberg.flink.maintenance.api.LockConfig;
 import org.apache.iceberg.flink.maintenance.api.RewriteDataFilesConfig;
+import org.apache.iceberg.flink.maintenance.operator.ManualSource;
 import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
 import org.apache.iceberg.relocated.com.google.common.collect.Lists;
@@ -61,6 +65,7 @@ import 
org.apache.iceberg.relocated.com.google.common.collect.Maps;
 import org.apache.iceberg.util.ContentFileUtil;
 import org.awaitility.Awaitility;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.FieldSource;
@@ -436,6 +441,87 @@ class TestIcebergSinkTableMaintenance extends 
TestFlinkIcebergSinkBase {
     }
   }
 
+  // TODO: Also run with the JDBC lock once JdbcLockFactory creates the lock 
table safely. The
+  // trigger manager and the lock remover both create it on open, and the 
second CREATE TABLE
+  // fails with "table already exists" when they race.
+  @Test
+  public void testConvertEqualityDeletesWithRewriteEnabledE2e() throws 
Exception {
+    setupLockConfig("");
+    setupCompactionConfig();
+    setupConvertEqualityDeletesConfig();
+    upgradeToFormatV3();
+    flinkConf.put(FlinkWriteOptions.BRANCH.key(), "staging");
+    flinkConf.put(ConvertEqualityDeletesConfig.TARGET_BRANCH, 
SnapshotRef.MAIN_BRANCH);
+    flinkConf.put(FlinkMaintenanceConfig.RATE_LIMIT, "1");
+    flinkConf.put(FlinkMaintenanceConfig.LOCK_CHECK_DELAY, "1");
+
+    new GenericAppenderHelper(table, FileFormat.PARQUET, tempDir)
+        .appendToTable(ImmutableList.of(SimpleDataUtil.createRecord(1, 
"aaa")));
+
+    ManualSource<Row> source = new ManualSource<>(env, ROW_TYPE_INFO);
+    DataStream<RowData> dataStream =
+        source
+            .dataStream()
+            .map(CONVERTER::toInternal, 
FlinkCompatibilityUtil.toTypeInfo(SimpleDataUtil.ROW_TYPE));
+
+    IcebergSink.forRowData(dataStream)
+        .table(table)
+        .tableLoader(tableLoader)
+        .upsert(true)
+        .equalityFieldColumns(Lists.newArrayList("id"))
+        .setAll(flinkConf)
+        .append();
+
+    source.sendRecord(Row.of(1, "bbb"));
+    JobClient jobClient = env.executeAsync("Test Rewrite On Convert Target 
Branch E2E");
+    try {
+      Awaitility.await()
+          .atMost(Duration.ofMinutes(2))
+          .pollInterval(Duration.ofMillis(200))
+          .untilAsserted(
+              () -> {
+                assertThat(dvCountOnMain(table)).isEqualTo(0);
+                assertThat(getDataFiles(table.currentSnapshot(), 
table)).hasSize(1);
+                SimpleDataUtil.assertTableRecords(
+                    table, ImmutableList.of(SimpleDataUtil.createRecord(1, 
"bbb")));
+              });
+    } finally {
+      jobClient.cancel();
+    }
+  }
+
+  @Test
+  void testRewriteBranchMustMatchConvertTargetBranch() {
+    setupLockConfig("");
+    setupCompactionConfig();
+    setupConvertEqualityDeletesConfig();
+    upgradeToFormatV3();
+    flinkConf.put(FlinkWriteOptions.BRANCH.key(), "staging");
+    flinkConf.put(ConvertEqualityDeletesConfig.TARGET_BRANCH, "main");
+    flinkConf.put(RewriteDataFilesConfig.BRANCH, "staging");
+
+    List<Row> rows = Lists.newArrayList(Row.of(1, "hello"));
+    DataStream<RowData> dataStream =
+        env.addSource(createBoundedSource(rows), ROW_TYPE_INFO)
+            .map(CONVERTER::toInternal, 
FlinkCompatibilityUtil.toTypeInfo(SimpleDataUtil.ROW_TYPE));
+
+    assertThatThrownBy(
+            () ->
+                IcebergSink.forRowData(dataStream)
+                    .table(table)
+                    .tableLoader(tableLoader)
+                    .upsert(true)
+                    .equalityFieldColumns(Lists.newArrayList("id"))
+                    .setAll(flinkConf)
+                    .append())
+        .isInstanceOf(IllegalArgumentException.class)
+        .hasMessage(
+            "Invalid rewrite branch staging. Expected rewrite branch main, "
+                + "the convert equality deletes target branch. Unset config "
+                + RewriteDataFilesConfig.BRANCH
+                + " to default to it.");
+  }
+
   private static long dvCountOnMain(Table table) throws IOException {
     table.refresh();
     if (table.currentSnapshot() == null) {

Reply via email to