From 9ba8e27119302c784a42b44b5fff9158f1cb934b Mon Sep 17 00:00:00 2001 From: Ahmed Abualsaud Date: Mon, 26 Oct 2026 15:19:07 -0700 Subject: [PATCH 1/3] managed add files --- CHANGES.md | 1 + .../pipeline/v1/external_transforms.proto | 2 + .../AddFilesSchemaTransformProvider.java | 21 +++++- .../IcebergSchemaTransformTranslation.java | 15 ++++ .../AddFilesSchemaTransformProviderTest.java | 55 +++++++++++++++ ...IcebergSchemaTransformTranslationTest.java | 68 +++++++++++++++++++ .../org/apache/beam/sdk/managed/Managed.java | 7 ++ .../python/apache_beam/transforms/external.py | 1 + sdks/python/apache_beam/transforms/managed.py | 5 +- sdks/python/apache_beam/yaml/standard_io.yaml | 2 +- sdks/standard_expansion_services.yaml | 1 + 11 files changed, 175 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 04ac961fcd93..e6c78023f4be 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -68,6 +68,7 @@ * (Java) FirestoreIO: added `RpcQosOptions.Builder.withRampupThrottlingDisabled()`, mirroring the existing DatastoreIO option, to opt out of the 500/50/5 write ramp-up ([#27170](https://github.com/apache/beam/issues/27170)). * Added support for attributing BigQuery API quota and billing to a specific GCP project (quota project): `quota_project_id` parameter in `ReadFromBigQuery` or `--quota_project_id` pipeline option, honored by the Python BigQuery IO transforms (`WriteToBigQuery` with `FILE_LOADS` or `STREAMING_INSERTS`; not yet `STORAGE_WRITE_API`, which is Java-backed), `--bigQueryQuotaProjectId` pipeline option (Java), and `bigqueryio.WithQuotaProject` read/query option (Go) ([#37431](https://github.com/apache/beam/issues/37431)). * (Java/Python/Go) DebeziumIO: `readAsJson()` can now include `op` and the remaining Debezium `source` fields (e.g. MySQL `file`, `pos`, `row`, `ts_ms`; PostgreSQL `lsn`, `txId`, `ts_ms`) in `metadata` via `withFullMetadata(true)` / `full_metadata=True` / `debeziumio.FullMetadata()`. Off by default ([#40701](https://github.com/apache/beam/issues/40701)). +* [IcebergIO] AddFiles is now available through the Managed API with `Managed.write(Managed.ICEBERG_ADD_FILES)` (Java/Python), with URN `beam:schematransform:org.apache.beam:iceberg_add_files:v1` ([#XXXXX](https://github.com/apache/beam/pull/XXXXX)). ## New Features / Improvements diff --git a/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/external_transforms.proto b/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/external_transforms.proto index debacc245d60..22a95d1dcd6a 100644 --- a/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/external_transforms.proto +++ b/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/external_transforms.proto @@ -109,6 +109,8 @@ message ManagedTransforms { "beam:schematransform:org.apache.beam:delta_lake_read:v1"]; DELTA_LAKE_CDC_READ = 14 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:schematransform:org.apache.beam:delta_lake_cdc_read:v1"]; + ICEBERG_ADD_FILES = 15 [(org.apache.beam.model.pipeline.v1.beam_urn) = + "beam:schematransform:org.apache.beam:iceberg_add_files:v1"]; } } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFilesSchemaTransformProvider.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFilesSchemaTransformProvider.java index c5cc758c555b..dced27a3aa2b 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFilesSchemaTransformProvider.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFilesSchemaTransformProvider.java @@ -21,6 +21,7 @@ import static org.apache.beam.sdk.io.iceberg.AddFiles.OUTPUT_TAG; import static org.apache.beam.sdk.io.iceberg.AddFilesSchemaTransformProvider.Configuration; import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; +import static org.apache.beam.sdk.util.construction.BeamUrns.getUrn; import com.google.auto.service.AutoService; import com.google.auto.value.AutoValue; @@ -30,8 +31,11 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.apache.beam.model.pipeline.v1.ExternalTransforms; import org.apache.beam.sdk.schemas.AutoValueSchema; +import org.apache.beam.sdk.schemas.NoSuchSchemaException; import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.SchemaRegistry; import org.apache.beam.sdk.schemas.annotations.DefaultSchema; import org.apache.beam.sdk.schemas.annotations.SchemaFieldDescription; import org.apache.beam.sdk.schemas.transforms.SchemaTransform; @@ -41,6 +45,7 @@ import org.apache.beam.sdk.transforms.Filter; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.values.PCollectionRowTuple; +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.base.Preconditions; import org.checkerframework.checker.nullness.qual.Nullable; @@ -55,7 +60,7 @@ public AddFilesSchemaTransform from(Configuration configuration) { @Override public String identifier() { - return "beam:schematransform:iceberg_add_files:v1"; + return getUrn(ExternalTransforms.ManagedTransforms.Urns.ICEBERG_ADD_FILES); } @DefaultSchema(AutoValueSchema.class) @@ -302,6 +307,20 @@ public AddFilesSchemaTransform(Configuration configuration) { this.configuration = configuration; } + Row getConfigurationRow() { + try { + // To stay consistent with our SchemaTransform configuration naming conventions, + // we sort lexicographically and convert field names to snake_case + return SchemaRegistry.createDefault() + .getToRowFunction(Configuration.class) + .apply(configuration) + .sorted() + .toSnakeCase(); + } catch (NoSuchSchemaException e) { + throw new RuntimeException(e); + } + } + @Override public PCollectionRowTuple expand(PCollectionRowTuple input) { Schema inputSchema = input.getSinglePCollection().getSchema(); diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergSchemaTransformTranslation.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergSchemaTransformTranslation.java index 801464010c69..15beb99a7a2b 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergSchemaTransformTranslation.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergSchemaTransformTranslation.java @@ -23,6 +23,7 @@ import com.google.auto.service.AutoService; import java.util.Map; +import org.apache.beam.sdk.io.iceberg.AddFilesSchemaTransformProvider.AddFilesSchemaTransform; import org.apache.beam.sdk.io.iceberg.IcebergCdcReadSchemaTransformProvider.IcebergCdcReadSchemaTransform; import org.apache.beam.sdk.schemas.transforms.SchemaTransformProvider; import org.apache.beam.sdk.transforms.PTransform; @@ -87,6 +88,19 @@ public Row toConfigRow(IcebergWriteSchemaTransform transform) { } } + static class AddFilesSchemaTransformTranslator + extends SchemaTransformPayloadTranslator { + @Override + public SchemaTransformProvider provider() { + return new AddFilesSchemaTransformProvider(); + } + + @Override + public Row toConfigRow(AddFilesSchemaTransform transform) { + return transform.getConfigurationRow(); + } + } + @AutoService(TransformPayloadTranslatorRegistrar.class) public static class WriteRegistrar implements TransformPayloadTranslatorRegistrar { @Override @@ -97,6 +111,7 @@ public static class WriteRegistrar implements TransformPayloadTranslatorRegistra getTransformPayloadTranslators() { return ImmutableMap., TransformPayloadTranslator>builder() .put(IcebergWriteSchemaTransform.class, new IcebergWriteSchemaTransformTranslator()) + .put(AddFilesSchemaTransform.class, new AddFilesSchemaTransformTranslator()) .build(); } } diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesSchemaTransformProviderTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesSchemaTransformProviderTest.java index 3b58af201543..463ee24ab55f 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesSchemaTransformProviderTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesSchemaTransformProviderTest.java @@ -17,24 +17,50 @@ */ package org.apache.beam.sdk.io.iceberg; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; +import java.util.List; import org.apache.beam.sdk.io.iceberg.AddFilesSchemaTransformProvider.Configuration; +import org.apache.beam.sdk.managed.Managed; +import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.transforms.providers.ErrorHandling; +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.values.PCollectionRowTuple; +import org.apache.beam.sdk.values.Row; +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.iceberg.DataFile; +import org.apache.iceberg.Table; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.data.IcebergGenerics; +import org.apache.iceberg.data.Record; +import org.hamcrest.Matchers; +import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class AddFilesSchemaTransformProviderTest { + @ClassRule public static final TemporaryFolder TEMPORARY_FOLDER = new TemporaryFolder(); + + @Rule + public transient TestDataWarehouse warehouse = new TestDataWarehouse(TEMPORARY_FOLDER, "default"); + + @Rule public transient TestPipeline pipeline = TestPipeline.create(); private static Configuration.Builder base() { return Configuration.builder() @@ -182,4 +208,33 @@ public void testSettingsWithoutOptionsRejectedNamingTheOptionsKey() { assertTrue( unverifiable.getMessage(), unverifiable.getMessage().contains("schema_evolution_options")); } + + @Test + public void testAddFilesUsingManagedTransform() throws IOException { + TableIdentifier tableId = TableIdentifier.of("default", "managed_add_files"); + DataFile file = + warehouse.writeRecords("data.parquet", TestFixtures.SCHEMA, TestFixtures.FILE1SNAPSHOT1); + + Schema inputSchema = Schema.builder().addStringField("file").build(); + PCollectionRowTuple output = + pipeline + .apply(Create.of(Row.withSchema(inputSchema).addValue(file.location()).build())) + .setRowSchema(inputSchema) + .apply( + Managed.write(Managed.ICEBERG_ADD_FILES) + .withConfig( + ImmutableMap.builder() + .put("table", tableId.toString()) + .put( + "catalog_properties", + ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location)) + .put("error_handling", ImmutableMap.of("output", "errors")) + .build())); + PAssert.that(output.get("errors")).empty(); + pipeline.run().waitUntilFinish(); + + Table table = warehouse.loadTable(tableId); + List records = ImmutableList.copyOf(IcebergGenerics.read(table).build()); + assertThat(records, Matchers.containsInAnyOrder(TestFixtures.FILE1SNAPSHOT1.toArray())); + } } diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergSchemaTransformTranslationTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergSchemaTransformTranslationTest.java index 3c07c97b4a52..5a49eb7bb7f1 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergSchemaTransformTranslationTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergSchemaTransformTranslationTest.java @@ -34,8 +34,11 @@ import org.apache.beam.model.pipeline.v1.RunnerApi; import org.apache.beam.sdk.Pipeline; import org.apache.beam.sdk.coders.RowCoder; +import org.apache.beam.sdk.io.iceberg.AddFilesSchemaTransformProvider.AddFilesSchemaTransform; import org.apache.beam.sdk.io.iceberg.IcebergCdcReadSchemaTransformProvider.IcebergCdcReadSchemaTransform; +import org.apache.beam.sdk.io.iceberg.IcebergSchemaTransformTranslation.AddFilesSchemaTransformTranslator; import org.apache.beam.sdk.io.iceberg.IcebergSchemaTransformTranslation.IcebergCdcReadSchemaTransformTranslator; +import org.apache.beam.sdk.managed.Managed; import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.SchemaTranslation; @@ -74,6 +77,8 @@ public class IcebergSchemaTransformTranslationTest { new IcebergReadSchemaTransformProvider(); static final IcebergCdcReadSchemaTransformProvider READ_CDC_PROVIDER = new IcebergCdcReadSchemaTransformProvider(); + static final AddFilesSchemaTransformProvider ADD_FILES_PROVIDER = + new AddFilesSchemaTransformProvider(); private static final Map CATALOG_PROPERTIES = ImmutableMap.builder() @@ -133,6 +138,15 @@ public class IcebergSchemaTransformTranslationTest { .withFieldValue("watermark_column", "event_micros") .build(); + private static final Row ADD_FILES_CONFIG_ROW = + Row.withSchema(ADD_FILES_PROVIDER.configurationSchema()) + .withFieldValue("table", "test_table_identifier") + .withFieldValue("catalog_properties", CATALOG_PROPERTIES) + .withFieldValue("partition_fields", Collections.singletonList("id")) + .withFieldValue("manifest_file_size", 100) + .withFieldValue("error_handling.output", "errors") + .build(); + @Test public void testReCreateWriteTransformFromRow() { IcebergWriteSchemaTransform writeTransform = @@ -374,6 +388,60 @@ public void testCdcReadTransformProtoTranslation() assertEquals(transformConfigRow, readTransformFromSpec.getConfigurationRow()); } + @Test + public void testAddFilesTransformProtoTranslation() + throws InvalidProtocolBufferException, IOException { + // First build a pipeline that adds files through the Managed API + Pipeline p = Pipeline.create(); + Schema inputSchema = Schema.builder().addStringField("file").build(); + p.apply(Create.of(Row.withSchema(inputSchema).addValue("data.parquet").build())) + .setRowSchema(inputSchema) + .apply( + Managed.write(Managed.ICEBERG_ADD_FILES) + .withConfig( + ImmutableMap.builder() + .put("table", "test_table_identifier") + .put("catalog_properties", CATALOG_PROPERTIES) + .put("partition_fields", Collections.singletonList("id")) + .put("manifest_file_size", 100) + .put("error_handling", ImmutableMap.of("output", "errors")) + .build())); + + // Then translate the pipeline to a proto and extract AddFilesSchemaTransform proto + RunnerApi.Pipeline pipelineProto = PipelineTranslation.toProto(p); + List addFilesTransformProto = + pipelineProto.getComponents().getTransformsMap().values().stream() + .filter( + tr -> { + RunnerApi.FunctionSpec spec = tr.getSpec(); + try { + return spec.getUrn().equals(BeamUrns.getUrn(SCHEMA_TRANSFORM)) + && SchemaTransformPayload.parseFrom(spec.getPayload()) + .getIdentifier() + .equals(ADD_FILES_PROVIDER.identifier()); + } catch (InvalidProtocolBufferException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toList()); + assertEquals(1, addFilesTransformProto.size()); + RunnerApi.FunctionSpec spec = addFilesTransformProto.get(0).getSpec(); + + // Check that the proto contains correct values + SchemaTransformPayload payload = SchemaTransformPayload.parseFrom(spec.getPayload()); + Schema schemaFromSpec = SchemaTranslation.schemaFromProto(payload.getConfigurationSchema()); + assertEquals(ADD_FILES_PROVIDER.configurationSchema(), schemaFromSpec); + Row rowFromSpec = RowCoder.of(schemaFromSpec).decode(payload.getConfigurationRow().newInput()); + assertEquals(ADD_FILES_CONFIG_ROW, rowFromSpec); + + // Use the information in the proto to recreate the AddFilesSchemaTransform + AddFilesSchemaTransformTranslator translator = new AddFilesSchemaTransformTranslator(); + AddFilesSchemaTransform transformFromSpec = + translator.fromConfigRow(rowFromSpec, PipelineOptionsFactory.create()); + + assertEquals(ADD_FILES_CONFIG_ROW, transformFromSpec.getConfigurationRow()); + } + private static Record record(long id, String data, long eventMicros) { return TestFixtures.createRecord( CDC_TRANSLATION_SCHEMA, diff --git a/sdks/java/managed/src/main/java/org/apache/beam/sdk/managed/Managed.java b/sdks/java/managed/src/main/java/org/apache/beam/sdk/managed/Managed.java index 27c647478e17..8ba003c94957 100644 --- a/sdks/java/managed/src/main/java/org/apache/beam/sdk/managed/Managed.java +++ b/sdks/java/managed/src/main/java/org/apache/beam/sdk/managed/Managed.java @@ -101,6 +101,7 @@ public class Managed { public static final String POSTGRES = "postgres"; public static final String MYSQL = "mysql"; public static final String SQL_SERVER = "sqlserver"; + public static final String ICEBERG_ADD_FILES = "iceberg_add_files"; // Supported SchemaTransforms public static final Map READ_TRANSFORMS = @@ -124,6 +125,9 @@ DELTA_LAKE_CDC, getUrn(ExternalTransforms.ManagedTransforms.Urns.DELTA_LAKE_CDC_ .put(POSTGRES, getUrn(ExternalTransforms.ManagedTransforms.Urns.POSTGRES_WRITE)) .put(MYSQL, getUrn(ExternalTransforms.ManagedTransforms.Urns.MYSQL_WRITE)) .put(SQL_SERVER, getUrn(ExternalTransforms.ManagedTransforms.Urns.SQL_SERVER_WRITE)) + .put( + ICEBERG_ADD_FILES, + getUrn(ExternalTransforms.ManagedTransforms.Urns.ICEBERG_ADD_FILES)) .build(); /** @@ -168,6 +172,9 @@ public static ManagedTransform read(String source) { * href="https://beam.apache.org/releases/javadoc/current/org/apache/beam/sdk/io/kafka/KafkaIO.html">KafkaIO *
  • {@link Managed#BIGQUERY} : Write to GCP BigQuery tables BigQueryIO + *
  • {@link Managed#ICEBERG_ADD_FILES} : Register existing data files in Apache Iceberg tables + * using AddFiles * */ public static ManagedTransform write(String sink) { diff --git a/sdks/python/apache_beam/transforms/external.py b/sdks/python/apache_beam/transforms/external.py index 33926281074d..5c5dbcd58fdf 100644 --- a/sdks/python/apache_beam/transforms/external.py +++ b/sdks/python/apache_beam/transforms/external.py @@ -88,6 +88,7 @@ ManagedTransforms.Urns.SQL_SERVER_WRITE.urn: _GCP_EXPANSION_SERVICE_JAR_TARGET, # pylint: disable=line-too-long ManagedTransforms.Urns.DELTA_LAKE_READ.urn: _IO_EXPANSION_SERVICE_JAR_TARGET, ManagedTransforms.Urns.DELTA_LAKE_CDC_READ.urn: _IO_EXPANSION_SERVICE_JAR_TARGET, + ManagedTransforms.Urns.ICEBERG_ADD_FILES.urn: _IO_EXPANSION_SERVICE_JAR_TARGET, # pylint: disable=line-too-long } diff --git a/sdks/python/apache_beam/transforms/managed.py b/sdks/python/apache_beam/transforms/managed.py index c252b10de5db..aeea50de56a9 100644 --- a/sdks/python/apache_beam/transforms/managed.py +++ b/sdks/python/apache_beam/transforms/managed.py @@ -90,6 +90,7 @@ SQL_SERVER = "sqlserver" DELTA = "delta" DELTA_CDC = "delta_cdc" +ICEBERG_ADD_FILES = "iceberg_add_files" __all__ = [ "ICEBERG", @@ -97,6 +98,7 @@ "BIGQUERY", "DELTA", "DELTA_CDC", + "ICEBERG_ADD_FILES", "Read", "Write", ] @@ -165,7 +167,8 @@ class Write(PTransform): BIGQUERY: ManagedTransforms.Urns.BIGQUERY_WRITE.urn, POSTGRES: ManagedTransforms.Urns.POSTGRES_WRITE.urn, MYSQL: ManagedTransforms.Urns.MYSQL_WRITE.urn, - SQL_SERVER: ManagedTransforms.Urns.SQL_SERVER_WRITE.urn + SQL_SERVER: ManagedTransforms.Urns.SQL_SERVER_WRITE.urn, + ICEBERG_ADD_FILES: ManagedTransforms.Urns.ICEBERG_ADD_FILES.urn, } def __init__( diff --git a/sdks/python/apache_beam/yaml/standard_io.yaml b/sdks/python/apache_beam/yaml/standard_io.yaml index 9dc3eff38b1e..b80e4ff7cc42 100644 --- a/sdks/python/apache_beam/yaml/standard_io.yaml +++ b/sdks/python/apache_beam/yaml/standard_io.yaml @@ -710,7 +710,7 @@ underlying_provider: type: beamJar transforms: - 'IcebergAddFiles': 'beam:schematransform:iceberg_add_files:v1' + 'IcebergAddFiles': 'beam:schematransform:org.apache.beam:iceberg_add_files:v1' config: gradle_target: 'sdks:java:io:expansion-service:shadowJar' diff --git a/sdks/standard_expansion_services.yaml b/sdks/standard_expansion_services.yaml index c6e1654bcd1f..51f9dac41ea4 100644 --- a/sdks/standard_expansion_services.yaml +++ b/sdks/standard_expansion_services.yaml @@ -54,6 +54,7 @@ - 'beam:schematransform:org.apache.beam:iceberg_cdc_read:v1' - 'beam:schematransform:org.apache.beam:delta_lake_read:v1' - 'beam:schematransform:org.apache.beam:delta_lake_cdc_read:v1' + - 'beam:schematransform:org.apache.beam:iceberg_add_files:v1' - gradle_target: 'sdks:java:io:messaging-expansion-service:shadowJar' destinations: From 980326afed0d619121f7ba37f5e890601dfebf79 Mon Sep 17 00:00:00 2001 From: Ahmed Abualsaud Date: Mon, 26 Oct 2026 15:20:58 -0700 Subject: [PATCH 2/3] adjust changes --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e6c78023f4be..1ad11f06d1b0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -68,7 +68,7 @@ * (Java) FirestoreIO: added `RpcQosOptions.Builder.withRampupThrottlingDisabled()`, mirroring the existing DatastoreIO option, to opt out of the 500/50/5 write ramp-up ([#27170](https://github.com/apache/beam/issues/27170)). * Added support for attributing BigQuery API quota and billing to a specific GCP project (quota project): `quota_project_id` parameter in `ReadFromBigQuery` or `--quota_project_id` pipeline option, honored by the Python BigQuery IO transforms (`WriteToBigQuery` with `FILE_LOADS` or `STREAMING_INSERTS`; not yet `STORAGE_WRITE_API`, which is Java-backed), `--bigQueryQuotaProjectId` pipeline option (Java), and `bigqueryio.WithQuotaProject` read/query option (Go) ([#37431](https://github.com/apache/beam/issues/37431)). * (Java/Python/Go) DebeziumIO: `readAsJson()` can now include `op` and the remaining Debezium `source` fields (e.g. MySQL `file`, `pos`, `row`, `ts_ms`; PostgreSQL `lsn`, `txId`, `ts_ms`) in `metadata` via `withFullMetadata(true)` / `full_metadata=True` / `debeziumio.FullMetadata()`. Off by default ([#40701](https://github.com/apache/beam/issues/40701)). -* [IcebergIO] AddFiles is now available through the Managed API with `Managed.write(Managed.ICEBERG_ADD_FILES)` (Java/Python), with URN `beam:schematransform:org.apache.beam:iceberg_add_files:v1` ([#XXXXX](https://github.com/apache/beam/pull/XXXXX)). +* [IcebergIO] AddFiles is now available through the Managed API with `Managed.write(Managed.ICEBERG_ADD_FILES)` (Java/Python), with URN `beam:schematransform:org.apache.beam:iceberg_add_files:v1` ([#40827](https://github.com/apache/beam/issues/40827)). ## New Features / Improvements From 7d0617fe5ca576c55cdf4bd65b949ff92914bb0d Mon Sep 17 00:00:00 2001 From: Ahmed Abualsaud Date: Tue, 27 Oct 2026 10:59:56 -0700 Subject: [PATCH 3/3] add doc changes and managed_replacement --- .../IO_Iceberg_Integration_Tests.json | 2 +- CHANGES.md | 3 +- .../pipeline/v1/external_transforms.proto | 2 +- .../apache/beam/sdk/io/iceberg/AddFiles.java | 31 +++- .../beam/sdk/io/iceberg/AddFilesIT.java | 153 +++++++++--------- sdks/python/apache_beam/yaml/standard_io.yaml | 7 +- sdks/standard_expansion_services.yaml | 1 - 7 files changed, 106 insertions(+), 93 deletions(-) diff --git a/.github/trigger_files/IO_Iceberg_Integration_Tests.json b/.github/trigger_files/IO_Iceberg_Integration_Tests.json index 7ab7bcd9a9c6..37dd25bf9029 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 1ad11f06d1b0..811095cbf2b3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -68,7 +68,7 @@ * (Java) FirestoreIO: added `RpcQosOptions.Builder.withRampupThrottlingDisabled()`, mirroring the existing DatastoreIO option, to opt out of the 500/50/5 write ramp-up ([#27170](https://github.com/apache/beam/issues/27170)). * Added support for attributing BigQuery API quota and billing to a specific GCP project (quota project): `quota_project_id` parameter in `ReadFromBigQuery` or `--quota_project_id` pipeline option, honored by the Python BigQuery IO transforms (`WriteToBigQuery` with `FILE_LOADS` or `STREAMING_INSERTS`; not yet `STORAGE_WRITE_API`, which is Java-backed), `--bigQueryQuotaProjectId` pipeline option (Java), and `bigqueryio.WithQuotaProject` read/query option (Go) ([#37431](https://github.com/apache/beam/issues/37431)). * (Java/Python/Go) DebeziumIO: `readAsJson()` can now include `op` and the remaining Debezium `source` fields (e.g. MySQL `file`, `pos`, `row`, `ts_ms`; PostgreSQL `lsn`, `txId`, `ts_ms`) in `metadata` via `withFullMetadata(true)` / `full_metadata=True` / `debeziumio.FullMetadata()`. Off by default ([#40701](https://github.com/apache/beam/issues/40701)). -* [IcebergIO] AddFiles is now available through the Managed API with `Managed.write(Managed.ICEBERG_ADD_FILES)` (Java/Python), with URN `beam:schematransform:org.apache.beam:iceberg_add_files:v1` ([#40827](https://github.com/apache/beam/issues/40827)). +* [IcebergIO] AddFiles is now available through the Managed API with `Managed.write(Managed.ICEBERG_ADD_FILES)` (Java/Python) ([#40827](https://github.com/apache/beam/issues/40827)). ## New Features / Improvements @@ -100,6 +100,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)). +* (YAML) `IcebergAddFiles` will be automatically replaced with the compatible managed transform. For older Beam versions, streaming update compatibility can be maintained by specifying the pipeline option `update_compatibility_version` ([#40827](https://github.com/apache/beam/issues/40827)). ## Deprecations diff --git a/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/external_transforms.proto b/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/external_transforms.proto index 22a95d1dcd6a..d08ef8fe6158 100644 --- a/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/external_transforms.proto +++ b/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/external_transforms.proto @@ -110,7 +110,7 @@ message ManagedTransforms { DELTA_LAKE_CDC_READ = 14 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:schematransform:org.apache.beam:delta_lake_cdc_read:v1"]; ICEBERG_ADD_FILES = 15 [(org.apache.beam.model.pipeline.v1.beam_urn) = - "beam:schematransform:org.apache.beam:iceberg_add_files:v1"]; + "beam:schematransform:iceberg_add_files:v1"]; } } 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 00cf77af1dca..770e7f40dc90 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 @@ -122,11 +122,26 @@ * path becomes a {@link DataFile} with partition metadata and column stats, batched into manifests * and committed as snapshots. * + *

    {@link AddFiles} is offered as a Managed transform. Its input is a {@link PCollection} of + * {@link Row}s with a single string field, the file's path, and its options are listed in {@link + * AddFilesSchemaTransformProvider.Configuration}: + * + *

    {@code
    + * Map config = Map.of(
    + *         "table", "db.sales",
    + *         "catalog_properties", Map.of("type", "rest", "uri", catalogUri),
    + *         "error_handling", Map.of("output", "errors"));
    + *
    + * PCollectionRowTuple result =
    + *     paths.apply(Managed.write(Managed.ICEBERG_ADD_FILES).withConfig(config));
    + * }
    + * *

    Outputs: {@code snapshots} (one row per commit), {@code errors} (one row per file that could * not be registered: {@code file}, {@code error}), and {@code dry_run_report} when a dry run is - * configured. + * configured. Through Managed, error rows are only output when {@code error_handling} is set, under + * the name it gives. * - *

    Schema evolution. With a {@link SchemaEvolutionConfig} whose options are set, a + *

    Schema evolution. With schema evolution options set ({@link SchemaEvolutionConfig}), a * pre-pass reads every Parquet footer, classifies the change each distinct file schema needs on the * table (add a column, relax a required column, promote a type), commits the allowed changes in one * transaction, and only then registers the files. Manifest entries are immutable, so this ordering @@ -142,12 +157,12 @@ * currently requires bounded input; unbounded input with options set is rejected at construction. * *

    {@code
    - * SchemaEvolutionConfig evolution =
    - *     SchemaEvolutionConfig.builder()
    - *         .setOptions(EnumSet.of(ALLOW_FIELD_ADDITION, ALLOW_TYPE_PROMOTION))
    - *         .setRequiredColumns(Collections.singleton("id"))
    - *         .build();
    - * paths.apply(new AddFiles(catalog, "db.sales", null, null, null, null, null, null, evolution));
    + * Map config = Map.of(
    + *         "table", "db.sales",
    + *         "catalog_properties", catalogProperties,
    + *         "schema_evolution_options", List.of("ALLOW_FIELD_ADDITION", "ALLOW_TYPE_PROMOTION"),
    + *         "required_columns", List.of("id"));
    + * paths.apply(Managed.write(Managed.ICEBERG_ADD_FILES).withConfig(config));
      * }
    * *

    Without options the table schema is never changed and files register as-is: columns the table diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesIT.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesIT.java index 7595483d58ac..9449e929a935 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesIT.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesIT.java @@ -21,6 +21,7 @@ import static org.apache.beam.sdk.io.FileIO.Write.defaultNaming; import static org.apache.beam.sdk.io.iceberg.IcebergUtils.beamSchemaToIcebergSchema; import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; +import static org.apache.beam.sdk.values.TypeDescriptors.rows; import static org.apache.beam.sdk.values.TypeDescriptors.strings; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.startsWith; @@ -43,7 +44,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -96,7 +96,6 @@ import org.apache.iceberg.util.Pair; import org.awaitility.Awaitility; import org.checkerframework.checker.nullness.qual.Nullable; -import org.joda.time.Duration; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -133,6 +132,7 @@ public class AddFilesIT { .addStringField("name") .addStringField("kind") .build(); + private static final Schema PATH_SCHEMA = Schema.builder().addStringField("path").build(); private static final Map LAKEHOUSE_PROPS = LakehouseTestCatalog.catalogProperties(); private Storage storage; @@ -477,17 +477,15 @@ public void testBatchParquetImportToTableInAdditionalLocation() throws IOExcepti List writtenFilePaths = writeParquetFiles(); Pipeline p = Pipeline.create(); PCollectionRowTuple tuple = - p.apply(Create.of(writtenFilePaths)) - .apply( - new AddFiles( - IcebergCatalogConfig.builder().setCatalogProperties(LAKEHOUSE_PROPS).build(), - destTableId.toString(), - null, - PARTITION_FIELDS, - null, - TABLE_PROPS, - null, - null)); + addFiles( + p.apply(Create.of(writtenFilePaths)), + ImmutableMap.of( + "table", + destTableId.toString(), + "partition_fields", + PARTITION_FIELDS, + "table_properties", + TABLE_PROPS)); PAssert.that(tuple.get("errors")).empty(); p.run().waitUntilFinish(); @@ -582,19 +580,14 @@ record -> } // run batch AddFiles + Map config = new HashMap<>(); + config.put("table", namespace + "." + destTableName); + config.put("table_properties", tableProps); + if (!isUIT) { + config.put("partition_fields", PARTITION_FIELDS); + } Pipeline p = Pipeline.create(); - PCollectionRowTuple tuple = - p.apply(Create.of(writtenFilePaths)) - .apply( - new AddFiles( - IcebergCatalogConfig.builder().setCatalogProperties(LAKEHOUSE_PROPS).build(), - namespace + "." + destTableName, - null, - isUIT ? null : PARTITION_FIELDS, - null, - tableProps, - null, - null)); + PCollectionRowTuple tuple = addFiles(p.apply(Create.of(writtenFilePaths)), config); PAssert.that(tuple.get("errors")).empty(); p.run().waitUntilFinish(); @@ -631,24 +624,17 @@ public void testBatchParquetImportWithSchemaEvolution() throws IOException { .collect(Collectors.toList()); assertEquals(20, writtenFilePaths.size()); - SchemaEvolutionConfig evolution = - SchemaEvolutionConfig.builder() - .setOptions(EnumSet.of(SchemaEvolutionOption.ALLOW_FIELD_ADDITION)) - .build(); Pipeline p = Pipeline.create(); PCollectionRowTuple tuple = - p.apply(Create.of(writtenFilePaths)) - .apply( - new AddFiles( - IcebergCatalogConfig.builder().setCatalogProperties(LAKEHOUSE_PROPS).build(), - namespace + "." + destTableName, - null, - null, - null, - TABLE_PROPS, - null, - null, - evolution)); + addFiles( + p.apply(Create.of(writtenFilePaths)), + ImmutableMap.of( + "table", + namespace + "." + destTableName, + "table_properties", + TABLE_PROPS, + "schema_evolution_options", + Arrays.asList("ALLOW_FIELD_ADDITION"))); PAssert.that(tuple.get("errors")).empty(); p.run().waitUntilFinish(); @@ -751,17 +737,10 @@ public void testBatchImportOfParquetFilesWithoutExtension() throws IOException { Pipeline p = Pipeline.create(); PCollectionRowTuple tuple = - p.apply(Create.of(inputs)) - .apply( - new AddFiles( - IcebergCatalogConfig.builder().setCatalogProperties(LAKEHOUSE_PROPS).build(), - namespace + "." + destTableName, - null, - null, - null, - TABLE_PROPS, - null, - null)); + addFiles( + p.apply(Create.of(inputs)), + ImmutableMap.of( + "table", namespace + "." + destTableName, "table_properties", TABLE_PROPS)); PAssert.that(tuple.get("errors")) .satisfies( rows -> { @@ -810,24 +789,19 @@ public void testBatchImportOfRequiredPartitionColumnFromThePath() throws IOExcep } assertEquals(20, files.size()); - SchemaEvolutionConfig evolution = - SchemaEvolutionConfig.builder() - .setOptions(EnumSet.of(SchemaEvolutionOption.ALLOW_FIELD_ADDITION)) - .build(); Pipeline p = Pipeline.create(); PCollectionRowTuple tuple = - p.apply(Create.of(files)) - .apply( - new AddFiles( - IcebergCatalogConfig.builder().setCatalogProperties(LAKEHOUSE_PROPS).build(), - namespace + "." + destTableName, - parquetDir, - null, - null, - TABLE_PROPS, - null, - null, - evolution)); + addFiles( + p.apply(Create.of(files)), + ImmutableMap.of( + "table", + namespace + "." + destTableName, + "location_prefix", + parquetDir, + "table_properties", + TABLE_PROPS, + "schema_evolution_options", + Arrays.asList("ALLOW_FIELD_ADDITION"))); PAssert.that(tuple.get("errors")).empty(); p.run().waitUntilFinish(); @@ -969,7 +943,7 @@ private PipelineResult startAddFilesListener( options.setBlockOnRun(false); Pipeline p = Pipeline.create(options); - PCollectionRowTuple tuple = + PCollection paths = p.apply(PubsubIO.readStrings().fromTopic(notificationsTopic)) .apply(JsonToRow.withSchema(NOTIFICATION_SCHEMA)) .apply(Filter.by(row -> objectNameFilter.apply(row.getString("name")))) @@ -978,17 +952,21 @@ private PipelineResult startAddFilesListener( .via( row -> format("gs://%s/%s", row.getString("bucket"), row.getString("name")))) - .apply(Deduplicate.values()) - .apply( - new AddFiles( - IcebergCatalogConfig.builder().setCatalogProperties(LAKEHOUSE_PROPS).build(), - namespace + "." + destTableName, - null, - PARTITION_FIELDS, - null, - TABLE_PROPS, - 10, - Duration.standardSeconds(10))); + .apply(Deduplicate.values()); + PCollectionRowTuple tuple = + addFiles( + paths, + ImmutableMap.of( + "table", + namespace + "." + destTableName, + "partition_fields", + PARTITION_FIELDS, + "table_properties", + TABLE_PROPS, + "manifest_file_size", + 10, + "triggering_frequency_seconds", + 10)); PAssert.that(tuple.get("errors")).empty(); PipelineResult result = p.run(); @@ -997,4 +975,19 @@ private PipelineResult startAddFilesListener( Thread.sleep(10_000); return result; } + + /** Registers the paths through the Managed API, with error rows output under "errors". */ + private static PCollectionRowTuple addFiles( + PCollection paths, Map config) { + Map managedConfig = new HashMap<>(config); + managedConfig.put("catalog_properties", LAKEHOUSE_PROPS); + managedConfig.put("error_handling", ImmutableMap.of("output", "errors")); + return paths + .apply( + "ToRows", + MapElements.into(rows()) + .via(path -> Row.withSchema(PATH_SCHEMA).addValue(path).build())) + .setRowSchema(PATH_SCHEMA) + .apply(Managed.write(Managed.ICEBERG_ADD_FILES).withConfig(managedConfig)); + } } diff --git a/sdks/python/apache_beam/yaml/standard_io.yaml b/sdks/python/apache_beam/yaml/standard_io.yaml index b80e4ff7cc42..a53bedd6b300 100644 --- a/sdks/python/apache_beam/yaml/standard_io.yaml +++ b/sdks/python/apache_beam/yaml/standard_io.yaml @@ -710,9 +710,14 @@ underlying_provider: type: beamJar transforms: - 'IcebergAddFiles': 'beam:schematransform:org.apache.beam:iceberg_add_files:v1' + 'IcebergAddFiles': 'beam:schematransform:iceberg_add_files:v1' config: gradle_target: 'sdks:java:io:expansion-service:shadowJar' + managed_replacement: + # Following transforms may be replaced with equivalent managed transforms, + # if the pipelines 'updateCompatibilityBeamVersion' match the provided + # version. + 'IcebergAddFiles': '2.78.0' #Datadog - type: renaming diff --git a/sdks/standard_expansion_services.yaml b/sdks/standard_expansion_services.yaml index 51f9dac41ea4..c6e1654bcd1f 100644 --- a/sdks/standard_expansion_services.yaml +++ b/sdks/standard_expansion_services.yaml @@ -54,7 +54,6 @@ - 'beam:schematransform:org.apache.beam:iceberg_cdc_read:v1' - 'beam:schematransform:org.apache.beam:delta_lake_read:v1' - 'beam:schematransform:org.apache.beam:delta_lake_cdc_read:v1' - - 'beam:schematransform:org.apache.beam:iceberg_add_files:v1' - gradle_target: 'sdks:java:io:messaging-expansion-service:shadowJar' destinations: