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

claudevdm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new 728ef1b4195 AddFiles: Wire a destination-table key through the 
registration chain (#40817)
728ef1b4195 is described below

commit 728ef1b4195f97ff47abceadb72d274b627959d6
Author: claudevdm <[email protected]>
AuthorDate: Tue Oct 27 16:01:06 2026 -0400

    AddFiles: Wire a destination-table key through the registration chain 
(#40817)
    
    * Key by table
    
    * trigger IT
    
    * comments
    
    * add untracked file
---
 .../IO_Iceberg_Integration_Tests.json              |   2 +-
 CHANGES.md                                         |   1 +
 .../org/apache/beam/sdk/io/iceberg/AddFiles.java   | 296 ++++++++++++++-------
 .../apache/beam/sdk/io/iceberg/ManifestKey.java    |  45 ++++
 .../beam/sdk/io/iceberg/ReadFooterSchema.java      |  60 +++--
 .../apache/beam/sdk/io/iceberg/AddFilesTest.java   |  20 +-
 .../beam/sdk/io/iceberg/ReadFooterSchemaTest.java  |  16 ++
 7 files changed, 324 insertions(+), 116 deletions(-)

diff --git a/.github/trigger_files/IO_Iceberg_Integration_Tests.json 
b/.github/trigger_files/IO_Iceberg_Integration_Tests.json
index 7ab7bcd9a9c..37dd25bf902 100644
--- a/.github/trigger_files/IO_Iceberg_Integration_Tests.json
+++ b/.github/trigger_files/IO_Iceberg_Integration_Tests.json
@@ -1,4 +1,4 @@
 {
     "comment": "Modify this file in a trivial way to cause this test suite to 
run.",
-    "modification": 2
+    "modification": 3
 }
diff --git a/CHANGES.md b/CHANGES.md
index d616a633531..c4cf7e11f13 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -99,6 +99,7 @@
 * (Python) `Regex.all_matches` now returns every group of the regex, instead 
of failing with a `TypeError` when the regex has no groups and dropping nested 
or unmatched trailing groups 
([#40562](https://github.com/apache/beam/issues/40562)).
 * (Python) `--dataflow_endpoint` now defaults to `''` instead of 
`https://dataflow.googleapis.com`. When unset, the Google Cloud client library 
resolves the Dataflow endpoint and honors `GOOGLE_CLOUD_UNIVERSE_DOMAIN`, so 
Dataflow jobs can be submitted in non-default universes. Code that reads 
`GoogleCloudOptions.dataflow_endpoint` should handle an empty value 
([#40569](https://github.com/apache/beam/issues/40569)).
 * (Python) The `xgboost` extras now install `xgboost>=2.0.0,<4` and 
`datatable>=1.1.0,<2`, moving both the lower and upper bounds for both. The 
previous minimum `xgboost` and `datatable` versions (`1.6.0` and `1.0.0` 
respectively) are no longer allowed 
([#40694](https://github.com/apache/beam/issues/40694)).
+* (Java) [IcebergIO] AddFiles (`IcebergAddFiles` in YAML) now carries the 
destination table alongside each file through the registration chain, so that 
one transform can register files into many tables. The coders of its internal 
`PCollection`s change, which makes this an update incompatible change for 
streaming pipelines that use it 
([#40817](https://github.com/apache/beam/pull/40817)).
 
 ## Deprecations
 
diff --git 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFiles.java
 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFiles.java
index 00cf77af1dc..c97a20c6221 100644
--- 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFiles.java
+++ 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFiles.java
@@ -38,11 +38,12 @@ import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Collectors;
 import org.apache.beam.sdk.coders.KvCoder;
 import org.apache.beam.sdk.coders.RowCoder;
-import org.apache.beam.sdk.coders.VarIntCoder;
+import org.apache.beam.sdk.coders.StringUtf8Coder;
 import org.apache.beam.sdk.coders.VarLongCoder;
 import 
org.apache.beam.sdk.io.iceberg.SchemaEvolutionConfig.UnverifiableFileHandling;
 import org.apache.beam.sdk.metrics.Counter;
@@ -58,6 +59,7 @@ import org.apache.beam.sdk.transforms.DoFn;
 import org.apache.beam.sdk.transforms.GroupIntoBatches;
 import org.apache.beam.sdk.transforms.PTransform;
 import org.apache.beam.sdk.transforms.ParDo;
+import org.apache.beam.sdk.transforms.Values;
 import org.apache.beam.sdk.transforms.Wait;
 import org.apache.beam.sdk.transforms.WithKeys;
 import org.apache.beam.sdk.transforms.windowing.BoundedWindow;
@@ -71,6 +73,7 @@ import org.apache.beam.sdk.values.PCollectionTuple;
 import org.apache.beam.sdk.values.Row;
 import org.apache.beam.sdk.values.TupleTag;
 import org.apache.beam.sdk.values.TupleTagList;
+import org.apache.beam.sdk.values.TypeDescriptors;
 import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions;
 import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Strings;
 import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.hash.Hasher;
@@ -284,13 +287,17 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
       paths = windowed.apply("WaitForSchemaCommit", Wait.on(committed));
     }
 
-    PCollectionTuple dataFiles =
+    PCollection<KV<String, String>> keyedPaths =
         paths.apply(
+            "KeyByTable",
+            WithKeys.<String, 
String>of(tableIdentifier).withKeyType(TypeDescriptors.strings()));
+
+    PCollectionTuple dataFiles =
+        keyedPaths.apply(
             "ConvertToDataFiles",
             ParDo.of(
                     new ConvertToDataFile(
                         catalogConfig,
-                        tableIdentifier,
                         locationPrefix,
                         partitionFields,
                         sortFields,
@@ -298,20 +305,37 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
                         evolution))
                 .withOutputTags(DATA_FILES, TupleTagList.of(ERRORS)));
     SchemaCoder<SerializableDataFile> sdfCoder;
+    SchemaCoder<ManifestKey> manifestKeyCoder;
     try {
-      sdfCoder = 
SchemaRegistry.createDefault().getSchemaCoder(SerializableDataFile.class);
+      SchemaRegistry schemas = SchemaRegistry.createDefault();
+      sdfCoder = schemas.getSchemaCoder(SerializableDataFile.class);
+      manifestKeyCoder = schemas.getSchemaCoder(ManifestKey.class);
     } catch (Exception e) {
       throw new RuntimeException(e);
     }
 
-    PCollection<KV<Integer, SerializableDataFile>> keyedFiles =
+    PCollection<KV<ManifestKey, SerializableDataFile>> keyedFiles =
         dataFiles
             .get(DATA_FILES)
-            .setCoder(sdfCoder)
-            .apply("AddSpecIdKey", 
WithKeys.of(SerializableDataFile::getPartitionSpecId))
-            .setCoder(KvCoder.of(VarIntCoder.of(), sdfCoder));
-
-    GroupIntoBatches<Integer, SerializableDataFile> batchDataFiles =
+            .setCoder(KvCoder.of(StringUtf8Coder.of(), sdfCoder))
+            .apply(
+                "AddTableAndSpecIdKey",
+                ParDo.of(
+                    new DoFn<
+                        KV<String, SerializableDataFile>, KV<ManifestKey, 
SerializableDataFile>>() {
+                      @ProcessElement
+                      public void process(
+                          @Element KV<String, SerializableDataFile> element,
+                          OutputReceiver<KV<ManifestKey, 
SerializableDataFile>> out) {
+                        SerializableDataFile file = element.getValue();
+                        out.output(
+                            KV.of(
+                                ManifestKey.of(element.getKey(), 
file.getPartitionSpecId()), file));
+                      }
+                    }))
+            .setCoder(KvCoder.of(manifestKeyCoder, sdfCoder));
+
+    GroupIntoBatches<ManifestKey, SerializableDataFile> batchDataFiles =
         GroupIntoBatches.ofSize(manifestFileSize);
     GroupIntoBatches<String, byte[]> batchManifestFiles =
         GroupIntoBatches.ofSize(DEFAULT_MAX_MANIFESTS_PER_SNAPSHOT);
@@ -322,29 +346,38 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
           
batchManifestFiles.withMaxBufferingDuration(checkStateNotNull(intervalTrigger));
     }
 
-    PCollection<KV<Integer, Iterable<SerializableDataFile>>> groupedFiles =
+    PCollection<KV<ManifestKey, Iterable<SerializableDataFile>>> groupedFiles =
         keyedFiles.apply("GroupDataFilesIntoBatches", batchDataFiles);
 
     PCollection<KV<String, byte[]>> manifests =
-        groupedFiles.apply(
-            "CreateManifests", ParDo.of(new CreateManifests(catalogConfig, 
tableIdentifier)));
+        groupedFiles.apply("CreateManifests", ParDo.of(new 
CreateManifests(catalogConfig)));
 
     PCollection<Row> snapshots =
         manifests
             .apply("GatherManifests", batchManifestFiles)
-            .apply(
-                "CommitManifests",
-                ParDo.of(new CommitManifestFilesDoFn(catalogConfig, 
tableIdentifier)))
+            .apply("CommitManifests", ParDo.of(new 
CommitManifestFilesDoFn(catalogConfig)))
             .setRowSchema(SnapshotInfo.getSchema());
 
     return PCollectionRowTuple.of(
         OUTPUT_TAG, snapshots, ERROR_TAG, 
dataFiles.get(ERRORS).setRowSchema(ERROR_SCHEMA));
   }
 
+  /** The footer read is keyed, so the single-table path supplies its one 
table as the key. */
+  private PCollection<KV<String, CollectDistinctSchemas.SchemaGroup>> 
fileSchemas(
+      PCollection<KV<String, String>> keyed) {
+    return keyed
+        .apply("ReadFooterSchema", ParDo.of(new ReadFooterSchema(evolution, 
locationPrefix)))
+        .setCoder(KvCoder.of(StringUtf8Coder.of(), 
CollectDistinctSchemas.groupCoder()));
+  }
+
   private PCollection<List<CollectDistinctSchemas.SchemaGroup>> 
distinctSchemas(
       PCollection<String> windowed) {
-    return windowed
-        .apply("ReadFooterSchema", ParDo.of(new ReadFooterSchema(evolution, 
locationPrefix)))
+    PCollection<KV<String, String>> keyed =
+        windowed.apply(
+            "KeyBySingleTable",
+            WithKeys.<String, 
String>of(tableIdentifier).withKeyType(TypeDescriptors.strings()));
+    return fileSchemas(keyed)
+        .apply("DropTableKey", Values.create())
         .setCoder(CollectDistinctSchemas.groupCoder())
         .apply(
             "CollectDistinctSchemas",
@@ -413,21 +446,37 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
    *       back to the runner on the main thread, as {@link 
MultiOutputReceiver} is not thread-safe.
    * </ul>
    */
-  static class ConvertToDataFile extends DoFn<String, SerializableDataFile> {
+  static class ConvertToDataFile
+      extends DoFn<KV<String, String>, KV<String, SerializableDataFile>> {
     private final IcebergCatalogConfig catalogConfig;
-    private final String identifier;
     public static final TupleTag<Row> ERRORS = new TupleTag<>();
-    public static final TupleTag<SerializableDataFile> DATA_FILES = new 
TupleTag<>();
+    public static final TupleTag<KV<String, SerializableDataFile>> DATA_FILES 
= new TupleTag<>();
     private final @Nullable String prefix;
     private final @Nullable List<String> partitionFields;
     private final @Nullable List<String> sortFields;
     private final @Nullable Map<String, String> tableProps;
     private final SchemaEvolutionConfig evolution;
     private transient @MonotonicNonNull BoundedAsyncTasks<ProcessResult> tasks;
-    private transient volatile @MonotonicNonNull Table table;
-    private transient volatile boolean tableMissing;
     private transient @MonotonicNonNull Set<String> warned;
-    private final AtomicBoolean refreshedThisBundle = new AtomicBoolean();
+    private transient @MonotonicNonNull ConcurrentMap<String, TableState> 
states;
+
+    /** One table's cached handle and flags; the single-table fields became a 
map of these. */
+    private static final class TableState {
+      volatile @Nullable Table table;
+      volatile boolean missing;
+      final AtomicBoolean refreshedThisBundle = new AtomicBoolean();
+    }
+
+    private TableState stateFor(String tableName) {
+      ConcurrentMap<String, TableState> map = checkStateNotNull(states);
+      TableState existing = map.get(tableName);
+      if (existing != null) {
+        return existing;
+      }
+      TableState created = new TableState();
+      TableState raced = map.putIfAbsent(tableName, created);
+      return raced != null ? raced : created;
+    }
 
     // Number of parallel threads processing incoming files
     private static final int THREAD_POOL_SIZE = 10;
@@ -435,14 +484,12 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
 
     public ConvertToDataFile(
         IcebergCatalogConfig catalogConfig,
-        String identifier,
         @Nullable String prefix,
         @Nullable List<String> partitionFields,
         @Nullable List<String> sortFields,
         @Nullable Map<String, String> tableProps) {
       this(
           catalogConfig,
-          identifier,
           prefix,
           partitionFields,
           sortFields,
@@ -452,14 +499,12 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
 
     public ConvertToDataFile(
         IcebergCatalogConfig catalogConfig,
-        String identifier,
         @Nullable String prefix,
         @Nullable List<String> partitionFields,
         @Nullable List<String> sortFields,
         @Nullable Map<String, String> tableProps,
         SchemaEvolutionConfig evolution) {
       this.catalogConfig = catalogConfig;
-      this.identifier = identifier;
       this.prefix = prefix;
       this.partitionFields = partitionFields;
       this.sortFields = sortFields;
@@ -516,6 +561,7 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
     }
 
     private static class ProcessResult {
+      final String table;
       final @Nullable SerializableDataFile dataFile;
       final @Nullable Row errorRow;
 
@@ -527,6 +573,7 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
       final PaneInfo paneInfo;
 
       ProcessResult(
+          String table,
           @Nullable SerializableDataFile dataFile,
           @Nullable Row errorRow,
           @Nullable Unverified unverified,
@@ -538,6 +585,7 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
             "Expected only one of dataFile or errorRow, but got both:%n\tfile: 
%s%n\terror: %s",
             dataFile != null ? dataFile.getPath() : null,
             errorRow);
+        this.table = table;
         this.dataFile = dataFile;
         this.errorRow = errorRow;
         this.unverified = unverified;
@@ -551,13 +599,16 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
     public void setup() {
       tasks = new BoundedAsyncTasks<>(THREAD_POOL_SIZE, MAX_IN_FLIGHT_TASKS);
       warned = ConcurrentHashMap.newKeySet();
+      states = new ConcurrentHashMap<>();
     }
 
     /** Clears anything left behind if the runner reuses this instance after a 
failed bundle. */
     @StartBundle
     public void startBundle() {
       checkStateNotNull(tasks).cancelAll();
-      refreshedThisBundle.set(false);
+      for (TableState state : checkStateNotNull(states).values()) {
+        state.refreshedThisBundle.set(false);
+      }
     }
 
     @Teardown
@@ -569,13 +620,14 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
 
     @ProcessElement
     public void process(
-        @Element String filePath,
+        @Element KV<String, String> element,
         @Timestamp Instant timestamp,
         BoundedWindow window,
         PaneInfo paneInfo,
         MultiOutputReceiver output)
         throws Exception {
-      Callable<ProcessResult> task = createProcessTask(filePath, timestamp, 
window, paneInfo);
+      Callable<ProcessResult> task =
+          createProcessTask(element.getKey(), element.getValue(), timestamp, 
window, paneInfo);
       checkStateNotNull(tasks).submit(task, result -> outputResult(result, 
output));
     }
 
@@ -593,7 +645,7 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
         output
             .get(DATA_FILES)
             .outputWindowedValue(
-                result.dataFile,
+                KV.of(result.table, result.dataFile),
                 result.timestamp,
                 Collections.singleton(result.window),
                 result.paneInfo);
@@ -611,7 +663,8 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
         context.output(ERRORS, result.errorRow, result.timestamp, 
result.window);
         numErrorFiles.inc();
       } else if (result.dataFile != null) {
-        context.output(DATA_FILES, result.dataFile, result.timestamp, 
result.window);
+        context.output(
+            DATA_FILES, KV.of(result.table, result.dataFile), 
result.timestamp, result.window);
         countUnverified(result);
       }
     }
@@ -625,9 +678,14 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
     }
 
     private Callable<ProcessResult> createProcessTask(
-        String filePath, Instant timestamp, BoundedWindow window, PaneInfo 
paneInfo) {
+        String tableName,
+        String filePath,
+        Instant timestamp,
+        BoundedWindow window,
+        PaneInfo paneInfo) {
 
       return () -> {
+        TableState state = stateFor(tableName);
         FileFormat format;
         @Nullable ParquetMetadata parquetFooter = null;
         if (hasNoExtension(filePath)) {
@@ -635,52 +693,65 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
             parquetFooter = ParquetFooters.read(filePath);
           } catch (Exception e) {
             return errorResult(
-                filePath, NOT_PARQUET_ERROR + errorMessage(e), timestamp, 
window, paneInfo);
+                tableName,
+                filePath,
+                NOT_PARQUET_ERROR + errorMessage(e),
+                timestamp,
+                window,
+                paneInfo);
           }
           format = FileFormat.PARQUET;
         } else {
           try {
             format = inferFormat(filePath);
           } catch (UnknownFormatException e) {
-            return errorResult(filePath, UNKNOWN_FORMAT_ERROR, timestamp, 
window, paneInfo);
+            return errorResult(
+                tableName, filePath, UNKNOWN_FORMAT_ERROR, timestamp, window, 
paneInfo);
           }
         }
 
         // ---- Infrastructure phase. Failures propagate so the runner retries 
the bundle;
         // per-file error rows here would silently drop in-flight files on a 
transient blip.
         // Only conditions that are properties of the file go to the error 
output.
-        if (tableMissing) {
+        if (state.missing) {
           return errorResult(
-              filePath, MISSING_TABLE_ERROR + identifier, timestamp, window, 
paneInfo);
+              tableName, filePath, MISSING_TABLE_ERROR + tableName, timestamp, 
window, paneInfo);
         }
-        if (table == null) {
-          synchronized (this) {
-            if (table == null) {
+        if (state.table == null) {
+          synchronized (state) {
+            if (state.table == null) {
               try {
-                table = getOrCreateTable(filePath, format);
+                state.table = getOrCreateTable(tableName, filePath, format);
               } catch (FileNotFoundException e) {
-                return errorResult(filePath, errorMessage(e), timestamp, 
window, paneInfo);
+                return errorResult(
+                    tableName, filePath, errorMessage(e), timestamp, window, 
paneInfo);
               } catch (NoSuchTableException e) {
-                tableMissing = true;
+                state.missing = true;
                 return errorResult(
-                    filePath, MISSING_TABLE_ERROR + identifier, timestamp, 
window, paneInfo);
+                    tableName,
+                    filePath,
+                    MISSING_TABLE_ERROR + tableName,
+                    timestamp,
+                    window,
+                    paneInfo);
               }
             }
           }
         }
+        Table table = checkStateNotNull(state.table);
 
         // Check if the file path contains the provided prefix
         if (table.spec().isPartitioned()
             && !Strings.isNullOrEmpty(prefix)
             && !filePath.startsWith(checkStateNotNull(prefix))) {
-          return errorResult(filePath, PREFIX_ERROR, timestamp, window, 
paneInfo);
+          return errorResult(tableName, filePath, PREFIX_ERROR, timestamp, 
window, paneInfo);
         }
 
-        if (table.schema().columns().isEmpty() && firstTime("empty schema")) {
+        if (table.schema().columns().isEmpty() && firstTime(tableName, "empty 
schema")) {
           LOG.warn(
               "Table {} has no columns: files register with no readable 
columns and no stats."
                   + " Enable schema evolution to infer the schema from the 
files.",
-              identifier);
+              tableName);
         }
 
         // ---- Per-file phase: every failure below is one error row, never a 
failed bundle.
@@ -688,15 +759,15 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
           try {
             parquetFooter = ParquetFooters.read(filePath);
           } catch (Exception e) {
-            return errorResult(filePath, errorMessage(e), timestamp, window, 
paneInfo);
+            return errorResult(tableName, filePath, errorMessage(e), 
timestamp, window, paneInfo);
           }
         }
         Verdict verdict = Verdict.OK;
         if (evolution.isEnabled()) {
-          verdict = verify(filePath, format, parquetFooter);
+          verdict = verify(table, tableName, state, filePath, format, 
parquetFooter);
         }
         if (verdict.error != null) {
-          return errorResult(filePath, verdict.error, timestamp, window, 
paneInfo);
+          return errorResult(tableName, filePath, verdict.error, timestamp, 
window, paneInfo);
         }
 
         // What readers resolve a file without field ids with, so its metrics 
and partition agree
@@ -712,7 +783,7 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
             resolvedFooter = ParquetFieldIds.resolve(parquetFooter, table, 
mapping);
           } catch (ParquetFieldIds.ConflictException e) {
             return errorResult(
-                filePath, FIELD_ID_ERROR + e.getMessage(), timestamp, window, 
paneInfo);
+                tableName, filePath, FIELD_ID_ERROR + e.getMessage(), 
timestamp, window, paneInfo);
           }
         }
 
@@ -729,7 +800,7 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
                   table.schema(),
                   resolvedFooter);
         } catch (Exception e) {
-          return errorResult(filePath, errorMessage(e), timestamp, window, 
paneInfo);
+          return errorResult(tableName, filePath, errorMessage(e), timestamp, 
window, paneInfo);
         }
 
         // Figure out which partition this DataFile should go to
@@ -739,7 +810,7 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
         if (partitioned && !Strings.isNullOrEmpty(prefix)) {
           // option 1: use directory structure to determine partition
           // Note: we don't validate the DataFile content here
-          partitionPath = getPartitionFromFilePath(filePath);
+          partitionPath = getPartitionFromFilePath(table, filePath);
         } else if (partitioned) {
           try {
             // option 2: examine DataFile min/max statistics to determine 
partition
@@ -747,12 +818,22 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
                 getPartitionFromMetrics(metrics, inputFile, table, mapping, 
resolvedFooter);
           } catch (UnknownPartitionException e) {
             return errorResult(
-                filePath, UNKNOWN_PARTITION_ERROR + e.getMessage(), timestamp, 
window, paneInfo);
+                tableName,
+                filePath,
+                UNKNOWN_PARTITION_ERROR + e.getMessage(),
+                timestamp,
+                window,
+                paneInfo);
           } catch (RuntimeException e) {
             // e.g. a bound that does not decode as the table's type: one 
error row, not a failed
             // bundle
             return errorResult(
-                filePath, UNKNOWN_PARTITION_ERROR + errorMessage(e), 
timestamp, window, paneInfo);
+                tableName,
+                filePath,
+                UNKNOWN_PARTITION_ERROR + errorMessage(e),
+                timestamp,
+                window,
+                paneInfo);
           }
         }
 
@@ -771,6 +852,7 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
           }
           DataFile df = builder.build();
           return new ProcessResult(
+              tableName,
               SerializableDataFile.from(df, table.spec()),
               null,
               verdict.unverified,
@@ -779,7 +861,7 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
               paneInfo);
         } catch (Exception e) {
           // getLength is a per-file read (e.g. the file was deleted 
mid-flight).
-          return errorResult(filePath, errorMessage(e), timestamp, window, 
paneInfo);
+          return errorResult(tableName, filePath, errorMessage(e), timestamp, 
window, paneInfo);
         }
       };
     }
@@ -788,19 +870,25 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
      * The checks the options promise, in order: a format the checks can read, 
a convertible schema,
      * coverage by the table, pins. The first failure is the verdict.
      */
-    private Verdict verify(String filePath, FileFormat format, @Nullable 
ParquetMetadata footer) {
+    private Verdict verify(
+        Table table,
+        String tableName,
+        TableState state,
+        String filePath,
+        FileFormat format,
+        @Nullable ParquetMetadata footer) {
       if (!format.equals(FileFormat.PARQUET)) {
         if (evolution.getUnverifiableFileHandling() == 
UnverifiableFileHandling.REJECT) {
           return Verdict.error(UNCHECKED_FORMAT_ERROR + format.name());
         }
-        if (firstTime("unchecked format")) {
+        if (firstTime(tableName, "unchecked format")) {
           LOG.warn(
               "Registering {} files in table {} unchecked 
(UnverifiableFileHandling.ACCEPT):"
                   + " coverage and pin checks read only Parquet, so a required 
column such a file"
                   + " lacks or holds nulls in fails reads of the table, not 
registration. First"
                   + " file: {}",
               format,
-              identifier,
+              tableName,
               filePath);
         }
         return Verdict.unverified(Unverified.FORMAT);
@@ -812,12 +900,15 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
       } catch (Exception e) {
         return Verdict.error(UNREADABLE_SCHEMA_ERROR + errorMessage(e));
       }
-      Set<Integer> identityPartitionColumnsFromPath = 
identityPartitionColumnsFromPath(filePath);
-      @Nullable String uncovered = uncoveredReason(fileSchema, 
identityPartitionColumnsFromPath);
+      Set<Integer> identityPartitionColumnsFromPath =
+          identityPartitionColumnsFromPath(table, filePath);
+      @Nullable String uncovered =
+          uncoveredReason(table, state, fileSchema, 
identityPartitionColumnsFromPath);
       if (uncovered != null) {
         return Verdict.error(uncovered);
       }
-      return checkPins(filePath, fileSchema, parquetFooter, 
identityPartitionColumnsFromPath);
+      return checkPins(
+          table, tableName, filePath, fileSchema, parquetFooter, 
identityPartitionColumnsFromPath);
     }
 
     /**
@@ -825,8 +916,7 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
      * for a file under {@code dt=2024-01-15/} when the table is partitioned 
by {@code dt}. Empty
      * without a prefix.
      */
-    private Set<Integer> identityPartitionColumnsFromPath(String filePath) {
-      Table table = checkStateNotNull(this.table);
+    private Set<Integer> identityPartitionColumnsFromPath(Table table, String 
filePath) {
       if (Strings.isNullOrEmpty(prefix) || table.spec().isUnpartitioned()) {
         return Collections.emptySet();
       }
@@ -840,16 +930,18 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
      * was cached) and report the remaining delta. Never changes the schema.
      */
     private @Nullable String uncoveredReason(
-        org.apache.iceberg.Schema fileSchema, Set<Integer> 
identityPartitionColumnsFromPath) {
-      Table table = checkStateNotNull(this.table);
+        Table table,
+        TableState state,
+        org.apache.iceberg.Schema fileSchema,
+        Set<Integer> identityPartitionColumnsFromPath) {
       SchemaDelta delta = SchemaDelta.classify(table, fileSchema, 
identityPartitionColumnsFromPath);
       if (delta.isEmpty()) {
         return null;
       }
       // a commit can land after the table was cached; one refresh per bundle 
is enough to see it,
       // and a bundle full of routed files must not load the table once per 
file
-      if (refreshedThisBundle.compareAndSet(false, true)) {
-        synchronized (this) {
+      if (state.refreshedThisBundle.compareAndSet(false, true)) {
+        synchronized (state) {
           table.refresh();
         }
       }
@@ -874,11 +966,12 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
      * goes on, so a pin the footer does count nulls for still fails the file.
      */
     private Verdict checkPins(
+        Table table,
+        String tableName,
         String filePath,
         org.apache.iceberg.Schema fileSchema,
         ParquetMetadata footer,
         Set<Integer> identityPartitionColumnsFromPath) {
-      Table table = checkStateNotNull(this.table);
       List<String> unproven = new ArrayList<>();
       for (String pinned : evolution.getRequiredColumns()) {
         org.apache.iceberg.types.Types.@Nullable NestedField tableField =
@@ -910,12 +1003,12 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
       if (unproven.isEmpty()) {
         return Verdict.OK;
       }
-      if (firstTime("unproven pins")) {
+      if (firstTime(tableName, "unproven pins")) {
         LOG.warn(
             "Registering files in table {} whose footer has no null count 
statistics for pinned"
                 + " column(s) {} on trust (UnverifiableFileHandling.ACCEPT): 
nulls there fail"
                 + " reads of the table, not registration. First file: {}",
-            identifier,
+            tableName,
             unproven,
             filePath);
       }
@@ -923,13 +1016,19 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
     }
 
     /** Once per instance per key: at volume a line per file would drown the 
log. */
-    private boolean firstTime(String key) {
-      return checkStateNotNull(warned).add(key);
+    private boolean firstTime(String tableName, String key) {
+      return checkStateNotNull(warned).add(tableName + ":" + key);
     }
 
     private static ProcessResult errorResult(
-        String filePath, String message, Instant timestamp, BoundedWindow 
window, PaneInfo pane) {
+        String tableName,
+        String filePath,
+        String message,
+        Instant timestamp,
+        BoundedWindow window,
+        PaneInfo pane) {
       return new ProcessResult(
+          tableName,
           null,
           Row.withSchema(ERROR_SCHEMA).addValues(filePath, message).build(),
           null,
@@ -942,8 +1041,9 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
       return transform.bind(type).apply(Conversions.fromByteBuffer(type, 
bytes));
     }
 
-    private Table getOrCreateTable(String filePath, FileFormat format) throws 
IOException {
-      TableIdentifier tableId = IcebergUtils.parseTableIdentifier(identifier);
+    private Table getOrCreateTable(String tableName, String filePath, 
FileFormat format)
+        throws IOException {
+      TableIdentifier tableId = IcebergUtils.parseTableIdentifier(tableName);
       try {
         return catalogConfig.catalog().loadTable(tableId);
       } catch (NoSuchTableException e) {
@@ -973,7 +1073,7 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
               .create();
 
         } catch (AlreadyExistsException e2) { // if table already exists, just 
load it
-          return 
catalogConfig.catalog().loadTable(IcebergUtils.parseTableIdentifier(identifier));
+          return catalogConfig.catalog().loadTable(tableId);
         }
       }
     }
@@ -991,8 +1091,8 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
       return ParquetSchemaUtil.convert(messageType);
     }
 
-    private String getPartitionFromFilePath(String filePath) {
-      if (checkStateNotNull(table).spec().isUnpartitioned()) {
+    private String getPartitionFromFilePath(Table table, String filePath) {
+      if (table.spec().isUnpartitioned()) {
         return "";
       }
       return PartitionPaths.relative(filePath, checkStateNotNull(prefix));
@@ -1188,29 +1288,34 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
    * downstream {@link CommitManifestFilesDoFn}.
    */
   static class CreateManifests
-      extends DoFn<KV<Integer, Iterable<SerializableDataFile>>, KV<String, 
byte[]>> {
+      extends DoFn<KV<ManifestKey, Iterable<SerializableDataFile>>, KV<String, 
byte[]>> {
     private final IcebergCatalogConfig catalogConfig;
-    private final String identifier;
-    private transient @MonotonicNonNull Table table;
+    private transient @MonotonicNonNull Map<String, Table> tables;
 
-    public CreateManifests(IcebergCatalogConfig catalogConfig, String 
identifier) {
+    public CreateManifests(IcebergCatalogConfig catalogConfig) {
       this.catalogConfig = catalogConfig;
-      this.identifier = identifier;
     }
 
     @ProcessElement
     public void process(
-        @Element KV<Integer, Iterable<SerializableDataFile>> batch,
+        @Element KV<ManifestKey, Iterable<SerializableDataFile>> batch,
         OutputReceiver<KV<String, byte[]>> output)
         throws IOException {
       if (!batch.getValue().iterator().hasNext()) {
         return;
       }
+      String identifier = batch.getKey().getTable();
+      if (tables == null) {
+        tables = new HashMap<>();
+      }
+      Table table = tables.get(identifier);
       if (table == null) {
         table = 
catalogConfig.catalog().loadTable(IcebergUtils.parseTableIdentifier(identifier));
+        tables.put(identifier, table);
       }
 
-      PartitionSpec spec = 
checkStateNotNull(table.specs().get(batch.getKey()));
+      PartitionSpec spec =
+          
checkStateNotNull(table.specs().get(batch.getKey().getPartitionSpecId()));
 
       String manifestPath =
           String.format(
@@ -1258,17 +1363,15 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
    */
   static class CommitManifestFilesDoFn extends DoFn<KV<String, 
Iterable<byte[]>>, Row> {
     private final IcebergCatalogConfig catalogConfig;
-    private final String identifier;
-    private transient @MonotonicNonNull Table table = null;
+    private transient @MonotonicNonNull Map<String, Table> tables = null;
     private static final String COMMIT_ID_KEY = "beam.add-files-commit-id";
 
     @StateId("lastCommitTimestamp")
     private final StateSpec<ValueState<Long>> lastCommitTimestamp =
         StateSpecs.value(VarLongCoder.of());
 
-    public CommitManifestFilesDoFn(IcebergCatalogConfig catalogConfig, String 
identifier) {
+    public CommitManifestFilesDoFn(IcebergCatalogConfig catalogConfig) {
       this.catalogConfig = catalogConfig;
-      this.identifier = identifier;
     }
 
     private static void ensureNameMappingPresent(Table table) {
@@ -1303,13 +1406,19 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
         manifests.add(ManifestFiles.decode(bytes));
       }
       String commitId = commitHash(manifests);
+      String identifier = batch.getKey();
+      if (tables == null) {
+        tables = new HashMap<>();
+      }
+      Table table = tables.get(identifier);
       if (table == null) {
         table = 
catalogConfig.catalog().loadTable(IcebergUtils.parseTableIdentifier(identifier));
+        tables.put(identifier, table);
       }
       table.refresh();
       ensureNameMappingPresent(table);
 
-      if (shouldSkip(commitId, lastCommitTimestamp.read())) {
+      if (shouldSkip(table, commitId, lastCommitTimestamp.read())) {
         return;
       }
 
@@ -1349,11 +1458,10 @@ public class AddFiles extends 
PTransform<PCollection<String>, PCollectionRowTupl
      * Performs a look-back through Iceberg table history to determine if this 
specific batch of
      * files has already been successfully committed.
      */
-    private boolean shouldSkip(String commitUID, @Nullable Long 
lastCommitTimestamp) {
+    private boolean shouldSkip(Table table, String commitUID, @Nullable Long 
lastCommitTimestamp) {
       if (lastCommitTimestamp == null) {
         return false;
       }
-      Table table = checkStateNotNull(this.table);
 
       // check past snapshots to see if they contain the commit ID
       @Nullable Snapshot current = table.currentSnapshot();
diff --git 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ManifestKey.java
 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ManifestKey.java
new file mode 100644
index 00000000000..90a8d62942d
--- /dev/null
+++ 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ManifestKey.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.beam.sdk.io.iceberg;
+
+import com.google.auto.value.AutoValue;
+import org.apache.beam.sdk.schemas.AutoValueSchema;
+import org.apache.beam.sdk.schemas.annotations.DefaultSchema;
+import org.apache.beam.sdk.schemas.annotations.SchemaFieldNumber;
+
+/**
+ * As much as one manifest file can hold: entries for a single table, all 
written against a single
+ * partition spec. Data files are batched by this before a manifest is written 
for each batch.
+ *
+ * <p>A schema type rather than a {@code KV} of the two. The field numbers pin 
each field's place in
+ * the encoding, so a third field can be added after these two without moving 
them.
+ */
+@DefaultSchema(AutoValueSchema.class)
+@AutoValue
+abstract class ManifestKey {
+
+  static ManifestKey of(String table, int partitionSpecId) {
+    return new AutoValue_ManifestKey(table, partitionSpecId);
+  }
+
+  @SchemaFieldNumber("0")
+  abstract String getTable();
+
+  @SchemaFieldNumber("1")
+  abstract int getPartitionSpecId();
+}
diff --git 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFooterSchema.java
 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFooterSchema.java
index 37310b8d7f7..9f7eafb7455 100644
--- 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFooterSchema.java
+++ 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFooterSchema.java
@@ -28,6 +28,7 @@ import org.apache.beam.sdk.metrics.Counter;
 import org.apache.beam.sdk.transforms.DoFn;
 import org.apache.beam.sdk.transforms.windowing.BoundedWindow;
 import org.apache.beam.sdk.transforms.windowing.PaneInfo;
+import org.apache.beam.sdk.values.KV;
 import org.apache.iceberg.FileFormat;
 import org.apache.parquet.hadoop.metadata.ParquetMetadata;
 import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@@ -40,7 +41,8 @@ import org.slf4j.LoggerFactory;
  * Emits one {@link CollectDistinctSchemas.SchemaGroup} of one file per 
readable Parquet file: its
  * declared schema and its null-free columns. Unreadable or non-Parquet files 
contribute nothing.
  */
-class ReadFooterSchema extends DoFn<String, 
CollectDistinctSchemas.SchemaGroup> {
+class ReadFooterSchema
+    extends DoFn<KV<String, String>, KV<String, 
CollectDistinctSchemas.SchemaGroup>> {
   private static final Logger LOG = 
LoggerFactory.getLogger(ReadFooterSchema.class);
 
   static final int DEFAULT_THREAD_POOL_SIZE = 10;
@@ -94,6 +96,7 @@ class ReadFooterSchema extends DoFn<String, 
CollectDistinctSchemas.SchemaGroup>
 
   /** Counters are updated on the processing thread: metrics touched from the 
executor are lost. */
   private static class ReadResult {
+    final String table;
     final CollectDistinctSchemas.@Nullable SchemaGroup schema;
     final boolean footerError;
     final Instant timestamp;
@@ -101,11 +104,13 @@ class ReadFooterSchema extends DoFn<String, 
CollectDistinctSchemas.SchemaGroup>
     final PaneInfo paneInfo;
 
     ReadResult(
+        String table,
         CollectDistinctSchemas.@Nullable SchemaGroup schema,
         boolean footerError,
         Instant timestamp,
         BoundedWindow window,
         PaneInfo paneInfo) {
+      this.table = table;
       this.schema = schema;
       this.footerError = footerError;
       this.timestamp = timestamp;
@@ -134,14 +139,15 @@ class ReadFooterSchema extends DoFn<String, 
CollectDistinctSchemas.SchemaGroup>
 
   @ProcessElement
   public void process(
-      @Element String filePath,
+      @Element KV<String, String> element,
       @Timestamp Instant timestamp,
       BoundedWindow window,
       PaneInfo paneInfo,
-      OutputReceiver<CollectDistinctSchemas.SchemaGroup> output)
+      OutputReceiver<KV<String, CollectDistinctSchemas.SchemaGroup>> output)
       throws Exception {
     numFilesRead.inc();
-    Callable<ReadResult> task = createReadTask(filePath, timestamp, window, 
paneInfo);
+    Callable<ReadResult> task =
+        createReadTask(element.getKey(), element.getValue(), timestamp, 
window, paneInfo);
     checkStateNotNull(tasks).submit(task, result -> outputResult(result, 
output));
   }
 
@@ -153,16 +159,19 @@ class ReadFooterSchema extends DoFn<String, 
CollectDistinctSchemas.SchemaGroup>
   private static void outputAtFinish(ReadResult result, FinishBundleContext 
context) {
     count(result);
     if (result.schema != null) {
-      context.output(result.schema, result.timestamp, result.window);
+      context.output(KV.of(result.table, result.schema), result.timestamp, 
result.window);
     }
   }
 
   private static void outputResult(
-      ReadResult result, OutputReceiver<CollectDistinctSchemas.SchemaGroup> 
output) {
+      ReadResult result, OutputReceiver<KV<String, 
CollectDistinctSchemas.SchemaGroup>> output) {
     count(result);
     if (result.schema != null) {
       output.outputWindowedValue(
-          result.schema, result.timestamp, 
Collections.singleton(result.window), result.paneInfo);
+          KV.of(result.table, result.schema),
+          result.timestamp,
+          Collections.singleton(result.window),
+          result.paneInfo);
     }
   }
 
@@ -176,30 +185,30 @@ class ReadFooterSchema extends DoFn<String, 
CollectDistinctSchemas.SchemaGroup>
   }
 
   private Callable<ReadResult> createReadTask(
-      String filePath, Instant timestamp, BoundedWindow window, PaneInfo 
paneInfo) {
+      String table, String filePath, Instant timestamp, BoundedWindow window, 
PaneInfo paneInfo) {
     return () -> {
       ParquetMetadata footer;
       if (AddFiles.hasNoExtension(filePath)) {
         try {
           footer = ParquetFooters.read(filePath);
         } catch (Exception e) {
-          return unknownFormat(timestamp, window, paneInfo);
+          return unknownFormat(table, timestamp, window, paneInfo);
         }
       } else {
         FileFormat format;
         try {
           format = AddFiles.inferFormat(filePath);
         } catch (AddFiles.UnknownFormatException e) {
-          return unknownFormat(timestamp, window, paneInfo);
+          return unknownFormat(table, timestamp, window, paneInfo);
         }
         if (!format.equals(FileFormat.PARQUET)) {
           return new ReadResult(
-              dryRunMarker(UNCHECKED_FORMAT_KEY), false, timestamp, window, 
paneInfo);
+              table, dryRunMarker(UNCHECKED_FORMAT_KEY), false, timestamp, 
window, paneInfo);
         }
         try {
           footer = ParquetFooters.read(filePath);
         } catch (Exception e) {
-          return unreadableFooter(filePath, e, timestamp, window, paneInfo);
+          return unreadableFooter(table, filePath, e, timestamp, window, 
paneInfo);
         }
       }
       try {
@@ -207,33 +216,44 @@ class ReadFooterSchema extends DoFn<String, 
CollectDistinctSchemas.SchemaGroup>
             new ArrayList<>(PartitionPaths.partitionKeysInPath(filePath, 
locationPrefix));
         CollectDistinctSchemas.SchemaGroup group =
             
FileSchemas.schemaGroup(footer).withPartitionKeysInPath(partitionKeysInPath);
-        return new ReadResult(group, false, timestamp, window, paneInfo);
+        return new ReadResult(table, group, false, timestamp, window, 
paneInfo);
       } catch (Exception e) {
-        return unconvertibleSchema(filePath, e, timestamp, window, paneInfo);
+        return unconvertibleSchema(table, filePath, e, timestamp, window, 
paneInfo);
       }
     };
   }
 
-  private ReadResult unknownFormat(Instant timestamp, BoundedWindow window, 
PaneInfo paneInfo) {
-    return new ReadResult(dryRunMarker(UNREADABLE_KEY), false, timestamp, 
window, paneInfo);
+  private ReadResult unknownFormat(
+      String table, Instant timestamp, BoundedWindow window, PaneInfo 
paneInfo) {
+    return new ReadResult(table, dryRunMarker(UNREADABLE_KEY), false, 
timestamp, window, paneInfo);
   }
 
   private ReadResult unreadableFooter(
-      String filePath, Exception e, Instant timestamp, BoundedWindow window, 
PaneInfo paneInfo) {
+      String table,
+      String filePath,
+      Exception e,
+      Instant timestamp,
+      BoundedWindow window,
+      PaneInfo paneInfo) {
     LOG.warn(
         "Could not read the footer of {}; the file will not contribute to 
schema inference: {}",
         filePath,
         AddFiles.errorMessage(e));
-    return new ReadResult(dryRunMarker(UNREADABLE_KEY), true, timestamp, 
window, paneInfo);
+    return new ReadResult(table, dryRunMarker(UNREADABLE_KEY), true, 
timestamp, window, paneInfo);
   }
 
   private ReadResult unconvertibleSchema(
-      String filePath, Exception e, Instant timestamp, BoundedWindow window, 
PaneInfo paneInfo) {
+      String table,
+      String filePath,
+      Exception e,
+      Instant timestamp,
+      BoundedWindow window,
+      PaneInfo paneInfo) {
     LOG.warn(
         "Could not convert the schema of {}; the file will not contribute to 
schema inference: {}",
         filePath,
         AddFiles.errorMessage(e));
-    return new ReadResult(dryRunMarker(UNREADABLE_KEY), false, timestamp, 
window, paneInfo);
+    return new ReadResult(table, dryRunMarker(UNREADABLE_KEY), false, 
timestamp, window, paneInfo);
   }
 
   /** A dry run counts files that contribute no schema; a real run handles 
them at registration. */
diff --git 
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesTest.java
 
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesTest.java
index 47aa8290f72..64ea7e87382 100644
--- 
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesTest.java
+++ 
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesTest.java
@@ -52,12 +52,15 @@ import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 import org.apache.beam.sdk.Pipeline;
 import org.apache.beam.sdk.PipelineResult;
+import org.apache.beam.sdk.coders.KvCoder;
 import org.apache.beam.sdk.coders.StringUtf8Coder;
 import 
org.apache.beam.sdk.io.iceberg.SchemaEvolutionConfig.UnverifiableFileHandling;
 import org.apache.beam.sdk.metrics.MetricNameFilter;
 import org.apache.beam.sdk.metrics.MetricResult;
 import org.apache.beam.sdk.metrics.MetricsFilter;
 import org.apache.beam.sdk.runners.TransformHierarchy;
+import org.apache.beam.sdk.schemas.NoSuchSchemaException;
+import org.apache.beam.sdk.schemas.SchemaRegistry;
 import org.apache.beam.sdk.testing.ExpectedLogs;
 import org.apache.beam.sdk.testing.PAssert;
 import org.apache.beam.sdk.testing.TestPipeline;
@@ -65,6 +68,7 @@ import org.apache.beam.sdk.testing.TestStream;
 import org.apache.beam.sdk.transforms.Count;
 import org.apache.beam.sdk.transforms.Create;
 import org.apache.beam.sdk.transforms.ParDo;
+import org.apache.beam.sdk.transforms.WithKeys;
 import org.apache.beam.sdk.transforms.windowing.FixedWindows;
 import org.apache.beam.sdk.transforms.windowing.Window;
 import org.apache.beam.sdk.values.PCollection;
@@ -73,6 +77,7 @@ import org.apache.beam.sdk.values.PCollectionTuple;
 import org.apache.beam.sdk.values.Row;
 import org.apache.beam.sdk.values.TimestampedValue;
 import org.apache.beam.sdk.values.TupleTagList;
+import org.apache.beam.sdk.values.TypeDescriptors;
 import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap;
 import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables;
 import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists;
@@ -1857,14 +1862,27 @@ public class AddFilesTest {
     PCollectionTuple out =
         pipeline
             .apply("Create Input", Create.of(Arrays.asList(files)))
+            .apply(
+                "KeyByTable",
+                WithKeys.<String, String>of(tableId.toString())
+                    .withKeyType(TypeDescriptors.strings()))
             .apply(
                 ParDo.of(
                         new AddFiles.ConvertToDataFile(
-                            catalogConfig, tableId.toString(), null, null, 
null, null, config))
+                            catalogConfig, null, null, null, null, config))
                     .withOutputTags(
                         AddFiles.ConvertToDataFile.DATA_FILES,
                         TupleTagList.of(AddFiles.ConvertToDataFile.ERRORS)));
     
out.get(AddFiles.ConvertToDataFile.ERRORS).setRowSchema(AddFiles.ERROR_SCHEMA);
+    try {
+      out.get(AddFiles.ConvertToDataFile.DATA_FILES)
+          .setCoder(
+              KvCoder.of(
+                  StringUtf8Coder.of(),
+                  
SchemaRegistry.createDefault().getSchemaCoder(SerializableDataFile.class)));
+    } catch (NoSuchSchemaException e) {
+      throw new RuntimeException(e);
+    }
     return out;
   }
 
diff --git 
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/ReadFooterSchemaTest.java
 
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/ReadFooterSchemaTest.java
index ea5a6e76868..55b0e5fa7c7 100644
--- 
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/ReadFooterSchemaTest.java
+++ 
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/ReadFooterSchemaTest.java
@@ -32,6 +32,8 @@ import java.util.Collections;
 import java.util.List;
 import org.apache.avro.SchemaBuilder;
 import org.apache.beam.sdk.PipelineResult;
+import org.apache.beam.sdk.coders.KvCoder;
+import org.apache.beam.sdk.coders.StringUtf8Coder;
 import org.apache.beam.sdk.metrics.MetricNameFilter;
 import org.apache.beam.sdk.metrics.MetricQueryResults;
 import org.apache.beam.sdk.metrics.MetricResult;
@@ -40,7 +42,10 @@ import org.apache.beam.sdk.testing.PAssert;
 import org.apache.beam.sdk.testing.TestPipeline;
 import org.apache.beam.sdk.transforms.Create;
 import org.apache.beam.sdk.transforms.ParDo;
+import org.apache.beam.sdk.transforms.Values;
+import org.apache.beam.sdk.transforms.WithKeys;
 import org.apache.beam.sdk.values.PCollection;
+import org.apache.beam.sdk.values.TypeDescriptors;
 import org.apache.hadoop.fs.Path;
 import org.apache.iceberg.PartitionSpec;
 import org.apache.iceberg.Schema;
@@ -341,9 +346,15 @@ public class ReadFooterSchemaTest {
   }
 
   private PCollection<CollectDistinctSchemas.SchemaGroup> run(String... paths) 
{
+    // the read is keyed by destination table; these tests are about the 
schema, so one key
     return pipeline
         .apply(Create.of(Arrays.asList(paths)))
+        .apply(
+            "KeyByTable",
+            WithKeys.<String, 
String>of("db.t").withKeyType(TypeDescriptors.strings()))
         .apply(ParDo.of(new 
ReadFooterSchema(SchemaEvolutionConfig.disabled())))
+        .setCoder(KvCoder.of(StringUtf8Coder.of(), 
CollectDistinctSchemas.groupCoder()))
+        .apply("DropKey", Values.create())
         .setCoder(CollectDistinctSchemas.groupCoder());
   }
 
@@ -351,7 +362,12 @@ public class ReadFooterSchemaTest {
       String prefix, String... paths) {
     return pipeline
         .apply(Create.of(Arrays.asList(paths)))
+        .apply(
+            "KeyByTable",
+            WithKeys.<String, 
String>of("db.t").withKeyType(TypeDescriptors.strings()))
         .apply(ParDo.of(new ReadFooterSchema(SchemaEvolutionConfig.disabled(), 
prefix)))
+        .setCoder(KvCoder.of(StringUtf8Coder.of(), 
CollectDistinctSchemas.groupCoder()))
+        .apply("DropKey", Values.create())
         .setCoder(CollectDistinctSchemas.groupCoder());
   }
 

Reply via email to