diff --git a/CHANGES.md b/CHANGES.md index c4cf7e11f133..1fef9780020f 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) ([#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 debacc245d60..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 @@ -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 c97a20c6221b..06bbee362a65 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 @@ * 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 @@ -145,12 +160,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/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/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/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..a53bedd6b300 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