This is an automated email from the ASF dual-hosted git repository.
ahmedabu98 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 70a522364e9 (IcebergIO) Support PartitionSpec/SortOrder on dynamic
table creation via IcebergIO (#39408)
70a522364e9 is described below
commit 70a522364e9a5766ddb48e2cadbf1b9471b6d205
Author: Claire McGinty <[email protected]>
AuthorDate: Mon Jul 27 16:34:21 2026 -0400
(IcebergIO) Support PartitionSpec/SortOrder on dynamic table creation via
IcebergIO (#39408)
* (IcebergIO) Support SortOrder on dynamic table creation via IcebergIO
* withPartitionSpec -> withPartitionFields
---
.../beam/sdk/io/iceberg/DynamicDestinations.java | 12 ++++-
.../org/apache/beam/sdk/io/iceberg/IcebergIO.java | 33 ++++++++++++-
.../io/iceberg/OneTableDynamicDestinations.java | 33 +++++++++++--
.../io/iceberg/WritePartitionedRowsToFiles.java | 13 +++++-
.../beam/sdk/io/iceberg/IcebergIOWriteTest.java | 54 ++++++++++++++++++++++
5 files changed, 138 insertions(+), 7 deletions(-)
diff --git
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/DynamicDestinations.java
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/DynamicDestinations.java
index 0185758c8ae..ba9ec771c11 100644
---
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/DynamicDestinations.java
+++
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/DynamicDestinations.java
@@ -18,10 +18,12 @@
package org.apache.beam.sdk.io.iceberg;
import java.io.Serializable;
+import java.util.List;
import org.apache.beam.sdk.schemas.Schema;
import org.apache.beam.sdk.values.Row;
import org.apache.beam.sdk.values.ValueInSingleWindow;
import org.apache.iceberg.catalog.TableIdentifier;
+import org.checkerframework.checker.nullness.qual.Nullable;
public interface DynamicDestinations extends Serializable {
@@ -34,6 +36,14 @@ public interface DynamicDestinations extends Serializable {
String getTableStringIdentifier(ValueInSingleWindow<Row> element);
static DynamicDestinations singleTable(TableIdentifier tableId, Schema
inputSchema) {
- return new OneTableDynamicDestinations(tableId, inputSchema);
+ return new OneTableDynamicDestinations(tableId, inputSchema, null, null);
+ }
+
+ static DynamicDestinations singleTable(
+ TableIdentifier tableId,
+ Schema inputSchema,
+ @Nullable List<String> partitionFields,
+ @Nullable List<String> sortFields) {
+ return new OneTableDynamicDestinations(tableId, inputSchema,
partitionFields, sortFields);
}
}
diff --git
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergIO.java
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergIO.java
index 04feea5037d..abdc2a179b5 100644
---
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergIO.java
+++
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergIO.java
@@ -409,6 +409,10 @@ public class IcebergIO {
abstract @Nullable Map<String, String> getWriteProperties();
+ abstract @Nullable List<String> getPartitionFields();
+
+ abstract @Nullable List<String> getSortFields();
+
abstract Builder toBuilder();
@AutoValue.Builder
@@ -429,6 +433,10 @@ public class IcebergIO {
abstract Builder setWriteProperties(Map<String, String> writeProperties);
+ abstract Builder setPartitionFields(List<String> partitionFields);
+
+ abstract Builder setSortFields(List<String> sortFields);
+
abstract WriteRows build();
}
@@ -483,6 +491,26 @@ public class IcebergIO {
return toBuilder().setWriteProperties(writeProperties).build();
}
+ /**
+ * Defines the desired Partition Spec to be applied when the Iceberg table
must be dynamically
+ * created, e.g. `bucket(id_field, 32)` or `day(timestamp_field)`
+ *
+ * <p>See: https://iceberg.apache.org/spec/#partitioning
+ */
+ public WriteRows withPartitionFields(List<String> partitionFields) {
+ return toBuilder().setPartitionFields(partitionFields).build();
+ }
+
+ /**
+ * Defines the desired Sort Order to be applied when the Iceberg table
must be dynamically
+ * created, e.g. `int_field desc` or `bucket(modulo_5, 4) asc nulls last`
+ *
+ * <p>See: https://iceberg.apache.org/spec/#sorting
+ */
+ public WriteRows withSortOrder(List<String> sortFields) {
+ return toBuilder().setSortFields(sortFields).build();
+ }
+
@Override
public IcebergWriteResult expand(PCollection<Row> input) {
List<?> allToArgs = Arrays.asList(getTableIdentifier(),
getDynamicDestinations());
@@ -494,7 +522,10 @@ public class IcebergIO {
if (destinations == null) {
destinations =
DynamicDestinations.singleTable(
- Preconditions.checkNotNull(getTableIdentifier()),
input.getSchema());
+ Preconditions.checkNotNull(getTableIdentifier()),
+ input.getSchema(),
+ getPartitionFields(),
+ getSortFields());
}
// Assign destinations before re-windowing to global in
WriteToDestinations because
diff --git
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/OneTableDynamicDestinations.java
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/OneTableDynamicDestinations.java
index afca43fca15..a9399558948 100644
---
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/OneTableDynamicDestinations.java
+++
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/OneTableDynamicDestinations.java
@@ -23,6 +23,7 @@ import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
+import java.util.List;
import org.apache.beam.sdk.schemas.Schema;
import org.apache.beam.sdk.values.Row;
import org.apache.beam.sdk.values.ValueInSingleWindow;
@@ -30,6 +31,7 @@ import
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.Vi
import org.apache.iceberg.FileFormat;
import org.apache.iceberg.catalog.TableIdentifier;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
class OneTableDynamicDestinations implements DynamicDestinations,
Externalizable {
// TableId represented as String for serializability
@@ -37,6 +39,8 @@ class OneTableDynamicDestinations implements
DynamicDestinations, Externalizable
private transient @MonotonicNonNull TableIdentifier tableId;
private transient @MonotonicNonNull Schema rowSchema;
+ private transient @Nullable List<String> partitionFields;
+ private transient @Nullable List<String> sortFields;
@VisibleForTesting
TableIdentifier getTableIdentifier() {
@@ -46,9 +50,15 @@ class OneTableDynamicDestinations implements
DynamicDestinations, Externalizable
return tableId;
}
- OneTableDynamicDestinations(TableIdentifier tableId, Schema rowSchema) {
+ OneTableDynamicDestinations(
+ TableIdentifier tableId,
+ Schema rowSchema,
+ @Nullable List<String> partitionFields,
+ @Nullable List<String> sortFields) {
this.tableIdString = IcebergUtils.tableIdentifierToString(tableId);
this.rowSchema = rowSchema;
+ this.partitionFields = partitionFields;
+ this.sortFields = sortFields;
}
@Override
@@ -68,9 +78,18 @@ class OneTableDynamicDestinations implements
DynamicDestinations, Externalizable
@Override
public IcebergDestination instantiateDestination(String unused) {
+ @Nullable IcebergTableCreateConfig createConfig = null;
+ if (partitionFields != null || sortFields != null) {
+ createConfig =
+ IcebergTableCreateConfig.builder()
+ .setSchema(checkStateNotNull(rowSchema))
+ .setPartitionFields(partitionFields)
+ .setSortFields(sortFields)
+ .build();
+ }
return IcebergDestination.builder()
.setTableIdentifier(getTableIdentifier())
- .setTableCreateConfig(null)
+ .setTableCreateConfig(createConfig)
.setFileFormat(FileFormat.PARQUET)
.build();
}
@@ -78,14 +97,22 @@ class OneTableDynamicDestinations implements
DynamicDestinations, Externalizable
// Need a public default constructor for custom serialization
public OneTableDynamicDestinations() {}
+ @SuppressWarnings("nullness")
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(checkStateNotNull(tableIdString));
+ out.writeObject(rowSchema);
+ out.writeObject(partitionFields);
+ out.writeObject(sortFields);
}
+ @SuppressWarnings("nullness")
@Override
- public void readExternal(ObjectInput in) throws IOException {
+ public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
tableIdString = in.readUTF();
tableId = IcebergUtils.parseTableIdentifier(tableIdString);
+ rowSchema = (Schema) in.readObject();
+ partitionFields = (List<String>) in.readObject();
+ sortFields = (List<String>) in.readObject();
}
}
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 84e7d05b124..d1a08980fa9 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
@@ -39,6 +39,7 @@ import org.apache.iceberg.DataFiles;
import org.apache.iceberg.PartitionField;
import org.apache.iceberg.PartitionKey;
import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.SortOrder;
import org.apache.iceberg.StructLike;
import org.apache.iceberg.Table;
import org.apache.iceberg.catalog.Catalog;
@@ -189,6 +190,8 @@ class WritePartitionedRowsToFiles
@Nullable IcebergTableCreateConfig createConfig =
destination.getTableCreateConfig();
PartitionSpec partitionSpec =
createConfig != null ? createConfig.getPartitionSpec() :
PartitionSpec.unpartitioned();
+ SortOrder sortOrder =
+ createConfig != null ? createConfig.getSortOrder() :
SortOrder.unsorted();
Map<String, String> tableProperties =
createConfig != null && createConfig.getTableProperties() != null
? createConfig.getTableProperties()
@@ -217,13 +220,19 @@ class WritePartitionedRowsToFiles
org.apache.iceberg.Schema tableSchema =
IcebergUtils.beamSchemaToIcebergSchema(dataSchema);
try {
Table table =
- catalog.createTable(identifier, tableSchema, partitionSpec,
tableProperties);
+ catalog
+ .buildTable(identifier, tableSchema)
+ .withPartitionSpec(partitionSpec)
+ .withSortOrder(sortOrder)
+ .withProperties(tableProperties)
+ .create();
LOG.info(
"Created Iceberg table '{}' with schema: {}\n"
- + ", partition spec: {}, table properties: {}",
+ + ", partition spec: {}, sort order: {}, table properties:
{}",
identifier,
tableSchema,
partitionSpec,
+ sortOrder,
tableProperties);
return table;
} catch (AlreadyExistsException ignored) {
diff --git
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOWriteTest.java
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOWriteTest.java
index 6580f4eeaf6..384e11b761b 100644
---
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOWriteTest.java
+++
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOWriteTest.java
@@ -76,7 +76,10 @@ import org.apache.iceberg.CatalogUtil;
import org.apache.iceberg.DataFile;
import org.apache.iceberg.DistributionMode;
import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.NullOrder;
import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.SortDirection;
+import org.apache.iceberg.SortOrder;
import org.apache.iceberg.Table;
import org.apache.iceberg.catalog.Namespace;
import org.apache.iceberg.catalog.SupportsNamespaces;
@@ -775,4 +778,55 @@ public class IcebergIOWriteTest implements Serializable {
.getCommitted();
assertEquals(5L, numWaves);
}
+
+ @Test
+ public void testCreateTableWithPartitionSpecAndSortOrder() {
+ TableIdentifier tableId =
+ TableIdentifier.of(
+ "default", "sorted_partitioned_" +
Long.toString(UUID.randomUUID().hashCode(), 16));
+
+ Schema beamSchema =
IcebergUtils.icebergSchemaToBeamSchema(TestFixtures.SCHEMA);
+
+ Map<String, String> catalogProps =
+ ImmutableMap.<String, String>builder()
+ .put("type", CatalogUtil.ICEBERG_CATALOG_TYPE_HADOOP)
+ .put("warehouse", warehouse.location)
+ .build();
+
+ IcebergCatalogConfig catalog =
+ IcebergCatalogConfig.builder()
+ .setCatalogName("name")
+ .setCatalogProperties(catalogProps)
+ .build();
+
+ testPipeline
+ .apply("Records To Add",
Create.of(TestFixtures.asRows(TestFixtures.FILE1SNAPSHOT1)))
+ .setRowSchema(beamSchema)
+ .apply(
+ "Append To Table",
+ writeTransform(catalog, tableId)
+ .withPartitionFields(ImmutableList.of("bucket(id, 2)"))
+ .withSortOrder(ImmutableList.of("data asc nulls first")));
+
+ testPipeline.run().waitUntilFinish();
+
+ Table table = warehouse.loadTable(tableId);
+
+ // verify partition spec
+ PartitionSpec spec = table.spec();
+ assertTrue(spec.isPartitioned());
+ assertEquals(1, spec.fields().size());
+ assertEquals("id_bucket", spec.fields().get(0).name());
+
+ // verify sort order
+ SortOrder sortOrder = table.sortOrder();
+ assertTrue(sortOrder.isSorted());
+ assertEquals(1, sortOrder.fields().size());
+ assertEquals(SortDirection.ASC, sortOrder.fields().get(0).direction());
+ assertEquals(NullOrder.NULLS_FIRST, sortOrder.fields().get(0).nullOrder());
+
+ // verify data was written correctly
+ List<Record> writtenRecords =
ImmutableList.copyOf(IcebergGenerics.read(table).build());
+ assertThat(writtenRecords,
Matchers.containsInAnyOrder(TestFixtures.FILE1SNAPSHOT1.toArray()));
+ }
}