Skip to content

Commit 8d347ac

Browse files
committed
Write data using the Delta Kernel API.
1 parent 23ddd1a commit 8d347ac

1 file changed

Lines changed: 142 additions & 71 deletions

File tree

  • sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta

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

Lines changed: 142 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -17,41 +17,43 @@
1717
*/
1818
package org.apache.beam.sdk.io.delta;
1919

20-
import static org.junit.Assert.assertNotNull;
21-
2220
import com.google.cloud.storage.Blob;
23-
import com.google.cloud.storage.BlobId;
24-
import com.google.cloud.storage.BlobInfo;
2521
import com.google.cloud.storage.Storage;
2622
import com.google.cloud.storage.StorageOptions;
27-
import java.nio.charset.StandardCharsets;
23+
import io.delta.kernel.DataWriteContext;
24+
import io.delta.kernel.Operation;
25+
import io.delta.kernel.Table;
26+
import io.delta.kernel.Transaction;
27+
import io.delta.kernel.TransactionBuilder;
28+
import io.delta.kernel.TransactionCommitResult;
29+
import io.delta.kernel.data.ColumnVector;
30+
import io.delta.kernel.data.ColumnarBatch;
31+
import io.delta.kernel.data.FilteredColumnarBatch;
32+
import io.delta.kernel.defaults.engine.DefaultEngine;
33+
import io.delta.kernel.defaults.internal.data.DefaultColumnarBatch;
34+
import io.delta.kernel.engine.Engine;
35+
import io.delta.kernel.types.DataType;
36+
import io.delta.kernel.types.IntegerType;
37+
import io.delta.kernel.types.StringType;
38+
import io.delta.kernel.types.StructType;
39+
import io.delta.kernel.utils.CloseableIterable;
40+
import io.delta.kernel.utils.CloseableIterator;
41+
import io.delta.kernel.utils.DataFileStatus;
42+
import java.util.Collections;
2843
import java.util.HashMap;
2944
import java.util.List;
3045
import java.util.Map;
46+
import java.util.Optional;
3147
import java.util.stream.Collectors;
3248
import java.util.stream.IntStream;
33-
import org.apache.avro.generic.GenericRecord;
34-
import org.apache.beam.runners.direct.DirectOptions;
35-
import org.apache.beam.runners.direct.DirectRunner;
36-
import org.apache.beam.sdk.Pipeline;
37-
import org.apache.beam.sdk.extensions.avro.coders.AvroCoder;
38-
import org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils;
39-
import org.apache.beam.sdk.io.Compression;
40-
import org.apache.beam.sdk.io.FileIO;
41-
import org.apache.beam.sdk.io.parquet.ParquetIO;
4249
import org.apache.beam.sdk.managed.Managed;
43-
import org.apache.beam.sdk.options.PipelineOptionsFactory;
4450
import org.apache.beam.sdk.schemas.Schema;
4551
import org.apache.beam.sdk.testing.PAssert;
4652
import org.apache.beam.sdk.testing.TestPipeline;
47-
import org.apache.beam.sdk.transforms.Create;
48-
import org.apache.beam.sdk.transforms.MapElements;
49-
import org.apache.beam.sdk.transforms.windowing.BoundedWindow;
50-
import org.apache.beam.sdk.transforms.windowing.PaneInfo;
5153
import org.apache.beam.sdk.values.PCollection;
5254
import org.apache.beam.sdk.values.Row;
53-
import org.apache.beam.sdk.values.TypeDescriptor;
5455
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap;
56+
import org.apache.hadoop.conf.Configuration;
5557
import org.junit.After;
5658
import org.junit.Before;
5759
import org.junit.Rule;
@@ -91,8 +93,8 @@ public void setup() throws Exception {
9193

9294
String tempLocation = readPipeline.getOptions().getTempLocation();
9395
if (tempLocation != null && tempLocation.startsWith("gs://")) {
94-
org.apache.beam.sdk.extensions.gcp.util.gcsfs.GcsPath gcsPath = org.apache.beam.sdk.extensions.gcp.util.gcsfs.GcsPath
95-
.fromUri(tempLocation);
96+
org.apache.beam.sdk.extensions.gcp.util.gcsfs.GcsPath gcsPath =
97+
org.apache.beam.sdk.extensions.gcp.util.gcsfs.GcsPath.fromUri(tempLocation);
9698
bucket = gcsPath.getBucket();
9799
repoPrefix = gcsPath.getObject() + "/delta_io_it/" + testName.getMethodName() + "-" + salt;
98100
} else {
@@ -103,51 +105,119 @@ public void setup() throws Exception {
103105

104106
LOG.info("Generating Delta Lake repository at {}", repoPath);
105107

106-
// 1. Write Parquet file using a direct local pipeline
107-
DirectOptions setupOptions = PipelineOptionsFactory.as(DirectOptions.class);
108-
setupOptions.setRunner(DirectRunner.class);
109-
setupOptions.setBlockOnRun(true);
110-
Pipeline setupPipeline = Pipeline.create(setupOptions);
111-
112-
org.apache.avro.Schema avroSchema = AvroUtils.toAvroSchema(ROW_SCHEMA);
113-
setupPipeline
114-
.apply(Create.of(TEST_ROWS).withRowSchema(ROW_SCHEMA))
115-
.apply(
116-
MapElements.into(TypeDescriptor.of(GenericRecord.class))
117-
.via(AvroUtils.getRowToGenericRecordFunction(avroSchema)))
118-
.setCoder(AvroCoder.of(avroSchema))
119-
.apply(
120-
FileIO.<GenericRecord>write()
121-
.via(ParquetIO.sink(avroSchema))
122-
.to(repoPath + "/")
123-
.withNaming(
124-
(BoundedWindow window,
125-
PaneInfo paneInfo,
126-
int numShards,
127-
int shardIndex,
128-
Compression compression) -> "part-00000.parquet"));
129-
setupPipeline.run().waitUntilFinish();
130-
131-
// 2. Find written Parquet file to inspect its size
132-
BlobId parquetBlobId = BlobId.of(bucket, repoPrefix + "/part-00000.parquet");
133-
Blob parquetBlob = storage.get(parquetBlobId);
134-
assertNotNull("Parquet file not found on GCS: " + parquetBlobId, parquetBlob);
135-
long fileSize = parquetBlob.getSize();
136-
137-
// 3. Create the Delta log commit file
138-
String commitContent =
139-
"{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n"
140-
+ "{\"metaData\":{\"id\":\""
141-
+ salt
142-
+ "\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"id\\\",\\\"type\\\":\\\"integer\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}},{\\\"name\\\":\\\"name\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdAt\":123456789}}\n"
143-
+ "{\"add\":{\"path\":\"part-00000.parquet\",\"partitionValues\":{},\"size\":"
144-
+ fileSize
145-
+ ",\"modificationTime\":123456789,\"dataChange\":true}}";
146-
147-
BlobId commitBlobId = BlobId.of(bucket, repoPrefix + "/_delta_log/00000000000000000000.json");
148-
BlobInfo commitBlobInfo =
149-
BlobInfo.newBuilder(commitBlobId).setContentType("application/json").build();
150-
storage.create(commitBlobInfo, commitContent.getBytes(StandardCharsets.UTF_8));
108+
Configuration configuration = new Configuration();
109+
configuration.set("fs.gs.impl", "com.google.cloud.hadoop.fs.gcs.GoogleHadoopFileSystem");
110+
configuration.set(
111+
"fs.AbstractFileSystem.gs.impl", "com.google.cloud.hadoop.fs.gcs.GoogleHadoopFS");
112+
configuration.set("fs.gs.auth.type", "APPLICATION_DEFAULT");
113+
String project =
114+
readPipeline
115+
.getOptions()
116+
.as(org.apache.beam.sdk.extensions.gcp.options.GcpOptions.class)
117+
.getProject();
118+
if (project != null) {
119+
configuration.set("fs.gs.project.id", project);
120+
}
121+
122+
Engine engine = DefaultEngine.create(configuration);
123+
Table table = Table.forPath(engine, repoPath);
124+
125+
StructType deltaSchema =
126+
new StructType().add("id", IntegerType.INTEGER).add("name", StringType.STRING);
127+
128+
TransactionBuilder txnBuilder =
129+
table.createTransactionBuilder(engine, "DeltaIOIT", Operation.CREATE_TABLE);
130+
txnBuilder = txnBuilder.withSchema(engine, deltaSchema);
131+
Transaction txn = txnBuilder.build(engine);
132+
io.delta.kernel.data.Row txnState = txn.getTransactionState(engine);
133+
134+
ColumnVector idVector =
135+
new ColumnVector() {
136+
@Override
137+
public DataType getDataType() {
138+
return IntegerType.INTEGER;
139+
}
140+
141+
@Override
142+
public int getSize() {
143+
return TEST_ROWS.size();
144+
}
145+
146+
@Override
147+
public void close() {}
148+
149+
@Override
150+
public boolean isNullAt(int rowId) {
151+
return TEST_ROWS.get(rowId).getValue("id") == null;
152+
}
153+
154+
@Override
155+
public int getInt(int rowId) {
156+
return TEST_ROWS.get(rowId).getInt32("id");
157+
}
158+
};
159+
160+
ColumnVector nameVector =
161+
new ColumnVector() {
162+
@Override
163+
public DataType getDataType() {
164+
return StringType.STRING;
165+
}
166+
167+
@Override
168+
public int getSize() {
169+
return TEST_ROWS.size();
170+
}
171+
172+
@Override
173+
public void close() {}
174+
175+
@Override
176+
public boolean isNullAt(int rowId) {
177+
return TEST_ROWS.get(rowId).getValue("name") == null;
178+
}
179+
180+
@Override
181+
public String getString(int rowId) {
182+
return TEST_ROWS.get(rowId).getString("name");
183+
}
184+
};
185+
186+
ColumnVector[] vectors = new ColumnVector[] {idVector, nameVector};
187+
ColumnarBatch columnarBatch = new DefaultColumnarBatch(TEST_ROWS.size(), deltaSchema, vectors);
188+
FilteredColumnarBatch filteredBatch =
189+
new FilteredColumnarBatch(columnarBatch, Optional.empty());
190+
191+
CloseableIterator<FilteredColumnarBatch> data =
192+
io.delta.kernel.internal.util.Utils.toCloseableIterator(
193+
Collections.singletonList(filteredBatch).iterator());
194+
195+
CloseableIterator<FilteredColumnarBatch> physicalData =
196+
Transaction.transformLogicalData(engine, txnState, data, Collections.emptyMap());
197+
198+
DataWriteContext writeContext =
199+
Transaction.getWriteContext(engine, txnState, Collections.emptyMap());
200+
201+
CloseableIterator<DataFileStatus> dataFiles =
202+
engine
203+
.getParquetHandler()
204+
.writeParquetFiles(
205+
writeContext.getTargetDirectory(),
206+
physicalData,
207+
writeContext.getStatisticsColumns());
208+
209+
CloseableIterator<io.delta.kernel.data.Row> dataActions =
210+
Transaction.generateAppendActions(engine, txnState, dataFiles, writeContext);
211+
212+
CloseableIterable<io.delta.kernel.data.Row> dataActionsIterable =
213+
CloseableIterable.inMemoryIterable(dataActions);
214+
215+
TransactionCommitResult commitResult = txn.commit(engine, dataActionsIterable);
216+
217+
if (commitResult.getVersion() < 0) {
218+
throw new RuntimeException("Table creation/write failed");
219+
}
220+
151221
LOG.info("Successfully generated Delta Lake repository");
152222
}
153223

@@ -172,10 +242,11 @@ public void testReadDeltaLakeTable() {
172242
hadoopConfig.put("fs.gs.impl", "com.google.cloud.hadoop.fs.gcs.GoogleHadoopFileSystem");
173243
hadoopConfig.put(
174244
"fs.AbstractFileSystem.gs.impl", "com.google.cloud.hadoop.fs.gcs.GoogleHadoopFS");
175-
String project = readPipeline
176-
.getOptions()
177-
.as(org.apache.beam.sdk.extensions.gcp.options.GcpOptions.class)
178-
.getProject();
245+
String project =
246+
readPipeline
247+
.getOptions()
248+
.as(org.apache.beam.sdk.extensions.gcp.options.GcpOptions.class)
249+
.getProject();
179250
if (project != null) {
180251
hadoopConfig.put("fs.gs.project.id", project);
181252
}

0 commit comments

Comments
 (0)