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 aea45221b65 Managed Iceberg AddFiles (#40827)
aea45221b65 is described below
commit aea45221b65a0c2e6fef2bf5c8f0f6f3b0753dd1
Author: Ahmed Abualsaud <[email protected]>
AuthorDate: Tue Oct 27 16:57:50 2026 -0700
Managed Iceberg AddFiles (#40827)
---
CHANGES.md | 2 +
.../model/pipeline/v1/external_transforms.proto | 2 +
.../org/apache/beam/sdk/io/iceberg/AddFiles.java | 31 +++--
.../iceberg/AddFilesSchemaTransformProvider.java | 21 ++-
.../iceberg/IcebergSchemaTransformTranslation.java | 15 ++
.../org/apache/beam/sdk/io/iceberg/AddFilesIT.java | 153 ++++++++++-----------
.../AddFilesSchemaTransformProviderTest.java | 55 ++++++++
.../IcebergSchemaTransformTranslationTest.java | 68 +++++++++
.../java/org/apache/beam/sdk/managed/Managed.java | 7 +
sdks/python/apache_beam/transforms/external.py | 1 +
sdks/python/apache_beam/transforms/managed.py | 5 +-
sdks/python/apache_beam/yaml/standard_io.yaml | 5 +
12 files changed, 275 insertions(+), 90 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index c4cf7e11f13..1fef9780020 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 [...]
* (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)
([#40827](https://github.com/apache/beam/issues/40827)).
## New Features / Improvements
@@ -100,6 +101,7 @@
* (Python) `--dataflow_endpoint` now defaults to `''` instead of
`https://dataflow.googleapis.com`. When unset, the Google Cloud client library
resolves the Dataflow endpoint and honors `GOOGLE_CLOUD_UNIVERSE_DOMAIN`, so
Dataflow jobs can be submitted in non-default universes. Code that reads
`GoogleCloudOptions.dataflow_endpoint` should handle an empty value
([#40569](https://github.com/apache/beam/issues/40569)).
* (Python) The `xgboost` extras now install `xgboost>=2.0.0,<4` and
`datatable>=1.1.0,<2`, moving both the lower and upper bounds for both. The
previous minimum `xgboost` and `datatable` versions (`1.6.0` and `1.0.0`
respectively) are no longer allowed
([#40694](https://github.com/apache/beam/issues/40694)).
* (Java) [IcebergIO] AddFiles (`IcebergAddFiles` in YAML) now carries the
destination table alongside each file through the registration chain, so that
one transform can register files into many tables. The coders of its internal
`PCollection`s change, which makes this an update incompatible change for
streaming pipelines that use it
([#40817](https://github.com/apache/beam/pull/40817)).
+* (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 debacc245d6..d08ef8fe615 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: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 c97a20c6221..06bbee362a6 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
@@ -125,11 +125,26 @@ import org.slf4j.LoggerFactory;
* path becomes a {@link DataFile} with partition metadata and column stats,
batched into manifests
* and committed as snapshots.
*
+ * <p>{@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}:
+ *
+ * <pre>{@code
+ * Map<String, Object> 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));
+ * }</pre>
+ *
* <p>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.
*
- * <p><b>Schema evolution.</b> With a {@link SchemaEvolutionConfig} whose
options are set, a
+ * <p><b>Schema evolution.</b> 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
@@ -145,12 +160,12 @@ import org.slf4j.LoggerFactory;
* currently requires bounded input; unbounded input with options set is
rejected at construction.
*
* <pre>{@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<String, Object> 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));
* }</pre>
*
* <p>Without options the table schema is never changed and files register
as-is: columns the table
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 c5cc758c555..dced27a3aa2 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.ERROR_TAG;
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.LinkedHashSet;
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.schemas.transforms.providers.ErrorHandling;
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 class AddFilesSchemaTransformProvider extends
TypedSchemaTransformProvide
@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 class AddFilesSchemaTransformProvider extends
TypedSchemaTransformProvide
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 801464010c6..15beb99a7a2 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 static
org.apache.beam.sdk.schemas.transforms.SchemaTransformTranslation.
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 class IcebergSchemaTransformTranslation {
}
}
+ static class AddFilesSchemaTransformTranslator
+ extends SchemaTransformPayloadTranslator<AddFilesSchemaTransform> {
+ @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 class IcebergSchemaTransformTranslation {
getTransformPayloadTranslators() {
return ImmutableMap.<Class<? extends PTransform>,
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/AddFilesIT.java
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesIT.java
index 7595483d58a..9449e929a93 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 java.lang.String.format;
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 com.google.cloud.storage.StorageOptions;
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.rest.RESTCatalog;
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<String, String> LAKEHOUSE_PROPS =
LakehouseTestCatalog.catalogProperties();
private Storage storage;
@@ -477,17 +477,15 @@ public class AddFilesIT {
List<String> 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 @@ public class AddFilesIT {
}
// run batch AddFiles
+ Map<String, Object> 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 class AddFilesIT {
.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 class AddFilesIT {
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 class AddFilesIT {
}
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 @@ public class AddFilesIT {
options.setBlockOnRun(false);
Pipeline p = Pipeline.create(options);
- PCollectionRowTuple tuple =
+ PCollection<String> 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 @@ public class AddFilesIT {
.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 @@ public class AddFilesIT {
Thread.sleep(10_000);
return result;
}
+
+ /** Registers the paths through the Managed API, with error rows output
under "errors". */
+ private static PCollectionRowTuple addFiles(
+ PCollection<String> paths, Map<String, Object> config) {
+ Map<String, Object> 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/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 3b58af20154..463ee24ab55 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 class AddFilesSchemaTransformProviderTest {
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.<String, Object>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<Record> 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 3c07c97b4a5..5a49eb7bb7f 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.ExternalTransforms.SchemaTransformPaylo
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<String, String> CATALOG_PROPERTIES =
ImmutableMap.<String, String>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 class IcebergSchemaTransformTranslationTest {
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.<String, Object>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<RunnerApi.PTransform> 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 27c647478e1..8ba003c9495 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<String, String> READ_TRANSFORMS =
@@ -124,6 +125,9 @@ public class Managed {
.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 class Managed {
*
href="https://beam.apache.org/releases/javadoc/current/org/apache/beam/sdk/io/kafka/KafkaIO.html">KafkaIO</a>
* <li>{@link Managed#BIGQUERY} : Write to GCP BigQuery tables <a
*
href="https://beam.apache.org/releases/javadoc/current/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIO.html">BigQueryIO</a>
+ * <li>{@link Managed#ICEBERG_ADD_FILES} : Register existing data files in
Apache Iceberg tables
+ * using <a
+ *
href="https://beam.apache.org/releases/javadoc/current/org/apache/beam/sdk/io/iceberg/AddFiles.html">AddFiles</a>
* </ul>
*/
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 33926281074..5c5dbcd58fd 100644
--- a/sdks/python/apache_beam/transforms/external.py
+++ b/sdks/python/apache_beam/transforms/external.py
@@ -88,6 +88,7 @@ MANAGED_TRANSFORM_URN_TO_JAR_TARGET_MAPPING = {
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 c252b10de5d..aeea50de56a 100644
--- a/sdks/python/apache_beam/transforms/managed.py
+++ b/sdks/python/apache_beam/transforms/managed.py
@@ -90,6 +90,7 @@ MYSQL = "mysql"
SQL_SERVER = "sqlserver"
DELTA = "delta"
DELTA_CDC = "delta_cdc"
+ICEBERG_ADD_FILES = "iceberg_add_files"
__all__ = [
"ICEBERG",
@@ -97,6 +98,7 @@ __all__ = [
"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 9dc3eff38b1..a53bedd6b30 100644
--- a/sdks/python/apache_beam/yaml/standard_io.yaml
+++ b/sdks/python/apache_beam/yaml/standard_io.yaml
@@ -713,6 +713,11 @@
'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