Skip to content

Commit ca66eda

Browse files
delta lake python/yaml (#39052)
* make changes to support delta * add transformprovider and yaml file * add managed * fix lint * Add Delta Lake source to the Java Managed API * match Cham's PR changes * remove delta lake entry from external transforms * set java home * add back import * remove 11 * Revert workflow changes * few test fixes * force yaml to 17 * fix another test * revert improvements for now * switch to only java version 17 --------- Co-authored-by: Chamikara Jayalath <chamikaramj@gmail.com>
1 parent 8610459 commit ca66eda

12 files changed

Lines changed: 217 additions & 16 deletions

File tree

.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,13 @@ jobs:
7474
uses: ./.github/actions/setup-environment-action
7575
with:
7676
python-version: default
77-
java-version: |
78-
17
79-
11
77+
java-version: '17'
8078
- name: Set up Cloud SDK
8179
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db
8280
- name: run PostCommit Yaml Xlang Direct script
8381
uses: ./.github/actions/gradle-command-self-hosted-action
8482
with:
8583
gradle-command: :sdks:python:postCommitYamlIntegrationTests -PyamlTestSet=${{ matrix.test_set }} -PbeamPythonExtra=p310_ml_test,yaml
86-
arguments: |
87-
-Pjava17Home=$JAVA_HOME_17_X64
8884
- name: Archive Python Test Results
8985
uses: actions/upload-artifact@v7
9086
if: failure()

.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,7 @@ jobs:
102102
- name: Setup environment
103103
uses: ./.github/actions/setup-environment-action
104104
with:
105-
java-version: |
106-
17
107-
11
105+
java-version: '17'
108106
python-version: ${{ matrix.python_version }}
109107
- name: Set PY_VER_CLEAN
110108
id: set_py_ver_clean
@@ -116,5 +114,3 @@ jobs:
116114
uses: ./.github/actions/gradle-command-self-hosted-action
117115
with:
118116
gradle-command: :sdks:python:test-suites:direct:crossLanguageWrapperValidationPreCommit --info
119-
arguments: |
120-
-Pjava17Home=$JAVA_HOME_17_X64

.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,12 @@ jobs:
8888
- name: Setup environment
8989
uses: ./.github/actions/setup-environment-action
9090
with:
91-
java-version: |
92-
17
93-
11
91+
java-version: '17'
9492
python-version: default
9593
- name: run PreCommit Yaml Xlang Direct script
9694
uses: ./.github/actions/gradle-command-self-hosted-action
9795
with:
9896
gradle-command: :sdks:python:yamlIntegrationTests -PbeamPythonExtra=p310_ml_test,yaml
99-
arguments: |
100-
-Pjava17Home=$JAVA_HOME_17_X64
10197
- name: Archive Python Test Results
10298
uses: actions/upload-artifact@v7
10399
if: failure()
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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.io.delta.DeltaReadSchemaTransformProvider.OUTPUT_TAG;
22+
23+
import java.io.File;
24+
import java.nio.charset.StandardCharsets;
25+
import java.nio.file.Files;
26+
import org.apache.avro.generic.GenericRecord;
27+
import org.apache.beam.sdk.extensions.avro.coders.AvroCoder;
28+
import org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils;
29+
import org.apache.beam.sdk.io.Compression;
30+
import org.apache.beam.sdk.io.FileIO;
31+
import org.apache.beam.sdk.io.parquet.ParquetIO;
32+
import org.apache.beam.sdk.schemas.Schema;
33+
import org.apache.beam.sdk.testing.PAssert;
34+
import org.apache.beam.sdk.testing.TestPipeline;
35+
import org.apache.beam.sdk.transforms.Create;
36+
import org.apache.beam.sdk.transforms.windowing.BoundedWindow;
37+
import org.apache.beam.sdk.transforms.windowing.PaneInfo;
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.junit.Rule;
42+
import org.junit.Test;
43+
import org.junit.rules.TemporaryFolder;
44+
import org.junit.runner.RunWith;
45+
import org.junit.runners.JUnit4;
46+
47+
/** Tests for {@link DeltaReadSchemaTransformProvider}. */
48+
@RunWith(JUnit4.class)
49+
public class DeltaReadSchemaTransformProviderTest {
50+
51+
@Rule public TestPipeline writePipeline = TestPipeline.create();
52+
@Rule public TestPipeline readPipeline = TestPipeline.create();
53+
@Rule public TemporaryFolder tempFolder = new TemporaryFolder();
54+
55+
@Test
56+
public void testBuildTransformWithRow() {
57+
java.util.Map<String, String> hadoopConfig = new java.util.HashMap<>();
58+
hadoopConfig.put("fs.gs.project.id", "test-project");
59+
60+
Row config =
61+
Row.withSchema(new DeltaReadSchemaTransformProvider().configurationSchema())
62+
.withFieldValue("table", "/path/to/table")
63+
.withFieldValue("version", 5L)
64+
.withFieldValue("timestamp", "2026-06-04T12:00:00Z")
65+
.withFieldValue("hadoop_config", hadoopConfig)
66+
.build();
67+
68+
new DeltaReadSchemaTransformProvider().from(config);
69+
}
70+
71+
@Test
72+
public void testSimpleScan() throws Exception {
73+
File tableDir = tempFolder.newFolder("delta-table-simple");
74+
75+
// 1. Write a Parquet file using Beam
76+
Schema schema = Schema.builder().addField("name", Schema.FieldType.STRING).build();
77+
Row row = Row.withSchema(schema).addValues("test-name").build();
78+
79+
org.apache.avro.Schema avroSchema = AvroUtils.toAvroSchema(schema);
80+
GenericRecord record = AvroUtils.toGenericRecord(row, avroSchema);
81+
82+
writePipeline
83+
.apply("Create Input", Create.of(record).withCoder(AvroCoder.of(avroSchema)))
84+
.apply(
85+
"Write Parquet",
86+
FileIO.<GenericRecord>write()
87+
.via(ParquetIO.sink(avroSchema))
88+
.to(tableDir.getAbsolutePath() + "/")
89+
.withNaming(
90+
(BoundedWindow window,
91+
PaneInfo paneInfo,
92+
int numShards,
93+
int shardIndex,
94+
Compression compression) -> "part-00000.parquet"));
95+
96+
writePipeline.run().waitUntilFinish();
97+
98+
File parquetFile = new File(tableDir, "part-00000.parquet");
99+
byte[] fileBytes = Files.readAllBytes(parquetFile.toPath());
100+
101+
// 2. Create the Delta log
102+
File logDir = new File(tableDir, "_delta_log");
103+
logDir.mkdirs();
104+
File commitFile = new File(logDir, "00000000000000000000.json");
105+
106+
String commitContent =
107+
"{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n"
108+
+ "{\"metaData\":{\"id\":\"test-id\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"name\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdAt\":123456789}}\n"
109+
+ "{\"add\":{\"path\":\"part-00000.parquet\",\"partitionValues\":{},\"size\":"
110+
+ fileBytes.length
111+
+ ",\"modificationTime\":123456789,\"dataChange\":true}}";
112+
113+
Files.write(commitFile.toPath(), commitContent.getBytes(StandardCharsets.UTF_8));
114+
115+
// 3. Read it using DeltaReadSchemaTransformProvider
116+
Configuration readConfig = Configuration.builder().setTable(tableDir.getAbsolutePath()).build();
117+
118+
PCollection<Row> output =
119+
PCollectionRowTuple.empty(readPipeline)
120+
.apply(new DeltaReadSchemaTransformProvider().from(readConfig))
121+
.get(OUTPUT_TAG);
122+
123+
PAssert.that(output).containsInAnyOrder(row);
124+
125+
readPipeline.run().waitUntilFinish();
126+
}
127+
}

sdks/java/io/expansion-service/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ dependencies {
9797

9898
runtimeOnly project(":sdks:java:io:datadog")
9999
runtimeOnly project(":sdks:java:io:mongodb")
100+
runtimeOnly project(":sdks:java:io:delta")
100101
runtimeOnly library.java.kafka_clients
101102
runtimeOnly library.java.slf4j_jdk14
102103

sdks/python/apache_beam/transforms/external.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
ManagedTransforms.Urns.MYSQL_WRITE.urn: _GCP_EXPANSION_SERVICE_JAR_TARGET,
8787
ManagedTransforms.Urns.SQL_SERVER_READ.urn: _GCP_EXPANSION_SERVICE_JAR_TARGET, # pylint: disable=line-too-long
8888
ManagedTransforms.Urns.SQL_SERVER_WRITE.urn: _GCP_EXPANSION_SERVICE_JAR_TARGET, # pylint: disable=line-too-long
89+
ManagedTransforms.Urns.DELTA_LAKE_READ.urn: _IO_EXPANSION_SERVICE_JAR_TARGET,
8990
}
9091

9192

sdks/python/apache_beam/transforms/managed.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,9 @@
8888
POSTGRES = "postgres"
8989
MYSQL = "mysql"
9090
SQL_SERVER = "sqlserver"
91+
DELTA = "delta"
9192

92-
__all__ = ["ICEBERG", "KAFKA", "BIGQUERY", "Read", "Write"]
93+
__all__ = ["ICEBERG", "KAFKA", "BIGQUERY", "DELTA", "Read", "Write"]
9394

9495

9596
class Read(PTransform):
@@ -102,6 +103,7 @@ class Read(PTransform):
102103
POSTGRES: ManagedTransforms.Urns.POSTGRES_READ.urn,
103104
MYSQL: ManagedTransforms.Urns.MYSQL_READ.urn,
104105
SQL_SERVER: ManagedTransforms.Urns.SQL_SERVER_READ.urn,
106+
DELTA: ManagedTransforms.Urns.DELTA_LAKE_READ.urn,
105107
}
106108

107109
def __init__(

sdks/python/apache_beam/yaml/integration_tests.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ def get_impl(self):
7373
None, lambda payload, components, context: BigEndianIntegerCoder())
7474

7575
import psycopg2
76+
import pyarrow as pa
77+
import pyarrow.parquet as pq
7678
import pytds
7779
import sqlalchemy
7880
import yaml
@@ -692,6 +694,26 @@ def temp_pubsub_emulator(project_id="apache-beam-testing"):
692694
yield created_topic_object.name
693695

694696

697+
@contextlib.contextmanager
698+
def temp_delta_table():
699+
with tempfile.TemporaryDirectory() as temp_dir:
700+
log_dir = os.path.join(temp_dir, "_delta_log")
701+
os.makedirs(log_dir, exist_ok=True)
702+
table_data = pa.table({"name": ["a", "b", "c"]})
703+
parquet_path = os.path.join(temp_dir, "part-00000.parquet")
704+
pq.write_table(table_data, parquet_path)
705+
file_size = os.path.getsize(parquet_path)
706+
commit_content = (
707+
'{"protocol":{"minReaderVersion":1,"minWriterVersion":2}}\n'
708+
'{"metaData":{"id":"test-id","format":{"provider":"parquet","options":{}},"schemaString":"{\\"type\\":\\"struct\\",\\"fields\\":[{\\"name\\":\\"name\\",\\"type\\":\\"string\\",\\"nullable\\":true,\\"metadata\\":{}}]}","partitionColumns":[],"configuration":{},"createdAt":123456789}}\n'
709+
f'{{"add":{{"path":"part-00000.parquet","partitionValues":{{}},"size":{file_size},"modificationTime":123456789,"dataChange":true}}}}\n'
710+
)
711+
commit_file = os.path.join(log_dir, "00000000000000000000.json")
712+
with open(commit_file, "w") as f:
713+
f.write(commit_content)
714+
yield temp_dir
715+
716+
695717
def replace_recursive(spec, vars):
696718
"""Recursively replaces string placeholders in a spec with values from vars.
697719

sdks/python/apache_beam/yaml/standard_io.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
'WriteToTFRecord': 'apache_beam.yaml.yaml_io.write_to_tfrecord'
118118
'ReadFromMongoDB': 'apache_beam.yaml.yaml_io.read_from_mongodb'
119119
'WriteToMongoDB': 'apache_beam.yaml.yaml_io.write_to_mongodb'
120+
'ReadFromDelta': 'apache_beam.yaml.yaml_io.read_from_delta'
120121

121122
# General File Formats
122123
# Declared as a renaming transform to avoid exposing all
@@ -479,3 +480,4 @@
479480
'WriteToMongoDB': 'beam:schematransform:org.apache.beam:mongodb_write:v1'
480481
config:
481482
gradle_target: 'sdks:java:io:expansion-service:shadowJar'
483+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
fixtures:
19+
- name: DELTA_TABLE
20+
type: "apache_beam.yaml.integration_tests.temp_delta_table"
21+
22+
pipelines:
23+
- pipeline:
24+
type: chain
25+
transforms:
26+
- type: ReadFromDelta
27+
config:
28+
table: "{DELTA_TABLE}"
29+
- type: AssertEqual
30+
config:
31+
elements:
32+
- {name: "a"}
33+
- {name: "b"}
34+
- {name: "c"}

0 commit comments

Comments
 (0)