Skip to content

Commit 782953e

Browse files
authored
Add Delta Lake source to the Java Managed API (#38902)
1 parent 6f30f14 commit 782953e

5 files changed

Lines changed: 190 additions & 0 deletions

File tree

model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/external_transforms.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ message ManagedTransforms {
8888
"beam:schematransform:org.apache.beam:sql_server_read:v1"];
8989
SQL_SERVER_WRITE = 12 [(org.apache.beam.model.pipeline.v1.beam_urn) =
9090
"beam:schematransform:org.apache.beam:sql_server_write:v1"];
91+
DELTA_LAKE_READ = 13 [(org.apache.beam.model.pipeline.v1.beam_urn) =
92+
"beam:schematransform:org.apache.beam:delta_lake_read:v1"];
9193
}
9294
}
9395

sdks/java/io/delta/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def parquet_version = "1.16.0"
3333

3434
dependencies {
3535
implementation project(path: ":sdks:java:core", configuration: "shadow")
36+
implementation project(path: ":model:pipeline", configuration: "shadow")
3637
implementation library.java.delta_kernel_api
3738
implementation library.java.delta_kernel_defaults
3839

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.sdk.io.delta;
19+
20+
import static org.apache.beam.sdk.io.delta.DeltaReadSchemaTransformProvider.Configuration;
21+
import static org.apache.beam.sdk.util.construction.BeamUrns.getUrn;
22+
23+
import com.google.auto.service.AutoService;
24+
import com.google.auto.value.AutoValue;
25+
import java.util.Collections;
26+
import java.util.List;
27+
import java.util.Map;
28+
import org.apache.beam.model.pipeline.v1.ExternalTransforms;
29+
import org.apache.beam.sdk.io.delta.DeltaReadSchemaTransformProvider.Configuration;
30+
import org.apache.beam.sdk.schemas.AutoValueSchema;
31+
import org.apache.beam.sdk.schemas.NoSuchSchemaException;
32+
import org.apache.beam.sdk.schemas.SchemaRegistry;
33+
import org.apache.beam.sdk.schemas.annotations.DefaultSchema;
34+
import org.apache.beam.sdk.schemas.annotations.SchemaFieldDescription;
35+
import org.apache.beam.sdk.schemas.transforms.SchemaTransform;
36+
import org.apache.beam.sdk.schemas.transforms.SchemaTransformProvider;
37+
import org.apache.beam.sdk.schemas.transforms.TypedSchemaTransformProvider;
38+
import org.apache.beam.sdk.values.PCollection;
39+
import org.apache.beam.sdk.values.PCollectionRowTuple;
40+
import org.apache.beam.sdk.values.Row;
41+
import org.checkerframework.checker.nullness.qual.Nullable;
42+
43+
/**
44+
* SchemaTransform implementation for {@link DeltaIO#readRows}. Reads records from Delta Lake and
45+
* outputs a {@link org.apache.beam.sdk.values.PCollection} of Beam {@link
46+
* org.apache.beam.sdk.values.Row}s.
47+
*/
48+
@AutoService(SchemaTransformProvider.class)
49+
public class DeltaReadSchemaTransformProvider extends TypedSchemaTransformProvider<Configuration> {
50+
static final String OUTPUT_TAG = "output";
51+
52+
@Override
53+
protected SchemaTransform from(Configuration configuration) {
54+
return new DeltaReadSchemaTransform(configuration);
55+
}
56+
57+
@Override
58+
public List<String> outputCollectionNames() {
59+
return Collections.singletonList(OUTPUT_TAG);
60+
}
61+
62+
@Override
63+
public String identifier() {
64+
return getUrn(ExternalTransforms.ManagedTransforms.Urns.DELTA_LAKE_READ);
65+
}
66+
67+
static class DeltaReadSchemaTransform extends SchemaTransform {
68+
private final Configuration configuration;
69+
70+
DeltaReadSchemaTransform(Configuration configuration) {
71+
this.configuration =
72+
java.util.Objects.requireNonNull(configuration, "configuration cannot be null");
73+
}
74+
75+
Row getConfigurationRow() {
76+
try {
77+
return SchemaRegistry.createDefault()
78+
.getToRowFunction(Configuration.class)
79+
.apply(configuration)
80+
.sorted()
81+
.toSnakeCase();
82+
} catch (NoSuchSchemaException e) {
83+
throw new RuntimeException(e);
84+
}
85+
}
86+
87+
@Override
88+
public PCollectionRowTuple expand(PCollectionRowTuple input) {
89+
DeltaIO.ReadRows read = DeltaIO.readRows().from(configuration.getTable());
90+
if (configuration.getVersion() != null) {
91+
read = read.withVersion(configuration.getVersion());
92+
}
93+
if (configuration.getTimestamp() != null) {
94+
read = read.withTimestamp(configuration.getTimestamp());
95+
}
96+
Map<String, String> hadoopConfig = configuration.getHadoopConfig();
97+
if (hadoopConfig != null) {
98+
read = read.withConfig(hadoopConfig);
99+
}
100+
101+
PCollection<Row> output = input.getPipeline().apply(read);
102+
103+
return PCollectionRowTuple.of(OUTPUT_TAG, output);
104+
}
105+
}
106+
107+
@DefaultSchema(AutoValueSchema.class)
108+
@AutoValue
109+
public abstract static class Configuration {
110+
static Builder builder() {
111+
return new AutoValue_DeltaReadSchemaTransformProvider_Configuration.Builder();
112+
}
113+
114+
@SchemaFieldDescription("Identifier of the Delta Lake table.")
115+
abstract String getTable();
116+
117+
@SchemaFieldDescription("Version of the Delta Lake table to read.")
118+
@Nullable
119+
abstract Long getVersion();
120+
121+
@SchemaFieldDescription("Timestamp of the Delta Lake table to read.")
122+
@Nullable
123+
abstract String getTimestamp();
124+
125+
@SchemaFieldDescription("Properties passed to the Hadoop Configuration.")
126+
@Nullable
127+
abstract Map<String, String> getHadoopConfig();
128+
129+
@AutoValue.Builder
130+
abstract static class Builder {
131+
abstract Builder setTable(String table);
132+
133+
abstract Builder setVersion(@Nullable Long version);
134+
135+
abstract Builder setTimestamp(@Nullable String timestamp);
136+
137+
abstract Builder setHadoopConfig(@Nullable Map<String, String> hadoopConfig);
138+
139+
abstract Configuration build();
140+
}
141+
}
142+
}

sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.apache.beam.sdk.io.FileIO;
4343
import org.apache.beam.sdk.io.delta.DeltaIO.ReadRows;
4444
import org.apache.beam.sdk.io.parquet.ParquetIO;
45+
import org.apache.beam.sdk.managed.Managed;
4546
import org.apache.beam.sdk.schemas.Schema;
4647
import org.apache.beam.sdk.testing.PAssert;
4748
import org.apache.beam.sdk.testing.TestPipeline;
@@ -52,6 +53,7 @@
5253
import org.apache.beam.sdk.transforms.windowing.PaneInfo;
5354
import org.apache.beam.sdk.values.PCollection;
5455
import org.apache.beam.sdk.values.Row;
56+
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap;
5557
import org.junit.Assert;
5658
import org.junit.Rule;
5759
import org.junit.Test;
@@ -387,6 +389,45 @@ private byte[] writeParquetFile(File file, Row row) throws Exception {
387389
return java.nio.file.Files.readAllBytes(file.toPath());
388390
}
389391

392+
@Test
393+
public void testManagedDeltaRead() throws Exception {
394+
File tableDir = tempFolder.newFolder("managed-delta-table");
395+
396+
// 1. Write a Parquet file to simulate a Delta table
397+
Schema schema = Schema.builder().addField("name", Schema.FieldType.STRING).build();
398+
Row row = Row.withSchema(schema).addValues("test-name").build();
399+
writeParquetFile(new File(tableDir, "part-00000.parquet"), row);
400+
401+
// 2. Create the Delta log
402+
File logDir = new File(tableDir, "_delta_log");
403+
logDir.mkdirs();
404+
File commitFile = new File(logDir, "00000000000000000000.json");
405+
406+
File parquetFile = new File(tableDir, "part-00000.parquet");
407+
byte[] fileBytes = Files.readAllBytes(parquetFile.toPath());
408+
409+
String commitContent =
410+
"{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n"
411+
+ "{\"metaData\":{\"id\":\"test-id\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"name\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdAt\":123456789}}\n"
412+
+ "{\"add\":{\"path\":\"part-00000.parquet\",\"partitionValues\":{},\"size\":"
413+
+ fileBytes.length
414+
+ ",\"modificationTime\":123456789,\"dataChange\":true}}";
415+
416+
Files.write(commitFile.toPath(), commitContent.getBytes(StandardCharsets.UTF_8));
417+
418+
// 3. Read it using Managed
419+
PCollection<Row> output =
420+
readPipeline
421+
.apply(
422+
Managed.read(Managed.DELTA_LAKE)
423+
.withConfig(ImmutableMap.of("table", tableDir.getAbsolutePath())))
424+
.getSinglePCollection();
425+
426+
PAssert.that(output).containsInAnyOrder(row);
427+
428+
readPipeline.run().waitUntilFinish();
429+
}
430+
390431
@Test
391432
@org.junit.Ignore("Manual integration test with external local table")
392433
public void testReadingLocalTable() throws Exception {

sdks/java/managed/src/main/java/org/apache/beam/sdk/managed/Managed.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ public class Managed {
9393

9494
// TODO: Dynamically generate a list of supported transforms
9595
public static final String ICEBERG = "iceberg";
96+
public static final String DELTA_LAKE = "delta";
9697
public static final String ICEBERG_CDC = "iceberg_cdc";
9798
public static final String KAFKA = "kafka";
9899
public static final String BIGQUERY = "bigquery";
@@ -104,6 +105,7 @@ public class Managed {
104105
public static final Map<String, String> READ_TRANSFORMS =
105106
ImmutableMap.<String, String>builder()
106107
.put(ICEBERG, getUrn(ExternalTransforms.ManagedTransforms.Urns.ICEBERG_READ))
108+
.put(DELTA_LAKE, getUrn(ExternalTransforms.ManagedTransforms.Urns.DELTA_LAKE_READ))
107109
.put(ICEBERG_CDC, getUrn(ExternalTransforms.ManagedTransforms.Urns.ICEBERG_CDC_READ))
108110
.put(KAFKA, getUrn(ExternalTransforms.ManagedTransforms.Urns.KAFKA_READ))
109111
.put(BIGQUERY, getUrn(ExternalTransforms.ManagedTransforms.Urns.BIGQUERY_READ))
@@ -128,6 +130,8 @@ public class Managed {
128130
* <ul>
129131
* <li>{@link Managed#ICEBERG} : Read from Apache Iceberg tables using <a
130132
* href="https://beam.apache.org/releases/javadoc/current/org/apache/beam/sdk/io/iceberg/IcebergIO.html">IcebergIO</a>
133+
* <li>{@link Managed#DELTA_LAKE} : Read from Delta Lake tables using <a
134+
* href="https://beam.apache.org/releases/javadoc/current/org/apache/beam/sdk/io/delta/DeltaIO.html">DeltaIO</a>
131135
* <li>{@link Managed#ICEBERG_CDC} : CDC Read from Apache Iceberg tables using <a
132136
* href="https://beam.apache.org/releases/javadoc/current/org/apache/beam/sdk/io/iceberg/IcebergIO.html">IcebergIO</a>
133137
* <li>{@link Managed#KAFKA} : Read from Apache Kafka topics using <a

0 commit comments

Comments
 (0)