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

jrmccluskey 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 333a7787e94 Implement Iceberg Side-Input Table Cache Integration with 
Fallback (#40080)
333a7787e94 is described below

commit 333a7787e94117440c82226a3acc3436ef338ac2
Author: Jack McCluskey <[email protected]>
AuthorDate: Wed Sep 16 09:57:24 2026 -0400

    Implement Iceberg Side-Input Table Cache Integration with Fallback (#40080)
    
    * Implement Iceberg Side-Input Table Cache Integration with Fallback
    
    * review comments
    
    * clean up extra identifier round trip
---
 .../iceberg/AssignDestinationsAndPartitions.java   |  75 +++-
 .../beam/sdk/io/iceberg/RecordWriterManager.java   |  56 ++-
 .../sdk/io/iceberg/WriteDirectRowsToFiles.java     |  44 ++-
 .../sdk/io/iceberg/WriteGroupedRowsToFiles.java    |  47 ++-
 .../io/iceberg/WritePartitionedRowsToFiles.java    |  66 +++-
 .../beam/sdk/io/iceberg/WriteToDestinations.java   |  31 +-
 .../beam/sdk/io/iceberg/WriteToPartitions.java     |  21 +-
 .../sdk/io/iceberg/WriteUngroupedRowsToFiles.java  |  72 +++-
 .../AssignDestinationsAndPartitionsTest.java       | 200 +++++++++++
 .../sdk/io/iceberg/RecordWriterManagerTest.java    | 104 ++++++
 .../sdk/io/iceberg/WriteWithMetadataViewTest.java  | 397 +++++++++++++++++++++
 11 files changed, 1062 insertions(+), 51 deletions(-)

diff --git 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitions.java
 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitions.java
index a744ff93097..4167ad00624 100644
--- 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitions.java
+++ 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitions.java
@@ -30,11 +30,13 @@ 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.beam.sdk.values.PCollection;
+import org.apache.beam.sdk.values.PCollectionView;
 import org.apache.beam.sdk.values.Row;
 import org.apache.beam.sdk.values.ValueInSingleWindow;
 import org.apache.iceberg.PartitionKey;
 import org.apache.iceberg.PartitionSpec;
 import org.apache.iceberg.Schema;
+import org.apache.iceberg.Table;
 import org.apache.iceberg.exceptions.NoSuchTableException;
 import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
 import org.checkerframework.checker.nullness.qual.Nullable;
@@ -51,6 +53,7 @@ class AssignDestinationsAndPartitions
 
   private final DynamicDestinations dynamicDestinations;
   private final IcebergCatalogConfig catalogConfig;
+  private final @Nullable PCollectionView<Map<String, SerializableTableSpec>> 
metadataView;
 
   static final String DESTINATION = "destination";
   static final String PARTITION = "partition";
@@ -63,14 +66,27 @@ class AssignDestinationsAndPartitions
 
   public AssignDestinationsAndPartitions(
       DynamicDestinations dynamicDestinations, IcebergCatalogConfig 
catalogConfig) {
+    this(dynamicDestinations, catalogConfig, null);
+  }
+
+  public AssignDestinationsAndPartitions(
+      DynamicDestinations dynamicDestinations,
+      IcebergCatalogConfig catalogConfig,
+      @Nullable PCollectionView<Map<String, SerializableTableSpec>> 
metadataView) {
     this.dynamicDestinations = dynamicDestinations;
     this.catalogConfig = catalogConfig;
+    this.metadataView = metadataView;
   }
 
   @Override
   public PCollection<KV<Row, Row>> expand(PCollection<Row> input) {
+    ParDo.SingleOutput<Row, KV<Row, Row>> parDo =
+        ParDo.of(new AssignDoFn(dynamicDestinations, catalogConfig, 
metadataView));
+    if (metadataView != null) {
+      parDo = parDo.withSideInputs(metadataView);
+    }
     return input
-        .apply(ParDo.of(new AssignDoFn(dynamicDestinations, catalogConfig)))
+        .apply(parDo)
         .setCoder(
             KvCoder.of(
                 RowCoder.of(OUTPUT_SCHEMA), 
RowCoder.of(dynamicDestinations.getDataSchema())));
@@ -83,13 +99,23 @@ class AssignDestinationsAndPartitions
     private transient @MonotonicNonNull Map<String, PartitionKey> 
partitionKeys;
     private transient @MonotonicNonNull Map<String, BeamRowWrapper> wrappers;
     private transient @MonotonicNonNull Map<String, Instant> lastRefreshTimes;
+    private transient @MonotonicNonNull Map<String, Integer> cachedSpecIds;
 
     private final DynamicDestinations dynamicDestinations;
     private final IcebergCatalogConfig catalogConfig;
+    private final @Nullable PCollectionView<Map<String, 
SerializableTableSpec>> metadataView;
 
     AssignDoFn(DynamicDestinations dynamicDestinations, IcebergCatalogConfig 
catalogConfig) {
+      this(dynamicDestinations, catalogConfig, null);
+    }
+
+    AssignDoFn(
+        DynamicDestinations dynamicDestinations,
+        IcebergCatalogConfig catalogConfig,
+        @Nullable PCollectionView<Map<String, SerializableTableSpec>> 
metadataView) {
       this.dynamicDestinations = dynamicDestinations;
       this.catalogConfig = catalogConfig;
+      this.metadataView = metadataView;
     }
 
     @Setup
@@ -97,10 +123,12 @@ class AssignDestinationsAndPartitions
       this.wrappers = new HashMap<>();
       this.partitionKeys = new HashMap<>();
       this.lastRefreshTimes = new HashMap<>();
+      this.cachedSpecIds = new HashMap<>();
     }
 
     @ProcessElement
     public void processElement(
+        ProcessContext c,
         @Element Row element,
         BoundedWindow window,
         PaneInfo paneInfo,
@@ -111,58 +139,71 @@ class AssignDestinationsAndPartitions
           dynamicDestinations.getTableStringIdentifier(
               ValueInSingleWindow.of(element, timestamp, window, paneInfo));
 
+      SerializableTableSpec tableSpec = null;
+      if (metadataView != null) {
+        Map<String, SerializableTableSpec> viewMap = c.sideInput(metadataView);
+        if (viewMap != null) {
+          tableSpec = viewMap.get(tableIdentifier);
+        }
+      }
+
       Row data = dynamicDestinations.getData(element);
 
       @Nullable PartitionKey partitionKey = 
checkStateNotNull(partitionKeys).get(tableIdentifier);
-
       @Nullable BeamRowWrapper wrapper = 
checkStateNotNull(wrappers).get(tableIdentifier);
-
       @Nullable Instant lastRefresh = 
checkStateNotNull(lastRefreshTimes).get(tableIdentifier);
+      @Nullable Integer cachedSpecId = 
checkStateNotNull(cachedSpecIds).get(tableIdentifier);
 
       Instant now = Instant.now();
 
+      boolean specChanged =
+          tableSpec != null
+              && (cachedSpecId == null || 
!cachedSpecId.equals(tableSpec.getSpecId()));
+
       boolean shouldRefresh =
           partitionKey == null
               || wrapper == null
-              || lastRefresh == null
-              || now.isAfter(lastRefresh.plus(REFRESH_INTERVAL));
+              || specChanged
+              || (tableSpec == null
+                  && (lastRefresh == null || 
now.isAfter(lastRefresh.plus(REFRESH_INTERVAL))));
 
       if (shouldRefresh) {
 
         PartitionSpec spec = PartitionSpec.unpartitioned();
-
         Schema schema = 
IcebergUtils.beamSchemaToIcebergSchema(data.getSchema());
 
         @Nullable IcebergTableCreateConfig createConfig =
             
dynamicDestinations.instantiateDestination(tableIdentifier).getTableCreateConfig();
 
-        if (createConfig != null && createConfig.getPartitionFields() != null) 
{
-
+        if (tableSpec != null) {
+          spec = tableSpec.getPartitionSpec();
+          if (data.getSchema().getFieldCount() == 
tableSpec.getSchema().columns().size()) {
+            schema = tableSpec.getSchema();
+          }
+          checkStateNotNull(cachedSpecIds).put(tableIdentifier, 
tableSpec.getSpecId());
+        } else if (createConfig != null && createConfig.getPartitionFields() 
!= null) {
           spec =
               
PartitionUtils.toPartitionSpec(createConfig.getPartitionFields(), 
data.getSchema());
-
         } else {
-
           try {
             // see if table already exists with a spec
-            spec =
+            Table table =
                 TableCache.getAndRefreshIfStale(
-                        catalogConfig, 
IcebergUtils.parseTableIdentifier(tableIdentifier))
-                    .spec();
-
+                    catalogConfig, 
IcebergUtils.parseTableIdentifier(tableIdentifier));
+            spec = table.spec();
+            if (data.getSchema().getFieldCount() == 
table.schema().columns().size()) {
+              schema = table.schema();
+            }
           } catch (NoSuchTableException ignored) {
             // no partition to apply
           }
         }
 
         partitionKey = new PartitionKey(spec, schema);
-
         wrapper = new BeamRowWrapper(data.getSchema(), schema.asStruct());
 
         checkStateNotNull(partitionKeys).put(tableIdentifier, partitionKey);
-
         checkStateNotNull(wrappers).put(tableIdentifier, wrapper);
-
         checkStateNotNull(lastRefreshTimes).put(tableIdentifier, now);
       }
 
diff --git 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriterManager.java
 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriterManager.java
index 0995e7a6102..25e5a13da43 100644
--- 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriterManager.java
+++ 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriterManager.java
@@ -27,6 +27,7 @@ import java.time.ZoneOffset;
 import java.time.format.DateTimeFormatter;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
@@ -251,6 +252,7 @@ class RecordWriterManager implements AutoCloseable {
   private final long maxFileSize;
   private final int maxNumWriters;
   private final @Nullable Map<String, String> writeProperties;
+  private volatile @Nullable Map<String, SerializableTableSpec> 
sideInputTableSpecs;
   @VisibleForTesting int openWriters = 0;
 
   @VisibleForTesting
@@ -263,7 +265,7 @@ class RecordWriterManager implements AutoCloseable {
 
   RecordWriterManager(
       IcebergCatalogConfig catalogConfig, String filePrefix, long maxFileSize, 
int maxNumWriters) {
-    this(catalogConfig, filePrefix, maxFileSize, maxNumWriters, null);
+    this(catalogConfig, filePrefix, maxFileSize, maxNumWriters, null, null);
   }
 
   RecordWriterManager(
@@ -272,11 +274,27 @@ class RecordWriterManager implements AutoCloseable {
       long maxFileSize,
       int maxNumWriters,
       @Nullable Map<String, String> writeProperties) {
+    this(catalogConfig, filePrefix, maxFileSize, maxNumWriters, 
writeProperties, null);
+  }
+
+  RecordWriterManager(
+      IcebergCatalogConfig catalogConfig,
+      String filePrefix,
+      long maxFileSize,
+      int maxNumWriters,
+      @Nullable Map<String, String> writeProperties,
+      @Nullable Map<String, SerializableTableSpec> sideInputTableSpecs) {
     this.catalogConfig = catalogConfig;
     this.filePrefix = filePrefix;
     this.maxFileSize = maxFileSize;
     this.maxNumWriters = maxNumWriters;
     this.writeProperties = writeProperties;
+    this.sideInputTableSpecs = sideInputTableSpecs;
+  }
+
+  @VisibleForTesting
+  void setSideInputTableSpecs(@Nullable Map<String, SerializableTableSpec> 
sideInputTableSpecs) {
+    this.sideInputTableSpecs = sideInputTableSpecs;
   }
 
   /**
@@ -291,12 +309,29 @@ class RecordWriterManager implements AutoCloseable {
    * using the Iceberg API.
    */
   @VisibleForTesting
-  Table getOrCreateTable(IcebergDestination destination, Schema dataSchema) {
+  Table getOrCreateTable(
+      IcebergDestination destination,
+      Schema dataSchema,
+      @Nullable Map<String, SerializableTableSpec> sideInputTableSpecs) {
     TableIdentifier identifier = destination.getTableIdentifier();
+    String tableIdString = IcebergUtils.tableIdentifierToString(identifier);
+    if (sideInputTableSpecs != null && 
sideInputTableSpecs.containsKey(tableIdString)) {
+      SerializableTableSpec spec = sideInputTableSpecs.get(tableIdString);
+      if (spec != null) {
+        Map<String, String> catalogProperties = 
catalogConfig.getCatalogProperties();
+        return new SideInputTable(
+            spec, catalogProperties != null ? catalogProperties : 
Collections.emptyMap());
+      }
+    }
     return TableCache.getAndRefreshIfStale(
         catalogConfig, identifier, () -> loadOrCreateTable(destination, 
dataSchema));
   }
 
+  @VisibleForTesting
+  Table getOrCreateTable(IcebergDestination destination, Schema dataSchema) {
+    return getOrCreateTable(destination, dataSchema, this.sideInputTableSpecs);
+  }
+
   private Table loadOrCreateTable(IcebergDestination destination, Schema 
dataSchema) {
     Catalog catalog = catalogConfig.catalog();
     TableIdentifier identifier = destination.getTableIdentifier();
@@ -354,6 +389,23 @@ class RecordWriterManager implements AutoCloseable {
     }
   }
 
+  /**
+   * Fetches the appropriate {@link RecordWriter} for this destination and 
partition and writes the
+   * record, optionally updating the side-input table specs map.
+   *
+   * <p>If the {@link RecordWriterManager} is saturated (i.e. has hit the 
maximum limit of open
+   * writers), the record is rejected and {@code false} is returned.
+   */
+  public boolean write(
+      WindowedValue<IcebergDestination> icebergDestination,
+      Row row,
+      @Nullable Map<String, SerializableTableSpec> sideInputTableSpecs) {
+    if (sideInputTableSpecs != null) {
+      this.sideInputTableSpecs = sideInputTableSpecs;
+    }
+    return write(icebergDestination, row);
+  }
+
   /**
    * Fetches the appropriate {@link RecordWriter} for this destination and 
partition and writes the
    * record.
diff --git 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteDirectRowsToFiles.java
 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteDirectRowsToFiles.java
index e03085e6be7..8bafe9eeaef 100644
--- 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteDirectRowsToFiles.java
+++ 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteDirectRowsToFiles.java
@@ -26,6 +26,7 @@ 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.beam.sdk.values.PCollection;
+import org.apache.beam.sdk.values.PCollectionView;
 import org.apache.beam.sdk.values.Row;
 import org.apache.beam.sdk.values.WindowedValue;
 import org.apache.beam.sdk.values.WindowedValues;
@@ -41,6 +42,7 @@ class WriteDirectRowsToFiles
   private final String filePrefix;
   private final long maxBytesPerFile;
   private final @Nullable Map<String, String> writeProperties;
+  private final @Nullable PCollectionView<Map<String, SerializableTableSpec>> 
metadataView;
 
   WriteDirectRowsToFiles(
       IcebergCatalogConfig catalogConfig,
@@ -48,19 +50,39 @@ class WriteDirectRowsToFiles
       String filePrefix,
       long maxBytesPerFile,
       @Nullable Map<String, String> writeProperties) {
+    this(catalogConfig, dynamicDestinations, filePrefix, maxBytesPerFile, 
writeProperties, null);
+  }
+
+  WriteDirectRowsToFiles(
+      IcebergCatalogConfig catalogConfig,
+      DynamicDestinations dynamicDestinations,
+      String filePrefix,
+      long maxBytesPerFile,
+      @Nullable Map<String, String> writeProperties,
+      @Nullable PCollectionView<Map<String, SerializableTableSpec>> 
metadataView) {
     this.catalogConfig = catalogConfig;
     this.dynamicDestinations = dynamicDestinations;
     this.filePrefix = filePrefix;
     this.maxBytesPerFile = maxBytesPerFile;
     this.writeProperties = writeProperties;
+    this.metadataView = metadataView;
   }
 
   @Override
   public PCollection<FileWriteResult> expand(PCollection<KV<String, Row>> 
input) {
-    return input.apply(
+    ParDo.SingleOutput<KV<String, Row>, FileWriteResult> parDo =
         ParDo.of(
             new WriteDirectRowsToFilesDoFn(
-                catalogConfig, dynamicDestinations, maxBytesPerFile, 
filePrefix, writeProperties)));
+                catalogConfig,
+                dynamicDestinations,
+                maxBytesPerFile,
+                filePrefix,
+                writeProperties,
+                metadataView));
+    if (metadataView != null) {
+      parDo = parDo.withSideInputs(metadataView);
+    }
+    return input.apply(parDo);
   }
 
   private static class WriteDirectRowsToFilesDoFn extends DoFn<KV<String, 
Row>, FileWriteResult> {
@@ -70,6 +92,7 @@ class WriteDirectRowsToFiles
     private final String filePrefix;
     private final long maxFileSize;
     private final @Nullable Map<String, String> writeProperties;
+    private final @Nullable PCollectionView<Map<String, 
SerializableTableSpec>> metadataView;
     private transient @Nullable RecordWriterManager recordWriterManager;
 
     WriteDirectRowsToFilesDoFn(
@@ -78,11 +101,22 @@ class WriteDirectRowsToFiles
         long maxFileSize,
         String filePrefix,
         @Nullable Map<String, String> writeProperties) {
+      this(catalogConfig, dynamicDestinations, maxFileSize, filePrefix, 
writeProperties, null);
+    }
+
+    WriteDirectRowsToFilesDoFn(
+        IcebergCatalogConfig catalogConfig,
+        DynamicDestinations dynamicDestinations,
+        long maxFileSize,
+        String filePrefix,
+        @Nullable Map<String, String> writeProperties,
+        @Nullable PCollectionView<Map<String, SerializableTableSpec>> 
metadataView) {
       this.catalogConfig = catalogConfig;
       this.dynamicDestinations = dynamicDestinations;
       this.filePrefix = filePrefix;
       this.maxFileSize = maxFileSize;
       this.writeProperties = writeProperties;
+      this.metadataView = metadataView;
       this.recordWriterManager = null;
     }
 
@@ -95,7 +129,7 @@ class WriteDirectRowsToFiles
 
     @ProcessElement
     public void processElement(
-        @SuppressWarnings("unused") ProcessContext context,
+        ProcessContext context,
         @Element KV<String, Row> element,
         BoundedWindow window,
         PaneInfo paneInfo)
@@ -104,8 +138,10 @@ class WriteDirectRowsToFiles
       IcebergDestination destination = 
dynamicDestinations.instantiateDestination(tableIdentifier);
       WindowedValue<IcebergDestination> windowedDestination =
           WindowedValues.of(destination, window.maxTimestamp(), window, 
paneInfo);
+      Map<String, SerializableTableSpec> sideInputs =
+          metadataView != null ? context.sideInput(metadataView) : null;
       Preconditions.checkNotNull(recordWriterManager)
-          .write(windowedDestination, element.getValue());
+          .write(windowedDestination, element.getValue(), sideInputs);
     }
 
     @FinishBundle
diff --git 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteGroupedRowsToFiles.java
 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteGroupedRowsToFiles.java
index e74715a7eeb..a2ed40c87f9 100644
--- 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteGroupedRowsToFiles.java
+++ 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteGroupedRowsToFiles.java
@@ -27,6 +27,7 @@ import org.apache.beam.sdk.transforms.windowing.PaneInfo;
 import org.apache.beam.sdk.util.ShardedKey;
 import org.apache.beam.sdk.values.KV;
 import org.apache.beam.sdk.values.PCollection;
+import org.apache.beam.sdk.values.PCollectionView;
 import org.apache.beam.sdk.values.Row;
 import org.apache.beam.sdk.values.WindowedValue;
 import org.apache.beam.sdk.values.WindowedValues;
@@ -42,6 +43,7 @@ class WriteGroupedRowsToFiles
   private final IcebergCatalogConfig catalogConfig;
   private final String filePrefix;
   private final @Nullable Map<String, String> writeProperties;
+  private final @Nullable PCollectionView<Map<String, SerializableTableSpec>> 
metadataView;
 
   WriteGroupedRowsToFiles(
       IcebergCatalogConfig catalogConfig,
@@ -49,20 +51,40 @@ class WriteGroupedRowsToFiles
       String filePrefix,
       long maxBytesPerFile,
       @Nullable Map<String, String> writeProperties) {
+    this(catalogConfig, dynamicDestinations, filePrefix, maxBytesPerFile, 
writeProperties, null);
+  }
+
+  WriteGroupedRowsToFiles(
+      IcebergCatalogConfig catalogConfig,
+      DynamicDestinations dynamicDestinations,
+      String filePrefix,
+      long maxBytesPerFile,
+      @Nullable Map<String, String> writeProperties,
+      @Nullable PCollectionView<Map<String, SerializableTableSpec>> 
metadataView) {
     this.catalogConfig = catalogConfig;
     this.dynamicDestinations = dynamicDestinations;
     this.filePrefix = filePrefix;
     this.maxBytesPerFile = maxBytesPerFile;
     this.writeProperties = writeProperties;
+    this.metadataView = metadataView;
   }
 
   @Override
   public PCollection<FileWriteResult> expand(
       PCollection<KV<ShardedKey<String>, Iterable<Row>>> input) {
-    return input.apply(
+    ParDo.SingleOutput<KV<ShardedKey<String>, Iterable<Row>>, FileWriteResult> 
parDo =
         ParDo.of(
             new WriteGroupedRowsToFilesDoFn(
-                catalogConfig, dynamicDestinations, maxBytesPerFile, 
filePrefix, writeProperties)));
+                catalogConfig,
+                dynamicDestinations,
+                maxBytesPerFile,
+                filePrefix,
+                writeProperties,
+                metadataView));
+    if (metadataView != null) {
+      parDo = parDo.withSideInputs(metadataView);
+    }
+    return input.apply(parDo);
   }
 
   private static class WriteGroupedRowsToFilesDoFn
@@ -73,6 +95,7 @@ class WriteGroupedRowsToFiles
     private final String filePrefix;
     private final long maxFileSize;
     private final @Nullable Map<String, String> writeProperties;
+    private final @Nullable PCollectionView<Map<String, 
SerializableTableSpec>> metadataView;
 
     WriteGroupedRowsToFilesDoFn(
         IcebergCatalogConfig catalogConfig,
@@ -80,11 +103,22 @@ class WriteGroupedRowsToFiles
         long maxFileSize,
         String filePrefix,
         @Nullable Map<String, String> writeProperties) {
+      this(catalogConfig, dynamicDestinations, maxFileSize, filePrefix, 
writeProperties, null);
+    }
+
+    WriteGroupedRowsToFilesDoFn(
+        IcebergCatalogConfig catalogConfig,
+        DynamicDestinations dynamicDestinations,
+        long maxFileSize,
+        String filePrefix,
+        @Nullable Map<String, String> writeProperties,
+        @Nullable PCollectionView<Map<String, SerializableTableSpec>> 
metadataView) {
       this.catalogConfig = catalogConfig;
       this.dynamicDestinations = dynamicDestinations;
       this.filePrefix = filePrefix;
       this.maxFileSize = maxFileSize;
       this.writeProperties = writeProperties;
+      this.metadataView = metadataView;
     }
 
     @ProcessElement
@@ -99,10 +133,17 @@ class WriteGroupedRowsToFiles
       IcebergDestination destination = 
dynamicDestinations.instantiateDestination(tableIdentifier);
       WindowedValue<IcebergDestination> windowedDestination =
           WindowedValues.of(destination, window.maxTimestamp(), window, 
paneInfo);
+      Map<String, SerializableTableSpec> sideInputs =
+          metadataView != null ? c.sideInput(metadataView) : null;
       RecordWriterManager writer;
       try (RecordWriterManager openWriter =
           new RecordWriterManager(
-              catalogConfig, filePrefix, maxFileSize, Integer.MAX_VALUE, 
writeProperties)) {
+              catalogConfig,
+              filePrefix,
+              maxFileSize,
+              Integer.MAX_VALUE,
+              writeProperties,
+              sideInputs)) {
         writer = openWriter;
         for (Row e : element.getValue()) {
           writer.write(windowedDestination, e);
diff --git 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WritePartitionedRowsToFiles.java
 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WritePartitionedRowsToFiles.java
index 338a2162080..881d2577fad 100644
--- 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WritePartitionedRowsToFiles.java
+++ 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WritePartitionedRowsToFiles.java
@@ -22,6 +22,7 @@ import static 
org.apache.beam.sdk.io.iceberg.AssignDestinationsAndPartitions.PAR
 import static 
org.apache.beam.sdk.io.iceberg.RecordWriterManager.getPartitionDataPath;
 import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull;
 
+import java.util.Collections;
 import java.util.Map;
 import java.util.UUID;
 import org.apache.beam.sdk.coders.IterableCoder;
@@ -33,6 +34,7 @@ import org.apache.beam.sdk.transforms.PTransform;
 import org.apache.beam.sdk.transforms.ParDo;
 import org.apache.beam.sdk.values.KV;
 import org.apache.beam.sdk.values.PCollection;
+import org.apache.beam.sdk.values.PCollectionView;
 import org.apache.beam.sdk.values.Row;
 import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps;
 import org.apache.iceberg.DataFiles;
@@ -61,16 +63,27 @@ class WritePartitionedRowsToFiles
   private final IcebergCatalogConfig catalogConfig;
   private final String filePrefix;
   private final @Nullable Map<String, String> writeProperties;
+  private final @Nullable PCollectionView<Map<String, SerializableTableSpec>> 
metadataView;
 
   WritePartitionedRowsToFiles(
       IcebergCatalogConfig catalogConfig,
       DynamicDestinations dynamicDestinations,
       String filePrefix,
       @Nullable Map<String, String> writeProperties) {
+    this(catalogConfig, dynamicDestinations, filePrefix, writeProperties, 
null);
+  }
+
+  WritePartitionedRowsToFiles(
+      IcebergCatalogConfig catalogConfig,
+      DynamicDestinations dynamicDestinations,
+      String filePrefix,
+      @Nullable Map<String, String> writeProperties,
+      @Nullable PCollectionView<Map<String, SerializableTableSpec>> 
metadataView) {
     this.catalogConfig = catalogConfig;
     this.dynamicDestinations = dynamicDestinations;
     this.filePrefix = filePrefix;
     this.writeProperties = writeProperties;
+    this.metadataView = metadataView;
   }
 
   @Override
@@ -81,10 +94,19 @@ class WritePartitionedRowsToFiles
                         ((KvCoder<Row, Iterable<Row>>) 
input.getCoder()).getValueCoder())
                     .getElemCoder())
             .getSchema();
-    return input.apply(
+    ParDo.SingleOutput<KV<Row, Iterable<Row>>, FileWriteResult> parDo =
         ParDo.of(
             new WriteDoFn(
-                catalogConfig, dynamicDestinations, filePrefix, dataSchema, 
writeProperties)));
+                catalogConfig,
+                dynamicDestinations,
+                filePrefix,
+                dataSchema,
+                writeProperties,
+                metadataView));
+    if (metadataView != null) {
+      parDo = parDo.withSideInputs(metadataView);
+    }
+    return input.apply(parDo);
   }
 
   private static class WriteDoFn extends DoFn<KV<Row, Iterable<Row>>, 
FileWriteResult> {
@@ -94,6 +116,7 @@ class WritePartitionedRowsToFiles
     private final String filePrefix;
     private final Schema dataSchema;
     private final @Nullable Map<String, String> writeProperties;
+    private final @Nullable PCollectionView<Map<String, 
SerializableTableSpec>> metadataView;
     private transient @MonotonicNonNull Map<TableIdentifier, Integer> specIds;
     private transient @MonotonicNonNull Map<TableIdentifier, Map<String, 
PartitionField>>
         partitionFieldMaps;
@@ -104,11 +127,22 @@ class WritePartitionedRowsToFiles
         String filePrefix,
         Schema dataSchema,
         @Nullable Map<String, String> writeProperties) {
+      this(catalogConfig, dynamicDestinations, filePrefix, dataSchema, 
writeProperties, null);
+    }
+
+    WriteDoFn(
+        IcebergCatalogConfig catalogConfig,
+        DynamicDestinations dynamicDestinations,
+        String filePrefix,
+        Schema dataSchema,
+        @Nullable Map<String, String> writeProperties,
+        @Nullable PCollectionView<Map<String, SerializableTableSpec>> 
metadataView) {
       this.catalogConfig = catalogConfig;
       this.dynamicDestinations = dynamicDestinations;
       this.filePrefix = filePrefix;
       this.dataSchema = dataSchema;
       this.writeProperties = writeProperties;
+      this.metadataView = metadataView;
     }
 
     @Setup
@@ -119,13 +153,17 @@ class WritePartitionedRowsToFiles
 
     @ProcessElement
     public void processElement(
-        @Element KV<Row, Iterable<Row>> element, 
OutputReceiver<FileWriteResult> out)
+        ProcessContext c,
+        @Element KV<Row, Iterable<Row>> element,
+        OutputReceiver<FileWriteResult> out)
         throws Exception {
       String tableIdentifier = 
checkStateNotNull(element.getKey().getString(DESTINATION));
       String partitionPath = 
checkStateNotNull(element.getKey().getString(PARTITION));
 
       IcebergDestination destination = 
dynamicDestinations.instantiateDestination(tableIdentifier);
-      Table table = getOrCreateTable(destination, dataSchema);
+      Map<String, SerializableTableSpec> sideInputs =
+          metadataView != null ? c.sideInput(metadataView) : null;
+      Table table = getOrCreateTable(destination, dataSchema, sideInputs);
       partitionPath =
           getPartitionDataPath(
               partitionPath, 
getPartitionFieldMap(destination.getTableIdentifier(), table));
@@ -176,14 +214,30 @@ class WritePartitionedRowsToFiles
       return partitionFieldMap;
     }
 
-    Table getOrCreateTable(IcebergDestination destination, Schema dataSchema) {
+    Table getOrCreateTable(
+        IcebergDestination destination,
+        Schema dataSchema,
+        @Nullable Map<String, SerializableTableSpec> sideInputTableSpecs) {
       TableIdentifier identifier = destination.getTableIdentifier();
+      String tableIdString = IcebergUtils.tableIdentifierToString(identifier);
+      if (sideInputTableSpecs != null && 
sideInputTableSpecs.containsKey(tableIdString)) {
+        SerializableTableSpec spec = sideInputTableSpecs.get(tableIdString);
+        if (spec != null) {
+          Map<String, String> catalogProperties = 
catalogConfig.getCatalogProperties();
+          return new SideInputTable(
+              spec, catalogProperties != null ? catalogProperties : 
Collections.emptyMap());
+        }
+      }
       return TableCache.getAndRefreshIfStale(
           catalogConfig,
           identifier,
           () -> loadOrCreateTable(catalogConfig.catalog(), destination, 
dataSchema));
     }
 
+    Table getOrCreateTable(IcebergDestination destination, Schema dataSchema) {
+      return getOrCreateTable(destination, dataSchema, null);
+    }
+
     private Table loadOrCreateTable(
         Catalog catalog, IcebergDestination destination, Schema dataSchema) {
       TableIdentifier identifier = destination.getTableIdentifier();
@@ -207,7 +261,7 @@ class WritePartitionedRowsToFiles
             LOG.info("Created new namespace '{}'.", namespace);
           } catch (AlreadyExistsException ignored) {
             // race condition: another worker already created this namespace
-            LOG.info("Namespace `{}` already exists.", namespace);
+            LOG.info("Namespace '{}' already exists.", namespace);
           }
         }
       }
diff --git 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteToDestinations.java
 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteToDestinations.java
index 684ef350a20..6d8f08a2ae5 100644
--- 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteToDestinations.java
+++ 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteToDestinations.java
@@ -41,6 +41,7 @@ import org.apache.beam.sdk.values.KV;
 import org.apache.beam.sdk.values.PCollection;
 import org.apache.beam.sdk.values.PCollectionList;
 import org.apache.beam.sdk.values.PCollectionTuple;
+import org.apache.beam.sdk.values.PCollectionView;
 import org.apache.beam.sdk.values.Row;
 import org.apache.beam.sdk.values.TupleTag;
 import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions;
@@ -60,6 +61,7 @@ class WriteToDestinations extends 
PTransform<PCollection<KV<String, Row>>, Icebe
   private final String filePrefix;
   private final @Nullable Integer directWriteByteLimit;
   private final @Nullable Map<String, String> writeProperties;
+  private final @Nullable PCollectionView<Map<String, SerializableTableSpec>> 
metadataView;
 
   WriteToDestinations(
       IcebergCatalogConfig catalogConfig,
@@ -67,11 +69,28 @@ class WriteToDestinations extends 
PTransform<PCollection<KV<String, Row>>, Icebe
       @Nullable Duration triggeringFrequency,
       @Nullable Integer directWriteByteLimit,
       @Nullable Map<String, String> writeProperties) {
+    this(
+        catalogConfig,
+        dynamicDestinations,
+        triggeringFrequency,
+        directWriteByteLimit,
+        writeProperties,
+        null);
+  }
+
+  WriteToDestinations(
+      IcebergCatalogConfig catalogConfig,
+      DynamicDestinations dynamicDestinations,
+      @Nullable Duration triggeringFrequency,
+      @Nullable Integer directWriteByteLimit,
+      @Nullable Map<String, String> writeProperties,
+      @Nullable PCollectionView<Map<String, SerializableTableSpec>> 
metadataView) {
     this.dynamicDestinations = dynamicDestinations;
     this.catalogConfig = catalogConfig;
     this.triggeringFrequency = triggeringFrequency;
     this.directWriteByteLimit = directWriteByteLimit;
     this.writeProperties = writeProperties;
+    this.metadataView = metadataView;
     // single unique prefix per write transform
     this.filePrefix = UUID.randomUUID().toString();
   }
@@ -119,7 +138,8 @@ class WriteToDestinations extends 
PTransform<PCollection<KV<String, Row>>, Icebe
             dynamicDestinations,
             filePrefix,
             DEFAULT_MAX_BYTES_PER_FILE,
-            writeProperties));
+            writeProperties,
+            metadataView));
   }
 
   private PCollection<FileWriteResult> 
applyUserTriggering(PCollection<FileWriteResult> input) {
@@ -168,7 +188,8 @@ class WriteToDestinations extends 
PTransform<PCollection<KV<String, Row>>, Icebe
                 dynamicDestinations,
                 filePrefix,
                 DEFAULT_MAX_BYTES_PER_FILE,
-                writeProperties));
+                writeProperties,
+                metadataView));
 
     PCollection<FileWriteResult> groupedFileWrites = 
groupAndWriteRecords(smallBatches);
 
@@ -204,7 +225,8 @@ class WriteToDestinations extends 
PTransform<PCollection<KV<String, Row>>, Icebe
                 dynamicDestinations,
                 filePrefix,
                 DEFAULT_MAX_BYTES_PER_FILE,
-                writeProperties));
+                writeProperties,
+                metadataView));
 
     // Then write the rest by shuffling on the destination
     PCollection<FileWriteResult> writeGroupedResult =
@@ -218,7 +240,8 @@ class WriteToDestinations extends 
PTransform<PCollection<KV<String, Row>>, Icebe
                     dynamicDestinations,
                     filePrefix,
                     DEFAULT_MAX_BYTES_PER_FILE,
-                    writeProperties));
+                    writeProperties,
+                    metadataView));
 
     return PCollectionList.of(writeUngroupedResult.getWrittenFiles())
         .and(writeGroupedResult)
diff --git 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteToPartitions.java
 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteToPartitions.java
index 84b9c6a9c0f..7e9d56df98a 100644
--- 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteToPartitions.java
+++ 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteToPartitions.java
@@ -36,6 +36,7 @@ import org.apache.beam.sdk.transforms.windowing.Repeatedly;
 import org.apache.beam.sdk.transforms.windowing.Window;
 import org.apache.beam.sdk.values.KV;
 import org.apache.beam.sdk.values.PCollection;
+import org.apache.beam.sdk.values.PCollectionView;
 import org.apache.beam.sdk.values.Row;
 import org.checkerframework.checker.nullness.qual.Nullable;
 import org.joda.time.Duration;
@@ -48,6 +49,7 @@ class WriteToPartitions extends 
PTransform<PCollection<KV<Row, Row>>, IcebergWri
   private final String filePrefix;
   private final boolean autoSharding;
   private final @Nullable Map<String, String> writeProperties;
+  private final @Nullable PCollectionView<Map<String, SerializableTableSpec>> 
metadataView;
 
   WriteToPartitions(
       IcebergCatalogConfig catalogConfig,
@@ -55,6 +57,22 @@ class WriteToPartitions extends 
PTransform<PCollection<KV<Row, Row>>, IcebergWri
       @Nullable Duration triggeringFrequency,
       boolean autoSharding,
       @Nullable Map<String, String> writeProperties) {
+    this(
+        catalogConfig,
+        dynamicDestinations,
+        triggeringFrequency,
+        autoSharding,
+        writeProperties,
+        null);
+  }
+
+  WriteToPartitions(
+      IcebergCatalogConfig catalogConfig,
+      DynamicDestinations dynamicDestinations,
+      @Nullable Duration triggeringFrequency,
+      boolean autoSharding,
+      @Nullable Map<String, String> writeProperties,
+      @Nullable PCollectionView<Map<String, SerializableTableSpec>> 
metadataView) {
     this.dynamicDestinations = dynamicDestinations;
     this.catalogConfig = catalogConfig;
     this.triggeringFrequency = triggeringFrequency;
@@ -62,6 +80,7 @@ class WriteToPartitions extends 
PTransform<PCollection<KV<Row, Row>>, IcebergWri
     this.filePrefix = UUID.randomUUID().toString();
     this.autoSharding = autoSharding;
     this.writeProperties = writeProperties;
+    this.metadataView = metadataView;
   }
 
   private PCollection<KV<Row, Iterable<Row>>> 
groupByPartition(PCollection<KV<Row, Row>> input) {
@@ -100,7 +119,7 @@ class WriteToPartitions extends 
PTransform<PCollection<KV<Row, Row>>, IcebergWri
     PCollection<FileWriteResult> writtenFiles =
         groupedRows.apply(
             new WritePartitionedRowsToFiles(
-                catalogConfig, dynamicDestinations, filePrefix, 
writeProperties));
+                catalogConfig, dynamicDestinations, filePrefix, 
writeProperties, metadataView));
 
     if (IcebergUtils.isUnbounded(input) && triggeringFrequency != null) {
       writtenFiles =
diff --git 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteUngroupedRowsToFiles.java
 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteUngroupedRowsToFiles.java
index 7c780e6395d..8eb462f159b 100644
--- 
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteUngroupedRowsToFiles.java
+++ 
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteUngroupedRowsToFiles.java
@@ -34,6 +34,7 @@ import org.apache.beam.sdk.util.ShardedKey;
 import org.apache.beam.sdk.values.KV;
 import org.apache.beam.sdk.values.PCollection;
 import org.apache.beam.sdk.values.PCollectionTuple;
+import org.apache.beam.sdk.values.PCollectionView;
 import org.apache.beam.sdk.values.PInput;
 import org.apache.beam.sdk.values.POutput;
 import org.apache.beam.sdk.values.PValue;
@@ -73,6 +74,7 @@ class WriteUngroupedRowsToFiles
   private final IcebergCatalogConfig catalogConfig;
   private final long maxBytesPerFile;
   private final @Nullable Map<String, String> writeProperties;
+  private final @Nullable PCollectionView<Map<String, SerializableTableSpec>> 
metadataView;
 
   WriteUngroupedRowsToFiles(
       IcebergCatalogConfig catalogConfig,
@@ -80,29 +82,46 @@ class WriteUngroupedRowsToFiles
       String filePrefix,
       long maxBytesPerFile,
       @Nullable Map<String, String> writeProperties) {
+    this(catalogConfig, dynamicDestinations, filePrefix, maxBytesPerFile, 
writeProperties, null);
+  }
+
+  WriteUngroupedRowsToFiles(
+      IcebergCatalogConfig catalogConfig,
+      DynamicDestinations dynamicDestinations,
+      String filePrefix,
+      long maxBytesPerFile,
+      @Nullable Map<String, String> writeProperties,
+      @Nullable PCollectionView<Map<String, SerializableTableSpec>> 
metadataView) {
     this.catalogConfig = catalogConfig;
     this.dynamicDestinations = dynamicDestinations;
     this.filePrefix = filePrefix;
     this.maxBytesPerFile = maxBytesPerFile;
     this.writeProperties = writeProperties;
+    this.metadataView = metadataView;
   }
 
   @Override
   public Result expand(PCollection<KV<String, Row>> input) {
 
-    PCollectionTuple resultTuple =
-        input.apply(
-            ParDo.of(
-                    new WriteUngroupedRowsToFilesDoFn(
-                        catalogConfig,
-                        dynamicDestinations,
-                        filePrefix,
-                        DEFAULT_MAX_WRITERS_PER_BUNDLE,
-                        maxBytesPerFile,
-                        writeProperties))
-                .withOutputTags(
-                    WRITTEN_FILES_TAG,
-                    TupleTagList.of(ImmutableList.of(WRITTEN_ROWS_TAG, 
SPILLED_ROWS_TAG))));
+    ParDo.MultiOutput<KV<String, Row>, FileWriteResult> parDo =
+        ParDo.of(
+                new WriteUngroupedRowsToFilesDoFn(
+                    catalogConfig,
+                    dynamicDestinations,
+                    filePrefix,
+                    DEFAULT_MAX_WRITERS_PER_BUNDLE,
+                    maxBytesPerFile,
+                    writeProperties,
+                    metadataView))
+            .withOutputTags(
+                WRITTEN_FILES_TAG,
+                TupleTagList.of(ImmutableList.of(WRITTEN_ROWS_TAG, 
SPILLED_ROWS_TAG)));
+
+    if (metadataView != null) {
+      parDo = parDo.withSideInputs(metadataView);
+    }
+
+    PCollectionTuple resultTuple = input.apply(parDo);
 
     return new Result(
         input.getPipeline(),
@@ -196,6 +215,7 @@ class WriteUngroupedRowsToFiles
     private final DynamicDestinations dynamicDestinations;
     private final IcebergCatalogConfig catalogConfig;
     private final @Nullable Map<String, String> writeProperties;
+    private final @Nullable PCollectionView<Map<String, 
SerializableTableSpec>> metadataView;
     private transient @Nullable RecordWriterManager recordWriterManager;
     private int spilledShardNumber;
 
@@ -206,12 +226,31 @@ class WriteUngroupedRowsToFiles
         int maximumWritersPerBundle,
         long maxFileSize,
         @Nullable Map<String, String> writeProperties) {
+      this(
+          catalogConfig,
+          dynamicDestinations,
+          filename,
+          maximumWritersPerBundle,
+          maxFileSize,
+          writeProperties,
+          null);
+    }
+
+    public WriteUngroupedRowsToFilesDoFn(
+        IcebergCatalogConfig catalogConfig,
+        DynamicDestinations dynamicDestinations,
+        String filename,
+        int maximumWritersPerBundle,
+        long maxFileSize,
+        @Nullable Map<String, String> writeProperties,
+        @Nullable PCollectionView<Map<String, SerializableTableSpec>> 
metadataView) {
       this.catalogConfig = catalogConfig;
       this.dynamicDestinations = dynamicDestinations;
       this.filename = filename;
       this.maxWritersPerBundle = maximumWritersPerBundle;
       this.maxFileSize = maxFileSize;
       this.writeProperties = writeProperties;
+      this.metadataView = metadataView;
     }
 
     @StartBundle
@@ -224,6 +263,7 @@ class WriteUngroupedRowsToFiles
 
     @ProcessElement
     public void processElement(
+        ProcessContext c,
         @Element KV<String, Row> element,
         BoundedWindow window,
         PaneInfo paneInfo,
@@ -235,12 +275,16 @@ class WriteUngroupedRowsToFiles
       WindowedValue<IcebergDestination> windowedDestination =
           WindowedValues.of(destination, window.maxTimestamp(), window, 
paneInfo);
 
+      Map<String, SerializableTableSpec> sideInputs =
+          metadataView != null ? c.sideInput(metadataView) : null;
+
       // Attempt to write record. If the writer is saturated and cannot accept
       // the record, spill it over to WriteGroupedRowsToFiles
       boolean writeSuccess;
       try {
         writeSuccess =
-            
Preconditions.checkNotNull(recordWriterManager).write(windowedDestination, 
data);
+            Preconditions.checkNotNull(recordWriterManager)
+                .write(windowedDestination, data, sideInputs);
       } catch (Exception e) {
         try {
           Preconditions.checkNotNull(recordWriterManager).close();
diff --git 
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitionsTest.java
 
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitionsTest.java
new file mode 100644
index 00000000000..a753c7b947a
--- /dev/null
+++ 
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitionsTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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 java.io.Serializable;
+import java.util.Map;
+import org.apache.beam.sdk.coders.KvCoder;
+import org.apache.beam.sdk.coders.StringUtf8Coder;
+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.MapElements;
+import org.apache.beam.sdk.transforms.View;
+import org.apache.beam.sdk.values.KV;
+import org.apache.beam.sdk.values.PCollection;
+import org.apache.beam.sdk.values.PCollectionView;
+import org.apache.beam.sdk.values.Row;
+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.hadoop.conf.Configuration;
+import org.apache.iceberg.CatalogProperties;
+import org.apache.iceberg.CatalogUtil;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.catalog.Catalog;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link AssignDestinationsAndPartitions}. */
+@RunWith(JUnit4.class)
+public class AssignDestinationsAndPartitionsTest implements Serializable {
+
+  @Rule public transient TestPipeline pipeline = TestPipeline.create();
+  @Rule public transient TemporaryFolder tempFolder = new TemporaryFolder();
+
+  private static final org.apache.beam.sdk.schemas.Schema BEAM_SCHEMA =
+      org.apache.beam.sdk.schemas.Schema.builder()
+          .addInt32Field("id")
+          .addStringField("name")
+          .addBooleanField("bool")
+          .build();
+
+  private static final org.apache.iceberg.Schema ICEBERG_SCHEMA =
+      IcebergUtils.beamSchemaToIcebergSchema(BEAM_SCHEMA);
+
+  private static final PartitionSpec PARTITION_SPEC =
+      PartitionSpec.builderFor(ICEBERG_SCHEMA).truncate("name", 
3).identity("bool").build();
+
+  private String warehouseLocation;
+  private IcebergCatalogConfig catalogConfig;
+
+  @Before
+  public void setUp() throws Exception {
+    warehouseLocation = "file:" + tempFolder.newFolder().getAbsolutePath();
+    catalogConfig =
+        IcebergCatalogConfig.builder()
+            .setCatalogName("hadoop")
+            .setCatalogProperties(ImmutableMap.of("type", "hadoop", 
"warehouse", warehouseLocation))
+            .build();
+    TableCache.invalidateAll();
+  }
+
+  private Catalog getCatalog() {
+    return CatalogUtil.loadCatalog(
+        CatalogUtil.ICEBERG_CATALOG_HADOOP,
+        "hadoop",
+        ImmutableMap.of(CatalogProperties.WAREHOUSE_LOCATION, 
warehouseLocation),
+        new Configuration());
+  }
+
+  @Test
+  public void testAssignDestinationsWithoutMetadataViewFallsBackToTableCache() 
{
+    TableIdentifier tableId = TableIdentifier.of("default", 
"test_table_no_view");
+    getCatalog().createTable(tableId, ICEBERG_SCHEMA, PARTITION_SPEC);
+
+    DynamicDestinations dynamicDestinations = 
DynamicDestinations.singleTable(tableId, BEAM_SCHEMA);
+
+    Row row1 = Row.withSchema(BEAM_SCHEMA).addValues(1, "alice", true).build();
+    Row row2 = Row.withSchema(BEAM_SCHEMA).addValues(2, "bob", false).build();
+
+    PCollection<Row> input =
+        pipeline.apply("CreateInput", Create.of(row1, 
row2).withRowSchema(BEAM_SCHEMA));
+
+    PCollection<KV<Row, Row>> assigned =
+        input.apply(new AssignDestinationsAndPartitions(dynamicDestinations, 
catalogConfig));
+
+    PCollection<String> partitionPaths =
+        assigned.apply(
+            "ExtractPartitionPaths",
+            MapElements.into(TypeDescriptors.strings())
+                .via(kv -> 
kv.getKey().getString(AssignDestinationsAndPartitions.PARTITION)));
+
+    PAssert.that(partitionPaths)
+        .containsInAnyOrder("name_trunc=ali/bool=true", 
"name_trunc=bob/bool=false");
+
+    pipeline.run();
+  }
+
+  @Test
+  public void testAssignDestinationsWithMetadataViewHit() {
+    TableIdentifier tableId = TableIdentifier.of("default", 
"test_table_view_hit");
+    Table realTable = getCatalog().createTable(tableId, ICEBERG_SCHEMA, 
PARTITION_SPEC);
+    SerializableTableSpec spec = SerializableTableSpec.fromTable(tableId, 
realTable);
+    String tableIdString = IcebergUtils.tableIdentifierToString(tableId);
+
+    DynamicDestinations dynamicDestinations = 
DynamicDestinations.singleTable(tableId, BEAM_SCHEMA);
+
+    // Drop table from catalog and clear cache so that any catalog fallback 
would fail to find the
+    // spec
+    getCatalog().dropTable(tableId);
+    TableCache.invalidateAll();
+
+    PCollectionView<Map<String, SerializableTableSpec>> metadataView =
+        pipeline
+            .apply(
+                "CreateMetadata",
+                Create.of(KV.of(tableIdString, spec))
+                    .withCoder(KvCoder.of(StringUtf8Coder.of(), 
SerializableTableSpec.getCoder())))
+            .apply("AsView", View.asMap());
+
+    Row row1 = Row.withSchema(BEAM_SCHEMA).addValues(1, "alice", true).build();
+
+    PCollection<Row> input =
+        pipeline.apply("CreateInput", 
Create.of(row1).withRowSchema(BEAM_SCHEMA));
+
+    PCollection<KV<Row, Row>> assigned =
+        input.apply(
+            new AssignDestinationsAndPartitions(dynamicDestinations, 
catalogConfig, metadataView));
+
+    PCollection<String> partitionPaths =
+        assigned.apply(
+            "ExtractPartitionPaths",
+            MapElements.into(TypeDescriptors.strings())
+                .via(kv -> 
kv.getKey().getString(AssignDestinationsAndPartitions.PARTITION)));
+
+    
PAssert.that(partitionPaths).containsInAnyOrder("name_trunc=ali/bool=true");
+
+    pipeline.run();
+  }
+
+  @Test
+  public void testAssignDestinationsWithMetadataViewMissFallsBack() {
+    TableIdentifier tableId = TableIdentifier.of("default", 
"test_table_view_miss");
+    getCatalog().createTable(tableId, ICEBERG_SCHEMA, PARTITION_SPEC);
+
+    TableIdentifier otherId = TableIdentifier.of("default", 
"test_other_table");
+    SerializableTableSpec otherSpec =
+        SerializableTableSpec.fromTable(tableId, 
getCatalog().loadTable(tableId));
+    String otherIdString = IcebergUtils.tableIdentifierToString(otherId);
+
+    DynamicDestinations dynamicDestinations = 
DynamicDestinations.singleTable(tableId, BEAM_SCHEMA);
+
+    PCollectionView<Map<String, SerializableTableSpec>> metadataView =
+        pipeline
+            .apply(
+                "CreateMetadata",
+                Create.of(KV.of(otherIdString, otherSpec))
+                    .withCoder(KvCoder.of(StringUtf8Coder.of(), 
SerializableTableSpec.getCoder())))
+            .apply("AsView", View.asMap());
+
+    Row row1 = Row.withSchema(BEAM_SCHEMA).addValues(1, "alice", true).build();
+
+    PCollection<Row> input =
+        pipeline.apply("CreateInput", 
Create.of(row1).withRowSchema(BEAM_SCHEMA));
+
+    PCollection<KV<Row, Row>> assigned =
+        input.apply(
+            new AssignDestinationsAndPartitions(dynamicDestinations, 
catalogConfig, metadataView));
+
+    PCollection<String> partitionPaths =
+        assigned.apply(
+            "ExtractPartitionPaths",
+            MapElements.into(TypeDescriptors.strings())
+                .via(kv -> 
kv.getKey().getString(AssignDestinationsAndPartitions.PARTITION)));
+
+    
PAssert.that(partitionPaths).containsInAnyOrder("name_trunc=ali/bool=true");
+
+    pipeline.run();
+  }
+}
diff --git 
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/RecordWriterManagerTest.java
 
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/RecordWriterManagerTest.java
index 03b3560f746..98597ba4c4f 100644
--- 
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/RecordWriterManagerTest.java
+++ 
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/RecordWriterManagerTest.java
@@ -1357,4 +1357,108 @@ public class RecordWriterManagerTest {
       }
     }
   }
+
+  @Test
+  public void testGetOrCreateTableWithSideInputHit() {
+    TableIdentifier tableId = TableIdentifier.of("default", 
"test_side_input_hit");
+    Table realTable = warehouse.createTable(tableId, ICEBERG_SCHEMA);
+    SerializableTableSpec spec = SerializableTableSpec.fromTable(tableId, 
realTable);
+    String tableIdString = IcebergUtils.tableIdentifierToString(tableId);
+
+    Catalog mockCatalog = mock(Catalog.class);
+    IcebergCatalogConfig mockCatalogConfig = mockCatalogConfigFor(mockCatalog);
+
+    IcebergDestination destination =
+        IcebergDestination.builder()
+            .setFileFormat(FileFormat.PARQUET)
+            .setTableIdentifier(tableId)
+            .build();
+
+    Map<String, SerializableTableSpec> sideInputs = 
ImmutableMap.of(tableIdString, spec);
+    RecordWriterManager writerManager =
+        new RecordWriterManager(mockCatalogConfig, "test_prefix", 1024L, 1, 
null, sideInputs);
+
+    Table resolvedTable = writerManager.getOrCreateTable(destination, 
BEAM_SCHEMA);
+    assertTrue(resolvedTable instanceof SideInputTable);
+    assertEquals(spec, ((SideInputTable) resolvedTable).getTableSpec());
+
+    // Verify catalog.loadTable was NEVER called
+    verify(mockCatalog, never()).loadTable(Mockito.any());
+  }
+
+  @Test
+  public void testGetOrCreateTableWithSideInputMissFallsBackToTableCache() {
+    TableIdentifier tableId = TableIdentifier.of("default", 
"test_side_input_miss");
+    Table realTable = warehouse.createTable(tableId, ICEBERG_SCHEMA);
+    TableIdentifier otherId = TableIdentifier.of("default", 
"test_other_table");
+    SerializableTableSpec otherSpec = SerializableTableSpec.fromTable(otherId, 
realTable);
+
+    IcebergDestination destination =
+        IcebergDestination.builder()
+            .setFileFormat(FileFormat.PARQUET)
+            .setTableIdentifier(tableId)
+            .build();
+
+    Map<String, SerializableTableSpec> sideInputs =
+        ImmutableMap.of(IcebergUtils.tableIdentifierToString(otherId), 
otherSpec);
+    RecordWriterManager writerManager =
+        new RecordWriterManager(catalogConfig, "test_prefix", 1024L, 1, null, 
sideInputs);
+
+    Table resolvedTable = writerManager.getOrCreateTable(destination, 
BEAM_SCHEMA);
+    assertNotNull(resolvedTable);
+    assertFalse(resolvedTable instanceof SideInputTable);
+    assertEquals(realTable.location(), resolvedTable.location());
+  }
+
+  @Test
+  public void testGetOrCreateTableWithNullSideInputMapFallsBack() {
+    TableIdentifier tableId = TableIdentifier.of("default", 
"test_null_side_input");
+    Table realTable = warehouse.createTable(tableId, ICEBERG_SCHEMA);
+
+    IcebergDestination destination =
+        IcebergDestination.builder()
+            .setFileFormat(FileFormat.PARQUET)
+            .setTableIdentifier(tableId)
+            .build();
+
+    RecordWriterManager writerManager =
+        new RecordWriterManager(catalogConfig, "test_prefix", 1024L, 1);
+
+    Table resolvedTable = writerManager.getOrCreateTable(destination, 
BEAM_SCHEMA, null);
+    assertNotNull(resolvedTable);
+    assertFalse(resolvedTable instanceof SideInputTable);
+    assertEquals(realTable.location(), resolvedTable.location());
+  }
+
+  @Test
+  public void testWriteWithSideInputTableProducesValidDataFiles() throws 
Exception {
+    TableIdentifier tableId = TableIdentifier.of("default", 
"test_side_input_write");
+    Table realTable = warehouse.createTable(tableId, ICEBERG_SCHEMA);
+    SerializableTableSpec spec = SerializableTableSpec.fromTable(tableId, 
realTable);
+    String tableIdString = IcebergUtils.tableIdentifierToString(tableId);
+
+    IcebergDestination destination =
+        IcebergDestination.builder()
+            .setFileFormat(FileFormat.PARQUET)
+            .setTableIdentifier(tableId)
+            .build();
+    WindowedValue<IcebergDestination> dest = 
WindowedValues.valueInGlobalWindow(destination);
+
+    Map<String, SerializableTableSpec> sideInputs = 
ImmutableMap.of(tableIdString, spec);
+    RecordWriterManager writerManager =
+        new RecordWriterManager(
+            catalogConfig, "test_side_input", Long.MAX_VALUE, 5, null, 
sideInputs);
+
+    Row row1 = Row.withSchema(BEAM_SCHEMA).addValues(1, "alice", true).build();
+    Row row2 = Row.withSchema(BEAM_SCHEMA).addValues(2, "bob", false).build();
+
+    assertTrue(writerManager.write(dest, row1));
+    assertTrue(writerManager.write(dest, row2));
+    writerManager.close();
+
+    List<SerializableDataFile> dataFiles = 
writerManager.getSerializableDataFiles().get(dest);
+    assertNotNull(dataFiles);
+    assertEquals(1, dataFiles.size());
+    assertEquals(2L, dataFiles.get(0).getRecordCount());
+  }
 }
diff --git 
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/WriteWithMetadataViewTest.java
 
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/WriteWithMetadataViewTest.java
new file mode 100644
index 00000000000..75677ff97bc
--- /dev/null
+++ 
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/WriteWithMetadataViewTest.java
@@ -0,0 +1,397 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+import org.apache.beam.sdk.coders.IterableCoder;
+import org.apache.beam.sdk.coders.KvCoder;
+import org.apache.beam.sdk.coders.RowCoder;
+import org.apache.beam.sdk.coders.StringUtf8Coder;
+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.MapElements;
+import org.apache.beam.sdk.transforms.View;
+import org.apache.beam.sdk.util.ShardedKey;
+import org.apache.beam.sdk.values.KV;
+import org.apache.beam.sdk.values.PCollection;
+import org.apache.beam.sdk.values.PCollectionView;
+import org.apache.beam.sdk.values.Row;
+import org.apache.beam.sdk.values.TypeDescriptors;
+import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList;
+import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.CatalogProperties;
+import org.apache.iceberg.CatalogUtil;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.catalog.Catalog;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.data.IcebergGenerics;
+import org.apache.iceberg.data.Record;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests verifying that file writers and orchestrators correctly resolve table 
metadata from {@link
+ * PCollectionView} of {@link SerializableTableSpec}.
+ */
+@RunWith(JUnit4.class)
+public class WriteWithMetadataViewTest implements Serializable {
+
+  @Rule public transient TestPipeline pipeline = TestPipeline.create();
+  @Rule public transient TemporaryFolder tempFolder = new TemporaryFolder();
+
+  private static final org.apache.beam.sdk.schemas.Schema BEAM_SCHEMA =
+      org.apache.beam.sdk.schemas.Schema.builder()
+          .addInt32Field("id")
+          .addStringField("name")
+          .addBooleanField("bool")
+          .build();
+
+  private static final org.apache.iceberg.Schema ICEBERG_SCHEMA =
+      IcebergUtils.beamSchemaToIcebergSchema(BEAM_SCHEMA);
+
+  private static final PartitionSpec PARTITION_SPEC =
+      PartitionSpec.builderFor(ICEBERG_SCHEMA).identity("bool").build();
+
+  private String warehouseLocation;
+  private IcebergCatalogConfig catalogConfig;
+
+  @Before
+  public void setUp() throws Exception {
+    warehouseLocation = "file:" + tempFolder.newFolder().getAbsolutePath();
+    catalogConfig =
+        IcebergCatalogConfig.builder()
+            .setCatalogName("hadoop")
+            .setCatalogProperties(ImmutableMap.of("type", "hadoop", 
"warehouse", warehouseLocation))
+            .build();
+    TableCache.invalidateAll();
+  }
+
+  private Catalog getCatalog() {
+    return CatalogUtil.loadCatalog(
+        CatalogUtil.ICEBERG_CATALOG_HADOOP,
+        "hadoop",
+        ImmutableMap.of(CatalogProperties.WAREHOUSE_LOCATION, 
warehouseLocation),
+        new Configuration());
+  }
+
+  @Test
+  public void testWriteUngroupedRowsToFilesWithMetadataView() {
+    TableIdentifier tableId = TableIdentifier.of("default", "test_ungrouped");
+    Table realTable = getCatalog().createTable(tableId, ICEBERG_SCHEMA, 
PARTITION_SPEC);
+    SerializableTableSpec spec = SerializableTableSpec.fromTable(tableId, 
realTable);
+    String tableIdString = IcebergUtils.tableIdentifierToString(tableId);
+
+    DynamicDestinations dynamicDestinations = 
DynamicDestinations.singleTable(tableId, BEAM_SCHEMA);
+
+    PCollectionView<Map<String, SerializableTableSpec>> metadataView =
+        pipeline
+            .apply(
+                "CreateMetadata",
+                Create.of(KV.of(tableIdString, spec))
+                    .withCoder(KvCoder.of(StringUtf8Coder.of(), 
SerializableTableSpec.getCoder())))
+            .apply("AsView", View.asMap());
+
+    Row row1 = Row.withSchema(BEAM_SCHEMA).addValues(1, "alice", true).build();
+    Row row2 = Row.withSchema(BEAM_SCHEMA).addValues(2, "bob", false).build();
+
+    PCollection<KV<String, Row>> input =
+        pipeline.apply(
+            "CreateInput",
+            Create.of(KV.of(tableIdString, row1), KV.of(tableIdString, row2))
+                .withCoder(KvCoder.of(StringUtf8Coder.of(), 
RowCoder.of(BEAM_SCHEMA))));
+
+    WriteUngroupedRowsToFiles.Result result =
+        input.apply(
+            new WriteUngroupedRowsToFiles(
+                catalogConfig, dynamicDestinations, "prefix", 1024L * 1024L, 
null, metadataView));
+
+    PCollection<String> tables =
+        result
+            .getWrittenFiles()
+            .apply(
+                MapElements.into(TypeDescriptors.strings())
+                    .via(f -> 
IcebergUtils.tableIdentifierToString(f.getTableIdentifier())));
+
+    PAssert.that(tables).containsInAnyOrder(tableIdString, tableIdString);
+
+    PAssert.that(result.getWrittenRows()).containsInAnyOrder(row1, row2);
+    pipeline.run();
+  }
+
+  @Test
+  public void testWriteGroupedRowsToFilesWithMetadataView() {
+    TableIdentifier tableId = TableIdentifier.of("default", "test_grouped");
+    Table realTable = getCatalog().createTable(tableId, ICEBERG_SCHEMA, 
PARTITION_SPEC);
+    SerializableTableSpec spec = SerializableTableSpec.fromTable(tableId, 
realTable);
+    String tableIdString = IcebergUtils.tableIdentifierToString(tableId);
+
+    DynamicDestinations dynamicDestinations = 
DynamicDestinations.singleTable(tableId, BEAM_SCHEMA);
+
+    PCollectionView<Map<String, SerializableTableSpec>> metadataView =
+        pipeline
+            .apply(
+                "CreateMetadata",
+                Create.of(KV.of(tableIdString, spec))
+                    .withCoder(KvCoder.of(StringUtf8Coder.of(), 
SerializableTableSpec.getCoder())))
+            .apply("AsView", View.asMap());
+
+    Row row1 = Row.withSchema(BEAM_SCHEMA).addValues(1, "alice", true).build();
+    Row row2 = Row.withSchema(BEAM_SCHEMA).addValues(2, "bob", false).build();
+
+    ShardedKey<String> shardedKey = ShardedKey.of(tableIdString, new byte[] 
{0});
+    PCollection<KV<ShardedKey<String>, Iterable<Row>>> input =
+        pipeline.apply(
+            "CreateGroupedInput",
+            Create.of(KV.of(shardedKey, (Iterable<Row>) ImmutableList.of(row1, 
row2)))
+                .withCoder(
+                    KvCoder.of(
+                        ShardedKey.Coder.of(StringUtf8Coder.of()),
+                        IterableCoder.of(RowCoder.of(BEAM_SCHEMA)))));
+
+    PCollection<FileWriteResult> writtenFiles =
+        input.apply(
+            new WriteGroupedRowsToFiles(
+                catalogConfig, dynamicDestinations, "prefix", 1024L * 1024L, 
null, metadataView));
+
+    PCollection<String> tables =
+        writtenFiles.apply(
+            MapElements.into(TypeDescriptors.strings())
+                .via(f -> 
IcebergUtils.tableIdentifierToString(f.getTableIdentifier())));
+
+    PAssert.that(tables).containsInAnyOrder(tableIdString, tableIdString);
+    pipeline.run();
+  }
+
+  @Test
+  public void testWriteDirectRowsToFilesWithMetadataView() {
+    TableIdentifier tableId = TableIdentifier.of("default", "test_direct");
+    Table realTable = getCatalog().createTable(tableId, ICEBERG_SCHEMA, 
PARTITION_SPEC);
+    SerializableTableSpec spec = SerializableTableSpec.fromTable(tableId, 
realTable);
+    String tableIdString = IcebergUtils.tableIdentifierToString(tableId);
+
+    DynamicDestinations dynamicDestinations = 
DynamicDestinations.singleTable(tableId, BEAM_SCHEMA);
+
+    PCollectionView<Map<String, SerializableTableSpec>> metadataView =
+        pipeline
+            .apply(
+                "CreateMetadata",
+                Create.of(KV.of(tableIdString, spec))
+                    .withCoder(KvCoder.of(StringUtf8Coder.of(), 
SerializableTableSpec.getCoder())))
+            .apply("AsView", View.asMap());
+
+    Row row1 = Row.withSchema(BEAM_SCHEMA).addValues(1, "alice", true).build();
+
+    PCollection<KV<String, Row>> input =
+        pipeline.apply(
+            "CreateDirectInput",
+            Create.of(KV.of(tableIdString, row1))
+                .withCoder(KvCoder.of(StringUtf8Coder.of(), 
RowCoder.of(BEAM_SCHEMA))));
+
+    PCollection<FileWriteResult> writtenFiles =
+        input.apply(
+            new WriteDirectRowsToFiles(
+                catalogConfig, dynamicDestinations, "prefix", 1024L * 1024L, 
null, metadataView));
+
+    PCollection<String> tables =
+        writtenFiles.apply(
+            MapElements.into(TypeDescriptors.strings())
+                .via(f -> 
IcebergUtils.tableIdentifierToString(f.getTableIdentifier())));
+
+    PAssert.that(tables).containsInAnyOrder(tableIdString);
+    pipeline.run();
+  }
+
+  @Test
+  public void testWritePartitionedRowsToFilesWithMetadataView() {
+    TableIdentifier tableId = TableIdentifier.of("default", 
"test_partitioned");
+    Table realTable = getCatalog().createTable(tableId, ICEBERG_SCHEMA, 
PARTITION_SPEC);
+    SerializableTableSpec spec = SerializableTableSpec.fromTable(tableId, 
realTable);
+    String tableIdString = IcebergUtils.tableIdentifierToString(tableId);
+
+    DynamicDestinations dynamicDestinations = 
DynamicDestinations.singleTable(tableId, BEAM_SCHEMA);
+
+    PCollectionView<Map<String, SerializableTableSpec>> metadataView =
+        pipeline
+            .apply(
+                "CreateMetadata",
+                Create.of(KV.of(tableIdString, spec))
+                    .withCoder(KvCoder.of(StringUtf8Coder.of(), 
SerializableTableSpec.getCoder())))
+            .apply("AsView", View.asMap());
+
+    Row partitionRow =
+        Row.withSchema(AssignDestinationsAndPartitions.OUTPUT_SCHEMA)
+            .addValues(tableIdString, "bool=true")
+            .build();
+    Row dataRow = Row.withSchema(BEAM_SCHEMA).addValues(1, "alice", 
true).build();
+
+    PCollection<KV<Row, Iterable<Row>>> input =
+        pipeline.apply(
+            "CreatePartitionedInput",
+            Create.of(KV.of(partitionRow, (Iterable<Row>) 
ImmutableList.of(dataRow)))
+                .withCoder(
+                    KvCoder.of(
+                        
RowCoder.of(AssignDestinationsAndPartitions.OUTPUT_SCHEMA),
+                        IterableCoder.of(RowCoder.of(BEAM_SCHEMA)))));
+
+    PCollection<FileWriteResult> writtenFiles =
+        input.apply(
+            new WritePartitionedRowsToFiles(
+                catalogConfig, dynamicDestinations, "prefix", null, 
metadataView));
+
+    PCollection<String> tables =
+        writtenFiles.apply(
+            MapElements.into(TypeDescriptors.strings())
+                .via(f -> 
IcebergUtils.tableIdentifierToString(f.getTableIdentifier())));
+
+    PAssert.that(tables).containsInAnyOrder(tableIdString);
+    pipeline.run();
+  }
+
+  @Test
+  public void testWriteToDestinationsUntriggeredWithMetadataView() {
+    TableIdentifier tableId = TableIdentifier.of("default", 
"test_destinations_end_to_end");
+    Table realTable = getCatalog().createTable(tableId, ICEBERG_SCHEMA, 
PARTITION_SPEC);
+    SerializableTableSpec spec = SerializableTableSpec.fromTable(tableId, 
realTable);
+    String tableIdString = IcebergUtils.tableIdentifierToString(tableId);
+
+    DynamicDestinations dynamicDestinations = 
DynamicDestinations.singleTable(tableId, BEAM_SCHEMA);
+
+    PCollectionView<Map<String, SerializableTableSpec>> metadataView =
+        pipeline
+            .apply(
+                "CreateMetadata",
+                Create.of(KV.of(tableIdString, spec))
+                    .withCoder(KvCoder.of(StringUtf8Coder.of(), 
SerializableTableSpec.getCoder())))
+            .apply("AsView", View.asMap());
+
+    Row row1 = Row.withSchema(BEAM_SCHEMA).addValues(1, "alice", true).build();
+    Row row2 = Row.withSchema(BEAM_SCHEMA).addValues(2, "bob", false).build();
+
+    PCollection<KV<String, Row>> input =
+        pipeline.apply(
+            "CreateInput",
+            Create.of(KV.of(tableIdString, row1), KV.of(tableIdString, row2))
+                .withCoder(KvCoder.of(StringUtf8Coder.of(), 
RowCoder.of(BEAM_SCHEMA))));
+
+    input.apply(
+        new WriteToDestinations(
+            catalogConfig, dynamicDestinations, null, null, null, 
metadataView));
+
+    pipeline.run();
+
+    // Verify records committed to table
+    realTable.refresh();
+    List<Record> committed = 
ImmutableList.copyOf(IcebergGenerics.read(realTable).build());
+    assertEquals(2, committed.size());
+  }
+
+  @Test
+  public void testWriteToPartitionsWithMetadataView() {
+    TableIdentifier tableId = TableIdentifier.of("default", 
"test_partitions_end_to_end");
+    Table realTable = getCatalog().createTable(tableId, ICEBERG_SCHEMA, 
PARTITION_SPEC);
+    SerializableTableSpec spec = SerializableTableSpec.fromTable(tableId, 
realTable);
+    String tableIdString = IcebergUtils.tableIdentifierToString(tableId);
+
+    DynamicDestinations dynamicDestinations = 
DynamicDestinations.singleTable(tableId, BEAM_SCHEMA);
+
+    PCollectionView<Map<String, SerializableTableSpec>> metadataView =
+        pipeline
+            .apply(
+                "CreateMetadata",
+                Create.of(KV.of(tableIdString, spec))
+                    .withCoder(KvCoder.of(StringUtf8Coder.of(), 
SerializableTableSpec.getCoder())))
+            .apply("AsView", View.asMap());
+
+    Row row1 = Row.withSchema(BEAM_SCHEMA).addValues(1, "alice", true).build();
+    Row row2 = Row.withSchema(BEAM_SCHEMA).addValues(2, "bob", false).build();
+
+    PCollection<Row> input =
+        pipeline.apply("CreateRows", Create.of(row1, 
row2).withRowSchema(BEAM_SCHEMA));
+
+    PCollection<KV<Row, Row>> assigned =
+        input.apply(
+            new AssignDestinationsAndPartitions(dynamicDestinations, 
catalogConfig, metadataView));
+
+    assigned.apply(
+        new WriteToPartitions(catalogConfig, dynamicDestinations, null, false, 
null, metadataView));
+
+    pipeline.run();
+
+    // Verify records committed to table
+    realTable.refresh();
+    List<Record> committed = 
ImmutableList.copyOf(IcebergGenerics.read(realTable).build());
+    assertEquals(2, committed.size());
+  }
+
+  @Test
+  public void testWriteUngroupedRowsBypassesCatalogWhenUsingMetadataView() {
+    TableIdentifier tableId = TableIdentifier.of("default", 
"test_bypasses_catalog");
+    Table realTable = getCatalog().createTable(tableId, ICEBERG_SCHEMA, 
PARTITION_SPEC);
+    SerializableTableSpec spec = SerializableTableSpec.fromTable(tableId, 
realTable);
+    String tableIdString = IcebergUtils.tableIdentifierToString(tableId);
+
+    // Drop table from catalog and invalidate cache so catalog.loadTable() 
would fail if invoked
+    getCatalog().dropTable(tableId, false);
+    TableCache.invalidateAll();
+
+    DynamicDestinations dynamicDestinations = 
DynamicDestinations.singleTable(tableId, BEAM_SCHEMA);
+
+    PCollectionView<Map<String, SerializableTableSpec>> metadataView =
+        pipeline
+            .apply(
+                "CreateMetadata",
+                Create.of(KV.of(tableIdString, spec))
+                    .withCoder(KvCoder.of(StringUtf8Coder.of(), 
SerializableTableSpec.getCoder())))
+            .apply("AsView", View.asMap());
+
+    Row row1 = Row.withSchema(BEAM_SCHEMA).addValues(1, "alice", true).build();
+
+    PCollection<KV<String, Row>> input =
+        pipeline.apply(
+            "CreateInput",
+            Create.of(KV.of(tableIdString, row1))
+                .withCoder(KvCoder.of(StringUtf8Coder.of(), 
RowCoder.of(BEAM_SCHEMA))));
+
+    WriteUngroupedRowsToFiles.Result result =
+        input.apply(
+            new WriteUngroupedRowsToFiles(
+                catalogConfig, dynamicDestinations, "prefix", 1024L * 1024L, 
null, metadataView));
+
+    PCollection<String> tables =
+        result
+            .getWrittenFiles()
+            .apply(
+                MapElements.into(TypeDescriptors.strings())
+                    .via(f -> 
IcebergUtils.tableIdentifierToString(f.getTableIdentifier())));
+
+    PAssert.that(tables).containsInAnyOrder(tableIdString);
+    PAssert.that(result.getWrittenRows()).containsInAnyOrder(row1);
+    pipeline.run();
+  }
+}

Reply via email to