This is an automated email from the ASF dual-hosted git repository.
chamikaramj 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 81b9188f409 Adds Delta Lake read support to Beam SQL (#39950)
81b9188f409 is described below
commit 81b9188f40919ae617a77db616c17543b2d9bb9e
Author: Chamikara Jayalath <[email protected]>
AuthorDate: Wed Sep 9 09:36:15 2026 -0700
Adds Delta Lake read support to Beam SQL (#39950)
---
.../beam_PostCommit_Java_Delta_IO_Dataflow.json | 2 +-
build.gradle.kts | 1 +
sdks/java/extensions/sql/delta/build.gradle | 50 +++
.../sql/meta/provider/delta/DeltaTable.java | 199 +++++++++
.../meta/provider/delta/DeltaTableProvider.java | 75 ++++
.../sql/meta/provider/delta/package-info.java | 20 +
.../sql/meta/provider/delta/BeamSqlDeltaTest.java | 475 +++++++++++++++++++++
.../provider/delta/DeltaTableProviderTest.java | 280 ++++++++++++
sdks/java/io/delta/build.gradle | 1 -
.../beam/sdk/io/delta/DeltaCDCSourceDoFn.java | 2 +-
.../java/org/apache/beam/sdk/io/delta/DeltaIO.java | 11 +-
.../apache/beam/sdk/io/delta/DeltaSourceDoFn.java | 30 +-
.../org/apache/beam/sdk/io/delta/DeltaIOIT.java | 17 +-
.../org/apache/beam/sdk/io/delta/DeltaIOTest.java | 145 +++++--
.../beam/sdk/io/delta/DeltaWriteTestUtils.java | 58 ++-
settings.gradle.kts | 3 +
16 files changed, 1304 insertions(+), 65 deletions(-)
diff --git a/.github/trigger_files/beam_PostCommit_Java_Delta_IO_Dataflow.json
b/.github/trigger_files/beam_PostCommit_Java_Delta_IO_Dataflow.json
index 44932dfca50..0ca37f8c8e2 100644
--- a/.github/trigger_files/beam_PostCommit_Java_Delta_IO_Dataflow.json
+++ b/.github/trigger_files/beam_PostCommit_Java_Delta_IO_Dataflow.json
@@ -1,5 +1,5 @@
{
"comment": "Modify this file in a trivial way to cause this test suite to
run.",
- "modification": 3,
+ "modification": 1,
"https://github.com/apache/beam/pull/39990": "removing dead code from
FnApiDoFnRunner"
}
diff --git a/build.gradle.kts b/build.gradle.kts
index 8de3f762fb7..cbdc35f3369 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -396,6 +396,7 @@ tasks.register("sqlPreCommit") {
dependsOn(":sdks:java:extensions:sql:expansion-service:build")
dependsOn(":sdks:java:extensions:sql:hcatalog:build")
dependsOn(":sdks:java:extensions:sql:iceberg:build")
+ dependsOn(":sdks:java:extensions:sql:delta:build")
dependsOn(":sdks:java:extensions:sql:jdbc:build")
dependsOn(":sdks:java:extensions:sql:jdbc:preCommit")
dependsOn(":sdks:java:extensions:sql:perf-tests:build")
diff --git a/sdks/java/extensions/sql/delta/build.gradle
b/sdks/java/extensions/sql/delta/build.gradle
new file mode 100644
index 00000000000..07cce842854
--- /dev/null
+++ b/sdks/java/extensions/sql/delta/build.gradle
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins { id 'org.apache.beam.module' }
+
+applyJavaNature(
+ automaticModuleName:
'org.apache.beam.sdk.extensions.sql.meta.provider.delta',
+ requireJavaVersion: JavaVersion.VERSION_17,
+)
+
+description = "Apache Beam :: SDKs :: Java :: Extensions :: SQL :: Delta Lake"
+ext.summary = "Delta Lake table provider for Beam SQL."
+
+dependencies {
+ implementation project(":sdks:java:extensions:sql")
+ implementation project(":sdks:java:core")
+ implementation project(":sdks:java:managed")
+ implementation project(":sdks:java:io:delta")
+ implementation library.java.jackson_databind
+ implementation library.java.jackson_core
+ implementation library.java.slf4j_api
+ implementation library.java.vendored_guava_32_1_2_jre
+ implementation library.java.vendored_calcite_1_40_0
+ // TODO(https://github.com/apache/beam/issues/21156): Determine how to build
without this dependency
+ provided "org.immutables:value:2.8.8"
+ permitUnusedDeclared "org.immutables:value:2.8.8"
+ permitUnusedDeclared project(":sdks:java:io:delta")
+ permitUnusedDeclared library.java.slf4j_api
+
+ testImplementation library.java.joda_time
+ testImplementation library.java.junit
+ testImplementation library.java.hadoop_common
+ testImplementation project(path: ":sdks:java:io:delta", configuration:
"testRuntimeMigration")
+ testImplementation project(path: ":runners:direct-java", configuration:
"shadow")
+}
diff --git
a/sdks/java/extensions/sql/delta/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/delta/DeltaTable.java
b/sdks/java/extensions/sql/delta/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/delta/DeltaTable.java
new file mode 100644
index 00000000000..290453a614c
--- /dev/null
+++
b/sdks/java/extensions/sql/delta/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/delta/DeltaTable.java
@@ -0,0 +1,199 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.beam.sdk.extensions.sql.meta.provider.delta;
+
+import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.beam.sdk.extensions.sql.TableUtils;
+import org.apache.beam.sdk.extensions.sql.meta.BeamSqlTableFilter;
+import org.apache.beam.sdk.extensions.sql.meta.DefaultTableFilter;
+import org.apache.beam.sdk.extensions.sql.meta.SchemaBaseBeamTable;
+import org.apache.beam.sdk.extensions.sql.meta.Table;
+import org.apache.beam.sdk.managed.Managed;
+import org.apache.beam.sdk.values.PBegin;
+import org.apache.beam.sdk.values.PCollection;
+import org.apache.beam.sdk.values.POutput;
+import org.apache.beam.sdk.values.Row;
+import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.rex.RexNode;
+import
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting;
+import
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+class DeltaTable extends SchemaBaseBeamTable {
+ @VisibleForTesting static final String VERSION_FIELD = "version";
+ @VisibleForTesting static final String TIMESTAMP_FIELD = "timestamp";
+ @VisibleForTesting static final String HADOOP_CONFIG_FIELD = "hadoop_config";
+ @VisibleForTesting static final String HADOOP_CONFIG_CAMEL_FIELD =
"hadoopConfig";
+
+ static final String BEAM_WRITE_PROPERTY = "beam.write.";
+ static final String BEAM_READ_PROPERTY = "beam.read.";
+
+ @VisibleForTesting final String tableLocation;
+ @VisibleForTesting final @Nullable Long version;
+ @VisibleForTesting final @Nullable String timestamp;
+ @VisibleForTesting final @Nullable Map<String, String> hadoopConfig;
+
+ DeltaTable(Table table) {
+ this(
+ checkArgumentNotNull(
+ table.getLocation(),
+ "Delta Lake table location must be specified (catalog-based tables
are not supported)."),
+ table);
+ }
+
+ DeltaTable(String tableLocation, Table table) {
+ super(table.getSchema());
+ this.schema = table.getSchema();
+ this.tableLocation = tableLocation;
+
+ Long parsedVersion = null;
+ String parsedTimestamp = null;
+ Map<String, String> parsedHadoopConfig = new HashMap<>();
+
+ ObjectNode properties = table.getProperties();
+ for (Map.Entry<String, JsonNode> property : properties.properties()) {
+ String key = property.getKey();
+ String lowerKey = key.toLowerCase();
+ JsonNode val = property.getValue();
+
+ if (lowerKey.startsWith(BEAM_WRITE_PROPERTY)) {
+ // TODO: Support writing to Delta Lake tables once a Delta Lake sink is
+ // available.
+ throw new IllegalArgumentException(
+ String.format(
+ "Beam write property '%s' is not supported. Writing to Delta
Lake tables is currently not supported.",
+ key));
+ } else if (lowerKey.startsWith(BEAM_READ_PROPERTY)) {
+ // none supported yet
+ throw new IllegalArgumentException("Unknown Beam read property: " +
key);
+ } else if (lowerKey.equalsIgnoreCase(VERSION_FIELD)) {
+ parsedVersion = parseVersion(val);
+ } else if (lowerKey.equalsIgnoreCase(TIMESTAMP_FIELD)) {
+ parsedTimestamp = val.asText();
+ } else if (lowerKey.equalsIgnoreCase(HADOOP_CONFIG_FIELD)
+ || lowerKey.equalsIgnoreCase(HADOOP_CONFIG_CAMEL_FIELD)) {
+ parseHadoopConfig(val, parsedHadoopConfig);
+ } else {
+ throw new IllegalArgumentException(String.format("Unknown property
'%s'", key));
+ }
+ }
+
+ if (parsedVersion != null && parsedTimestamp != null) {
+ throw new IllegalArgumentException("Cannot set both version and
timestamp.");
+ }
+
+ this.version = parsedVersion;
+ this.timestamp = parsedTimestamp;
+ this.hadoopConfig = parsedHadoopConfig.isEmpty() ? null :
parsedHadoopConfig;
+ }
+
+ private static Long parseVersion(JsonNode val) {
+ if (val.isNumber()) {
+ return val.asLong();
+ }
+ return Long.parseLong(val.asText());
+ }
+
+ private static void parseHadoopConfig(JsonNode val, Map<String, String>
targetMap) {
+ if (val.isObject()) {
+ Map<String, String> map =
+ TableUtils.getObjectMapper()
+ .convertValue(val, new TypeReference<Map<String, String>>() {});
+ if (map != null) {
+ targetMap.putAll(map);
+ }
+ } else if (val.isTextual()) {
+ try {
+ Map<String, String> map =
+ TableUtils.getObjectMapper()
+ .readValue(val.asText(), new TypeReference<Map<String,
String>>() {});
+ if (map != null) {
+ targetMap.putAll(map);
+ }
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Failed to parse hadoop_config
string as JSON", e);
+ }
+ }
+ }
+
+ @Override
+ public PCollection<Row> buildIOReader(PBegin begin) {
+ return begin
+ .apply(Managed.read(Managed.DELTA_LAKE).withConfig(getBaseConfig()))
+ .getSinglePCollection();
+ }
+
+ @Override
+ public PCollection<Row> buildIOReader(
+ PBegin begin, BeamSqlTableFilter filters, List<String> fieldNames) {
+ // TODO: Support predicate pushdown and column pruning when supported by
DeltaIO
+ // / Managed Delta
+ // Lake source.
+ String error = "%s does not support predicate/project push-down, yet
non-empty %s is passed.";
+ if (!(filters instanceof DefaultTableFilter)) {
+ throw new UnsupportedOperationException(
+ String.format(error, this.getClass().getName(), "'filters'"));
+ }
+ if (!fieldNames.isEmpty()) {
+ throw new UnsupportedOperationException(
+ String.format(error, this.getClass().getName(), "'fieldNames'"));
+ }
+ return buildIOReader(begin);
+ }
+
+ @Override
+ public POutput buildIOWriter(PCollection<Row> input) {
+ // TODO: Support writing to Delta Lake tables once a Delta Lake sink is
+ // available.
+ throw new UnsupportedOperationException(
+ "Writing to Delta Lake tables is currently not supported.");
+ }
+
+ @Override
+ public PCollection.IsBounded isBounded() {
+ return PCollection.IsBounded.BOUNDED;
+ }
+
+ @Override
+ public BeamSqlTableFilter constructFilter(List<RexNode> filter) {
+ // TODO: Support predicate pushdown when supported by DeltaIO / Managed
Delta
+ // Lake source.
+ return new DefaultTableFilter(filter);
+ }
+
+ private Map<String, Object> getBaseConfig() {
+ ImmutableMap.Builder<String, Object> managedConfigBuilder =
ImmutableMap.builder();
+ managedConfigBuilder.put("table", tableLocation);
+ if (version != null) {
+ managedConfigBuilder.put(VERSION_FIELD, version);
+ }
+ if (timestamp != null) {
+ managedConfigBuilder.put(TIMESTAMP_FIELD, timestamp);
+ }
+ if (hadoopConfig != null && !hadoopConfig.isEmpty()) {
+ managedConfigBuilder.put(HADOOP_CONFIG_FIELD, hadoopConfig);
+ }
+ return managedConfigBuilder.build();
+ }
+}
diff --git
a/sdks/java/extensions/sql/delta/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/delta/DeltaTableProvider.java
b/sdks/java/extensions/sql/delta/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/delta/DeltaTableProvider.java
new file mode 100644
index 00000000000..7e803825754
--- /dev/null
+++
b/sdks/java/extensions/sql/delta/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/delta/DeltaTableProvider.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.beam.sdk.extensions.sql.meta.provider.delta;
+
+import static
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument;
+
+import com.google.auto.service.AutoService;
+import org.apache.beam.sdk.extensions.sql.meta.BeamSqlTable;
+import org.apache.beam.sdk.extensions.sql.meta.Table;
+import
org.apache.beam.sdk.extensions.sql.meta.provider.InMemoryMetaTableProvider;
+import org.apache.beam.sdk.extensions.sql.meta.provider.TableProvider;
+
+/**
+ * {@link TableProvider} for Delta Lake tables.
+ *
+ * <p>A sample of Delta Lake table registration is:
+ *
+ * <pre>{@code
+ * CREATE EXTERNAL TABLE orders(
+ * id INTEGER,
+ * name VARCHAR,
+ * amount DOUBLE
+ * )
+ * TYPE 'delta'
+ * LOCATION '/path/to/delta/orders'
+ * TBLPROPERTIES '{"version": 1}'
+ * }</pre>
+ */
+@AutoService(TableProvider.class)
+public class DeltaTableProvider extends InMemoryMetaTableProvider {
+
+ @Override
+ public String getTableType() {
+ return "delta";
+ }
+
+ @Override
+ public void createTable(Table table) {
+ // TODO: Support catalog-based Delta Lake tables once Delta catalog
support is implemented.
+ checkArgument(
+ table.getLocation() != null,
+ "Delta Lake table location must be specified (catalog-based tables are
not supported).");
+ super.createTable(table);
+ }
+
+ @Override
+ public BeamSqlTable buildBeamSqlTable(Table table) {
+ // TODO: Support catalog-based Delta Lake tables once Delta catalog
support is implemented.
+ checkArgument(
+ table.getLocation() != null,
+ "Delta Lake table location must be specified (catalog-based tables are
not supported).");
+ return new DeltaTable(table);
+ }
+
+ @Override
+ public boolean supportsPartitioning(Table table) {
+ // TODO: Support partitioning when Delta Lake connector supports
partitioned reads.
+ return false;
+ }
+}
diff --git
a/sdks/java/extensions/sql/delta/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/delta/package-info.java
b/sdks/java/extensions/sql/delta/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/delta/package-info.java
new file mode 100644
index 00000000000..3f2eca1d856
--- /dev/null
+++
b/sdks/java/extensions/sql/delta/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/delta/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** Table schema for Delta Lake. */
+package org.apache.beam.sdk.extensions.sql.meta.provider.delta;
diff --git
a/sdks/java/extensions/sql/delta/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/delta/BeamSqlDeltaTest.java
b/sdks/java/extensions/sql/delta/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/delta/BeamSqlDeltaTest.java
new file mode 100644
index 00000000000..3ac9444e2f5
--- /dev/null
+++
b/sdks/java/extensions/sql/delta/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/delta/BeamSqlDeltaTest.java
@@ -0,0 +1,475 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.beam.sdk.extensions.sql.meta.provider.delta;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import io.delta.kernel.defaults.engine.DefaultEngine;
+import io.delta.kernel.engine.Engine;
+import io.delta.kernel.types.DateType;
+import io.delta.kernel.types.DoubleType;
+import io.delta.kernel.types.IntegerType;
+import io.delta.kernel.types.StringType;
+import io.delta.kernel.types.StructType;
+import io.delta.kernel.types.TimestampNTZType;
+import io.delta.kernel.types.TimestampType;
+import java.io.File;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.beam.sdk.Pipeline;
+import org.apache.beam.sdk.extensions.sql.BeamSqlCli;
+import org.apache.beam.sdk.extensions.sql.impl.BeamSqlEnv;
+import org.apache.beam.sdk.extensions.sql.impl.rel.BeamRelNode;
+import org.apache.beam.sdk.extensions.sql.impl.rel.BeamSqlRelUtils;
+import org.apache.beam.sdk.extensions.sql.meta.catalog.InMemoryCatalogManager;
+import org.apache.beam.sdk.io.delta.DeltaWriteTestUtils;
+import org.apache.beam.sdk.options.PipelineOptionsFactory;
+import org.apache.beam.sdk.schemas.Schema;
+import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes;
+import org.apache.beam.sdk.schemas.logicaltypes.Timestamp;
+import org.apache.beam.sdk.testing.PAssert;
+import org.apache.beam.sdk.testing.TestPipeline;
+import org.apache.beam.sdk.values.PCollection;
+import org.apache.beam.sdk.values.Row;
+import org.apache.hadoop.conf.Configuration;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration/functional tests for Delta Lake read support in Beam SQL. */
+@RunWith(JUnit4.class)
+public class BeamSqlDeltaTest {
+
+ @Rule public TestPipeline readPipeline = TestPipeline.create();
+ @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ private InMemoryCatalogManager catalogManager;
+ private BeamSqlCli cli;
+ private BeamSqlEnv sqlEnv;
+
+ private static final Schema PERSON_SCHEMA =
+ Schema.builder()
+ .addStringField("name")
+ .addInt32Field("age")
+ .addDoubleField("score")
+ .addStringField("country")
+ .build();
+
+ private static final StructType PERSON_DELTA_SCHEMA =
+ new StructType()
+ .add("name", StringType.STRING)
+ .add("age", IntegerType.INTEGER)
+ .add("score", DoubleType.DOUBLE)
+ .add("country", StringType.STRING);
+
+ private static final Row PERSON_1 =
+ Row.withSchema(PERSON_SCHEMA).addValues("Alice", 30, 95.5,
"USA").build();
+ private static final Row PERSON_2 =
+ Row.withSchema(PERSON_SCHEMA).addValues("Bob", 20, 70.0, "USA").build();
+ private static final Row PERSON_3 =
+ Row.withSchema(PERSON_SCHEMA).addValues("Charlie", 40, 85.0,
"Canada").build();
+
+ @Before
+ public void setUp() {
+ catalogManager = new InMemoryCatalogManager();
+ catalogManager.registerTableProvider(new DeltaTableProvider());
+ cli = new BeamSqlCli().catalogManager(catalogManager);
+ sqlEnv =
+ BeamSqlEnv.builder(catalogManager)
+ .setPipelineOptions(PipelineOptionsFactory.create())
+ .build();
+ }
+
+ private void createDeltaTable(File tableDir, List<Row> rows) throws
Exception {
+ createDeltaTable(tableDir, rows, PERSON_DELTA_SCHEMA);
+ }
+
+ private void createDeltaTable(File tableDir, List<Row> rows, StructType
deltaSchema)
+ throws Exception {
+ Engine engine = DefaultEngine.create(new Configuration());
+ DeltaWriteTestUtils.writeAppendCommit(
+ engine, tableDir.getAbsolutePath(), 0L, 100000000000L, deltaSchema,
rows);
+ }
+
+ private void createTwoVersionDeltaTable(File tableDir) throws Exception {
+ Engine engine = DefaultEngine.create(new Configuration());
+ Schema schema = Schema.builder().addField("name",
Schema.FieldType.STRING).build();
+ StructType deltaSchema = new StructType().add("name", StringType.STRING);
+
+ Row row1 = Row.withSchema(schema).addValues("v0-row1").build();
+ Row row2 = Row.withSchema(schema).addValues("v0-row2").build();
+ Row row3 = Row.withSchema(schema).addValues("v1-row3").build();
+
+ // Commit 0 (version 0 at timestamp 100000000000L)
+ DeltaWriteTestUtils.writeAppendCommit(
+ engine,
+ tableDir.getAbsolutePath(),
+ 0L,
+ 100000000000L,
+ deltaSchema,
+ Arrays.asList(row1, row2));
+
+ // Commit 1 (version 1 at timestamp 200000000000L)
+ DeltaWriteTestUtils.writeAppendCommit(
+ engine, tableDir.getAbsolutePath(), 1L, 200000000000L, deltaSchema,
Arrays.asList(row3));
+ }
+
+ @Test
+ public void testSelectAll() throws Exception {
+ File tableDir = tempFolder.newFolder("delta-table-all");
+ createDeltaTable(tableDir, Arrays.asList(PERSON_1, PERSON_2, PERSON_3));
+
+ sqlEnv.executeDdl(
+ String.format(
+ "CREATE EXTERNAL TABLE persons (\n"
+ + " name VARCHAR,\n"
+ + " age INTEGER,\n"
+ + " score DOUBLE,\n"
+ + " country VARCHAR\n"
+ + ")\n"
+ + "TYPE 'delta'\n"
+ + "LOCATION '%s'",
+ tableDir.getAbsolutePath()));
+
+ BeamRelNode relNode = sqlEnv.parseQuery("SELECT * FROM persons");
+ PCollection<Row> output = BeamSqlRelUtils.toPCollection(readPipeline,
relNode);
+
+ PAssert.that(output).containsInAnyOrder(PERSON_1, PERSON_2, PERSON_3);
+ readPipeline.run().waitUntilFinish();
+ }
+
+ @Test
+ public void testSelectWithProjection() throws Exception {
+ File tableDir = tempFolder.newFolder("delta-table-project");
+ createDeltaTable(tableDir, Arrays.asList(PERSON_1, PERSON_2, PERSON_3));
+
+ sqlEnv.executeDdl(
+ String.format(
+ "CREATE EXTERNAL TABLE persons (\n"
+ + " name VARCHAR,\n"
+ + " age INTEGER,\n"
+ + " score DOUBLE,\n"
+ + " country VARCHAR\n"
+ + ")\n"
+ + "TYPE 'delta'\n"
+ + "LOCATION '%s'",
+ tableDir.getAbsolutePath()));
+
+ BeamRelNode relNode = sqlEnv.parseQuery("SELECT name, score FROM persons");
+ PCollection<Row> output = BeamSqlRelUtils.toPCollection(readPipeline,
relNode);
+
+ Schema projectedSchema =
+
Schema.builder().addStringField("name").addDoubleField("score").build();
+
+ PAssert.that(output)
+ .containsInAnyOrder(
+ Row.withSchema(projectedSchema).addValues("Alice", 95.5).build(),
+ Row.withSchema(projectedSchema).addValues("Bob", 70.0).build(),
+ Row.withSchema(projectedSchema).addValues("Charlie",
85.0).build());
+
+ readPipeline.run().waitUntilFinish();
+ }
+
+ @Test
+ public void testSelectWithFilter() throws Exception {
+ File tableDir = tempFolder.newFolder("delta-table-filter");
+ createDeltaTable(tableDir, Arrays.asList(PERSON_1, PERSON_2, PERSON_3));
+
+ sqlEnv.executeDdl(
+ String.format(
+ "CREATE EXTERNAL TABLE persons (\n"
+ + " name VARCHAR,\n"
+ + " age INTEGER,\n"
+ + " score DOUBLE,\n"
+ + " country VARCHAR\n"
+ + ")\n"
+ + "TYPE 'delta'\n"
+ + "LOCATION '%s'",
+ tableDir.getAbsolutePath()));
+
+ BeamRelNode relNode = sqlEnv.parseQuery("SELECT * FROM persons WHERE score
>= 85.0");
+ PCollection<Row> output = BeamSqlRelUtils.toPCollection(readPipeline,
relNode);
+
+ PAssert.that(output).containsInAnyOrder(PERSON_1, PERSON_3);
+ readPipeline.run().waitUntilFinish();
+ }
+
+ @Test
+ public void testSelectWithAggregation() throws Exception {
+ File tableDir = tempFolder.newFolder("delta-table-agg");
+ createDeltaTable(tableDir, Arrays.asList(PERSON_1, PERSON_2, PERSON_3));
+
+ sqlEnv.executeDdl(
+ String.format(
+ "CREATE EXTERNAL TABLE persons (\n"
+ + " name VARCHAR,\n"
+ + " age INTEGER,\n"
+ + " score DOUBLE,\n"
+ + " country VARCHAR\n"
+ + ")\n"
+ + "TYPE 'delta'\n"
+ + "LOCATION '%s'",
+ tableDir.getAbsolutePath()));
+
+ BeamRelNode relNode =
+ sqlEnv.parseQuery(
+ "SELECT country, COUNT(*) as cnt, AVG(score) as avg_score FROM
persons GROUP BY country");
+ PCollection<Row> output = BeamSqlRelUtils.toPCollection(readPipeline,
relNode);
+
+ Schema aggSchema =
+ Schema.builder()
+ .addStringField("country")
+ .addInt64Field("cnt")
+ .addDoubleField("avg_score")
+ .build();
+
+ PAssert.that(output)
+ .containsInAnyOrder(
+ Row.withSchema(aggSchema).addValues("USA", 2L, 82.75).build(),
+ Row.withSchema(aggSchema).addValues("Canada", 1L, 85.0).build());
+
+ readPipeline.run().waitUntilFinish();
+ }
+
+ @Test
+ public void testReadWithVersionTimeTravel() throws Exception {
+ File tableDir = tempFolder.newFolder("delta-table-ver");
+ createTwoVersionDeltaTable(tableDir);
+
+ Schema schema = Schema.builder().addField("name",
Schema.FieldType.STRING).build();
+ Row row1 = Row.withSchema(schema).addValues("v0-row1").build();
+ Row row2 = Row.withSchema(schema).addValues("v0-row2").build();
+
+ sqlEnv.executeDdl(
+ String.format(
+ "CREATE EXTERNAL TABLE table_v0 (\n"
+ + " name VARCHAR\n"
+ + ")\n"
+ + "TYPE 'delta'\n"
+ + "LOCATION '%s'\n"
+ + "TBLPROPERTIES '{\"version\": 0}'",
+ tableDir.getAbsolutePath()));
+
+ BeamRelNode relNode = sqlEnv.parseQuery("SELECT * FROM table_v0");
+ PCollection<Row> output = BeamSqlRelUtils.toPCollection(readPipeline,
relNode);
+
+ PAssert.that(output).containsInAnyOrder(row1, row2);
+ readPipeline.run().waitUntilFinish();
+ }
+
+ @Test
+ public void testReadWithTimestampTimeTravel() throws Exception {
+ File tableDir = tempFolder.newFolder("delta-table-ts");
+ createTwoVersionDeltaTable(tableDir);
+
+ Schema schema = Schema.builder().addField("name",
Schema.FieldType.STRING).build();
+ Row row1 = Row.withSchema(schema).addValues("v0-row1").build();
+ Row row2 = Row.withSchema(schema).addValues("v0-row2").build();
+
+ String timestampV0 =
java.time.Instant.ofEpochMilli(150000000000L).toString();
+
+ sqlEnv.executeDdl(
+ String.format(
+ "CREATE EXTERNAL TABLE table_ts (\n"
+ + " name VARCHAR\n"
+ + ")\n"
+ + "TYPE 'delta'\n"
+ + "LOCATION '%s'\n"
+ + "TBLPROPERTIES '{\"timestamp\": \"%s\"}'",
+ tableDir.getAbsolutePath(), timestampV0));
+
+ BeamRelNode relNode = sqlEnv.parseQuery("SELECT * FROM table_ts");
+ PCollection<Row> output = BeamSqlRelUtils.toPCollection(readPipeline,
relNode);
+
+ PAssert.that(output).containsInAnyOrder(row1, row2);
+ readPipeline.run().waitUntilFinish();
+ }
+
+ @Test
+ public void testInsertIntoTableFails() throws Exception {
+ File tableDir = tempFolder.newFolder("delta-table-insert");
+ createDeltaTable(tableDir, Arrays.asList(PERSON_1));
+
+ sqlEnv.executeDdl(
+ String.format(
+ "CREATE EXTERNAL TABLE persons (\n"
+ + " name VARCHAR,\n"
+ + " age INTEGER,\n"
+ + " score DOUBLE,\n"
+ + " country VARCHAR\n"
+ + ")\n"
+ + "TYPE 'delta'\n"
+ + "LOCATION '%s'",
+ tableDir.getAbsolutePath()));
+
+ BeamRelNode insertRel =
+ sqlEnv.parseQuery("INSERT INTO persons VALUES ('Dave', 28, 88.0,
'UK')");
+
+ Pipeline pipeline = Pipeline.create();
+ UnsupportedOperationException exception =
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> BeamSqlRelUtils.toPCollection(pipeline, insertRel));
+
+ assertTrue(
+ exception
+ .getMessage()
+ .contains("Writing to Delta Lake tables is currently not
supported."));
+ }
+
+ @Test
+ public void testCreateCatalogFails() {
+ UnsupportedOperationException exception =
+ assertThrows(
+ UnsupportedOperationException.class,
+ () ->
+ cli.execute(
+ "CREATE CATALOG delta_cat TYPE delta PROPERTIES
('warehouse' = '/path')"));
+
+ assertTrue(
+ exception.getMessage().contains("Could not find type 'delta' for
catalog 'delta_cat'."));
+ }
+
+ @Test
+ public void testCreateTableWithoutLocationFails() {
+ assertThrows(
+ Exception.class,
+ () -> sqlEnv.executeDdl("CREATE EXTERNAL TABLE no_location (name
VARCHAR) TYPE 'delta'"));
+ }
+
+ @Test
+ public void testSelectDateAndTimestamp() throws Exception {
+ File tableDir = tempFolder.newFolder("delta-table-date-ts");
+ Engine engine = DefaultEngine.create(new Configuration());
+
+ Schema beamSchema =
+ Schema.builder()
+ .addInt32Field("id")
+ .addField("ts_col", Schema.FieldType.logicalType(Timestamp.MICROS))
+ .addField("date_col", Schema.FieldType.logicalType(SqlTypes.DATE))
+ .build();
+
+ StructType deltaSchema =
+ new StructType()
+ .add("id", IntegerType.INTEGER)
+ .add("ts_col", TimestampType.TIMESTAMP)
+ .add("date_col", DateType.DATE);
+
+ Instant ts = Instant.parse("2026-08-31T12:00:00.000000Z");
+ LocalDate date = LocalDate.of(2026, 8, 31);
+ Row inputRow = Row.withSchema(beamSchema).addValues(1, ts, date).build();
+
+ DeltaWriteTestUtils.writeAppendCommit(
+ engine,
+ tableDir.getAbsolutePath(),
+ 0L,
+ 100000000000L,
+ deltaSchema,
+ Arrays.asList(inputRow));
+
+ sqlEnv.executeDdl(
+ String.format(
+ "CREATE EXTERNAL TABLE date_ts_table (\n"
+ + " id INTEGER,\n"
+ + " ts_col TIMESTAMP,\n"
+ + " date_col DATE\n"
+ + ")\n"
+ + "TYPE 'delta'\n"
+ + "LOCATION '%s'",
+ tableDir.getAbsolutePath()));
+
+ BeamRelNode relNode = sqlEnv.parseQuery("SELECT id, ts_col, date_col FROM
date_ts_table");
+ PCollection<Row> output = BeamSqlRelUtils.toPCollection(readPipeline,
relNode);
+
+ Schema sqlOutputSchema =
+ Schema.builder()
+ .addInt32Field("id")
+ .addField("ts_col", Schema.FieldType.DATETIME)
+ .addField("date_col", Schema.FieldType.logicalType(SqlTypes.DATE))
+ .build();
+ Row expectedRow =
+ Row.withSchema(sqlOutputSchema)
+ .addValues(1, new org.joda.time.Instant(ts.toEpochMilli()), date)
+ .build();
+
+ PAssert.that(output).containsInAnyOrder(expectedRow);
+ readPipeline.run().waitUntilFinish();
+ }
+
+ @Test
+ public void testSelectTimestampNtz() throws Exception {
+ File tableDir = tempFolder.newFolder("delta-table-ts-ntz");
+ Engine engine = DefaultEngine.create(new Configuration());
+
+ Schema beamSchema =
+ Schema.builder()
+ .addInt32Field("id")
+ .addField("ts_ntz_col",
Schema.FieldType.logicalType(SqlTypes.DATETIME))
+ .build();
+
+ StructType deltaSchema =
+ new StructType()
+ .add("id", IntegerType.INTEGER)
+ .add("ts_ntz_col", TimestampNTZType.TIMESTAMP_NTZ);
+
+ LocalDateTime dt = LocalDateTime.of(2026, 8, 31, 12, 0, 0);
+ Row inputRow = Row.withSchema(beamSchema).addValues(1, dt).build();
+
+ DeltaWriteTestUtils.writeAppendCommit(
+ engine,
+ tableDir.getAbsolutePath(),
+ 0L,
+ 100000000000L,
+ deltaSchema,
+ Arrays.asList(inputRow));
+
+ sqlEnv.executeDdl(
+ String.format(
+ "CREATE EXTERNAL TABLE ts_ntz_table (\n"
+ + " id INTEGER,\n"
+ + " ts_ntz_col TIMESTAMP\n"
+ + ")\n"
+ + "TYPE 'delta'\n"
+ + "LOCATION '%s'",
+ tableDir.getAbsolutePath()));
+
+ BeamRelNode relNode = sqlEnv.parseQuery("SELECT id, ts_ntz_col FROM
ts_ntz_table");
+ PCollection<Row> output = BeamSqlRelUtils.toPCollection(readPipeline,
relNode);
+
+ Schema sqlOutputSchema =
+ Schema.builder()
+ .addInt32Field("id")
+ .addField("ts_ntz_col", Schema.FieldType.DATETIME)
+ .build();
+ Instant dtInst = dt.toInstant(java.time.ZoneOffset.UTC);
+ Row expectedRow =
+ Row.withSchema(sqlOutputSchema)
+ .addValues(1, new org.joda.time.Instant(dtInst.toEpochMilli()))
+ .build();
+
+ PAssert.that(output).containsInAnyOrder(expectedRow);
+ readPipeline.run().waitUntilFinish();
+ }
+}
diff --git
a/sdks/java/extensions/sql/delta/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/delta/DeltaTableProviderTest.java
b/sdks/java/extensions/sql/delta/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/delta/DeltaTableProviderTest.java
new file mode 100644
index 00000000000..b48defa0b23
--- /dev/null
+++
b/sdks/java/extensions/sql/delta/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/delta/DeltaTableProviderTest.java
@@ -0,0 +1,280 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.beam.sdk.extensions.sql.meta.provider.delta;
+
+import static org.apache.beam.sdk.schemas.Schema.toSchema;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+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.util.Collections;
+import java.util.stream.Stream;
+import org.apache.beam.sdk.extensions.sql.TableUtils;
+import org.apache.beam.sdk.extensions.sql.meta.BeamSqlTable;
+import org.apache.beam.sdk.extensions.sql.meta.BeamSqlTableFilter;
+import org.apache.beam.sdk.extensions.sql.meta.DefaultTableFilter;
+import org.apache.beam.sdk.extensions.sql.meta.ProjectSupport;
+import org.apache.beam.sdk.extensions.sql.meta.Table;
+import org.apache.beam.sdk.schemas.Schema;
+import org.apache.beam.sdk.values.PCollection;
+import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.rex.RexNode;
+import
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link DeltaTableProvider} and {@link DeltaTable}. */
+@RunWith(JUnit4.class)
+public class DeltaTableProviderTest {
+
+ private final DeltaTableProvider provider = new DeltaTableProvider();
+
+ private static Table fakeTable(String name, String location, String
properties) {
+ return Table.builder()
+ .name(name)
+ .comment(name + " table")
+ .location(location)
+ .schema(
+ Stream.of(
+ Schema.Field.nullable("id", Schema.FieldType.INT32),
+ Schema.Field.nullable("name", Schema.FieldType.STRING))
+ .collect(toSchema()))
+ .type("delta")
+ .properties(TableUtils.parseProperties(properties))
+ .build();
+ }
+
+ @Test
+ public void testGetTableType() {
+ assertEquals("delta", provider.getTableType());
+ }
+
+ @Test
+ public void testBuildBeamSqlTableBasic() {
+ Table table = fakeTable("my_table", "/path/to/delta/table", "{}");
+ BeamSqlTable sqlTable = provider.buildBeamSqlTable(table);
+
+ assertNotNull(sqlTable);
+ assertTrue(sqlTable instanceof DeltaTable);
+
+ DeltaTable deltaTable = (DeltaTable) sqlTable;
+ assertEquals("/path/to/delta/table", deltaTable.tableLocation);
+ assertNull(deltaTable.version);
+ assertNull(deltaTable.timestamp);
+ assertNull(deltaTable.hadoopConfig);
+ assertEquals(PCollection.IsBounded.BOUNDED, deltaTable.isBounded());
+ assertEquals(ProjectSupport.NONE, deltaTable.supportsProjects());
+ }
+
+ @Test
+ public void testBuildBeamSqlTableWithVersion() {
+ Table table = fakeTable("my_table", "/path/to/delta/table", "{\"version\":
5}");
+ BeamSqlTable sqlTable = provider.buildBeamSqlTable(table);
+
+ assertTrue(sqlTable instanceof DeltaTable);
+ DeltaTable deltaTable = (DeltaTable) sqlTable;
+ assertEquals(Long.valueOf(5L), deltaTable.version);
+ assertNull(deltaTable.timestamp);
+ }
+
+ @Test
+ public void testBuildBeamSqlTableWithTimestamp() {
+ Table table =
+ fakeTable("my_table", "/path/to/delta/table", "{\"timestamp\":
\"2026-05-20T15:43:26Z\"}");
+ BeamSqlTable sqlTable = provider.buildBeamSqlTable(table);
+
+ assertTrue(sqlTable instanceof DeltaTable);
+ DeltaTable deltaTable = (DeltaTable) sqlTable;
+ assertEquals("2026-05-20T15:43:26Z", deltaTable.timestamp);
+ assertNull(deltaTable.version);
+ }
+
+ @Test
+ public void testBuildBeamSqlTableWithHadoopConfig() {
+ Table table =
+ fakeTable(
+ "my_table",
+ "/path/to/delta/table",
+ "{\"hadoop_config\": {\"fs.gs.project.id\": \"my-project\",
\"foo\": \"bar\"}}");
+ BeamSqlTable sqlTable = provider.buildBeamSqlTable(table);
+
+ assertTrue(sqlTable instanceof DeltaTable);
+ DeltaTable deltaTable = (DeltaTable) sqlTable;
+ assertNotNull(deltaTable.hadoopConfig);
+ assertEquals("my-project",
deltaTable.hadoopConfig.get("fs.gs.project.id"));
+ assertEquals("bar", deltaTable.hadoopConfig.get("foo"));
+ }
+
+ @Test
+ public void testBuildBeamSqlTableWithoutLocationFails() {
+ Table table = fakeTable("my_table", null, "{}");
+ IllegalArgumentException exception =
+ assertThrows(IllegalArgumentException.class, () ->
provider.buildBeamSqlTable(table));
+ assertTrue(
+ exception
+ .getMessage()
+ .contains(
+ "Delta Lake table location must be specified (catalog-based
tables are not supported)."));
+ }
+
+ @Test
+ public void testCreateTableWithoutLocationFails() {
+ Table table = fakeTable("my_table", null, "{}");
+ IllegalArgumentException exception =
+ assertThrows(IllegalArgumentException.class, () ->
provider.createTable(table));
+ assertTrue(
+ exception
+ .getMessage()
+ .contains(
+ "Delta Lake table location must be specified (catalog-based
tables are not supported)."));
+ }
+
+ @Test
+ public void testBuildBeamSqlTableWithBothVersionAndTimestampFails() {
+ Table table =
+ fakeTable(
+ "my_table",
+ "/path/to/delta/table",
+ "{\"version\": 1, \"timestamp\": \"2026-05-20T15:43:26Z\"}");
+ IllegalArgumentException exception =
+ assertThrows(IllegalArgumentException.class, () ->
provider.buildBeamSqlTable(table));
+ assertTrue(exception.getMessage().contains("Cannot set both version and
timestamp."));
+ }
+
+ @Test
+ public void testBuildBeamSqlTableWithWritePropertyFails() {
+ Table table =
+ fakeTable(
+ "my_table",
+ "/path/to/delta/table",
+ "{\"beam.write.triggering_frequency_seconds\": 30}");
+ IllegalArgumentException exception =
+ assertThrows(IllegalArgumentException.class, () ->
provider.buildBeamSqlTable(table));
+ assertTrue(
+ exception
+ .getMessage()
+ .contains("Writing to Delta Lake tables is currently not
supported."));
+ }
+
+ @Test
+ public void testDirectDeltaTableConstructor() {
+ Table table = fakeTable("my_table", null, "{\"version\": 2}");
+ DeltaTable deltaTable = new DeltaTable("/direct/path", table);
+
+ assertEquals("/direct/path", deltaTable.tableLocation);
+ assertEquals(Long.valueOf(2L), deltaTable.version);
+ assertNull(deltaTable.timestamp);
+ assertNull(deltaTable.hadoopConfig);
+ }
+
+ @Test
+ public void testBuildBeamSqlTableWithCamelCaseHadoopConfig() {
+ Table table =
+ fakeTable(
+ "my_table",
+ "/path/to/delta/table",
+ "{\"hadoopConfig\": {\"fs.gs.project.id\": \"my-project\"}}");
+ BeamSqlTable sqlTable = provider.buildBeamSqlTable(table);
+
+ assertTrue(sqlTable instanceof DeltaTable);
+ DeltaTable deltaTable = (DeltaTable) sqlTable;
+ assertNotNull(deltaTable.hadoopConfig);
+ assertEquals("my-project",
deltaTable.hadoopConfig.get("fs.gs.project.id"));
+ }
+
+ @Test
+ public void testBuildBeamSqlTableWithUnknownBeamReadPropertyFails() {
+ Table table = fakeTable("my_table", "/path/to/delta/table",
"{\"beam.read.unsupported\": 123}");
+ IllegalArgumentException exception =
+ assertThrows(IllegalArgumentException.class, () ->
provider.buildBeamSqlTable(table));
+ assertTrue(
+ exception.getMessage().contains("Unknown Beam read property:
beam.read.unsupported"));
+ }
+
+ @Test
+ public void testBuildBeamSqlTableWithUnknownPropertyFails() {
+ Table table = fakeTable("my_table", "/path/to/delta/table",
"{\"unsupported_property\": 123}");
+ IllegalArgumentException exception =
+ assertThrows(IllegalArgumentException.class, () ->
provider.buildBeamSqlTable(table));
+ assertTrue(exception.getMessage().contains("Unknown property
'unsupported_property'"));
+ }
+
+ @Test
+ public void testSupportsPartitioning() {
+ Table table = fakeTable("my_table", "/path/to/delta/table", "{}");
+ assertFalse(provider.supportsPartitioning(table));
+ }
+
+ @Test
+ public void testAlterTableFails() {
+ assertThrows(UnsupportedOperationException.class, () ->
provider.alterTable("my_table"));
+ }
+
+ @Test
+ public void testBuildIOWriterFails() {
+ Table table = fakeTable("my_table", "/path/to/delta/table", "{}");
+ DeltaTable deltaTable = (DeltaTable) provider.buildBeamSqlTable(table);
+ UnsupportedOperationException exception =
+ assertThrows(UnsupportedOperationException.class, () ->
deltaTable.buildIOWriter(null));
+ assertTrue(
+ exception
+ .getMessage()
+ .contains("Writing to Delta Lake tables is currently not
supported."));
+ }
+
+ @Test
+ public void testConstructFilterReturnsDefaultTableFilter() {
+ Table table = fakeTable("my_table", "/path/to/delta/table", "{}");
+ DeltaTable deltaTable = (DeltaTable) provider.buildBeamSqlTable(table);
+ BeamSqlTableFilter filter =
deltaTable.constructFilter(Collections.emptyList());
+ assertTrue(filter instanceof DefaultTableFilter);
+ assertEquals(0, filter.numSupported());
+ }
+
+ @Test
+ public void testBuildIOReaderWithPushDownFails() {
+ Table table = fakeTable("my_table", "/path/to/delta/table", "{}");
+ DeltaTable deltaTable = (DeltaTable) provider.buildBeamSqlTable(table);
+
+ BeamSqlTableFilter nonDefaultFilter =
+ new BeamSqlTableFilter() {
+ @Override
+ public java.util.List<RexNode> getNotSupported() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public int numSupported() {
+ return 1;
+ }
+ };
+
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> deltaTable.buildIOReader(null, nonDefaultFilter,
Collections.emptyList()));
+
+ assertThrows(
+ UnsupportedOperationException.class,
+ () ->
+ deltaTable.buildIOReader(
+ null, new DefaultTableFilter(Collections.emptyList()),
ImmutableList.of("id")));
+ }
+}
diff --git a/sdks/java/io/delta/build.gradle b/sdks/java/io/delta/build.gradle
index a0de04ed854..2787c4bb606 100644
--- a/sdks/java/io/delta/build.gradle
+++ b/sdks/java/io/delta/build.gradle
@@ -43,7 +43,6 @@ dependencies {
permitUnusedDeclared library.java.delta_kernel_defaults
implementation library.java.hadoop_common
- implementation library.java.joda_time
// implementation library.java.slf4j_api
implementation "org.apache.parquet:parquet-column:$parquet_version"
implementation "org.apache.parquet:parquet-hadoop:$parquet_version"
diff --git
a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaCDCSourceDoFn.java
b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaCDCSourceDoFn.java
index 97d9c914a08..fe1f31e5be1 100644
---
a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaCDCSourceDoFn.java
+++
b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaCDCSourceDoFn.java
@@ -248,7 +248,7 @@ class DeltaCDCSourceDoFn extends DoFn<DeltaCDCReadTask,
Row> {
if (field.getName().equals(DeltaIO.COMMIT_VERSION_COLUMN)) {
value = task.getVersion();
} else if (field.getName().equals(DeltaIO.COMMIT_TIMESTAMP_COLUMN)) {
- value = new org.joda.time.Instant(task.getTimestamp());
+ value = java.time.Instant.ofEpochMilli(task.getTimestamp());
}
}
builder.addValue(value);
diff --git
a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaIO.java
b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaIO.java
index 4701ff59c4a..c63fdcacc1c 100644
--- a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaIO.java
+++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaIO.java
@@ -36,12 +36,15 @@ import io.delta.kernel.types.MapType;
import io.delta.kernel.types.StringType;
import io.delta.kernel.types.StructField;
import io.delta.kernel.types.StructType;
+import io.delta.kernel.types.TimestampNTZType;
import io.delta.kernel.types.TimestampType;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.apache.beam.sdk.annotations.Internal;
import org.apache.beam.sdk.schemas.Schema;
+import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes;
+import org.apache.beam.sdk.schemas.logicaltypes.Timestamp;
import org.apache.beam.sdk.transforms.Create;
import org.apache.beam.sdk.transforms.PTransform;
import org.apache.beam.sdk.transforms.ParDo;
@@ -197,9 +200,11 @@ public class DeltaIO {
} else if (deltaType instanceof BinaryType) {
return Schema.FieldType.BYTES;
} else if (deltaType instanceof TimestampType) {
- return Schema.FieldType.DATETIME;
+ return Schema.FieldType.logicalType(Timestamp.MICROS);
+ } else if (deltaType instanceof TimestampNTZType) {
+ return Schema.FieldType.logicalType(SqlTypes.DATETIME);
} else if (deltaType instanceof DateType) {
- return Schema.FieldType.DATETIME;
+ return Schema.FieldType.logicalType(SqlTypes.DATE);
} else if (deltaType instanceof ArrayType) {
DataType elementType = ((ArrayType) deltaType).getElementType();
return Schema.FieldType.iterable(convertToBeamFieldType(elementType));
@@ -230,7 +235,7 @@ public class DeltaIO {
} else if (col.equals(COMMIT_VERSION_COLUMN)) {
builder.addField(COMMIT_VERSION_COLUMN, Schema.FieldType.INT64);
} else if (col.equals(COMMIT_TIMESTAMP_COLUMN)) {
- builder.addField(COMMIT_TIMESTAMP_COLUMN, Schema.FieldType.DATETIME);
+ builder.addField(COMMIT_TIMESTAMP_COLUMN,
Schema.FieldType.logicalType(Timestamp.MICROS));
}
}
return builder.build();
diff --git
a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaSourceDoFn.java
b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaSourceDoFn.java
index fca19e30cb8..0b368739312 100644
---
a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaSourceDoFn.java
+++
b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaSourceDoFn.java
@@ -44,10 +44,15 @@ import io.delta.kernel.types.ShortType;
import io.delta.kernel.types.StringType;
import io.delta.kernel.types.StructField;
import io.delta.kernel.types.StructType;
+import io.delta.kernel.types.TimestampNTZType;
import io.delta.kernel.types.TimestampType;
import io.delta.kernel.utils.CloseableIterator;
import io.delta.kernel.utils.FileStatus;
import java.math.BigDecimal;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
@@ -250,10 +255,18 @@ class DeltaSourceDoFn extends DoFn<DeltaReadTask, Row> {
return row.getBinary(index);
} else if (type instanceof TimestampType) {
long microSeconds = row.getLong(index);
- return new org.joda.time.Instant(microSeconds / 1000L);
+ return Instant.ofEpochSecond(
+ Math.floorDiv(microSeconds, 1_000_000L),
+ Math.floorMod(microSeconds, 1_000_000L) * 1_000L);
+ } else if (type instanceof TimestampNTZType) {
+ long microSeconds = row.getLong(index);
+ return LocalDateTime.ofEpochSecond(
+ Math.floorDiv(microSeconds, 1_000_000L),
+ (int) (Math.floorMod(microSeconds, 1_000_000L) * 1_000L),
+ ZoneOffset.UTC);
} else if (type instanceof DateType) {
int daysSinceEpoch = row.getInt(index);
- return new org.joda.time.Instant(daysSinceEpoch * 86400000L);
+ return LocalDate.ofEpochDay(daysSinceEpoch);
} else if (type instanceof ArrayType) {
ArrayValue arrayValue = row.getArray(index);
int size = arrayValue.getSize();
@@ -312,11 +325,18 @@ class DeltaSourceDoFn extends DoFn<DeltaReadTask, Row> {
return vector.getBinary(index);
} else if (type instanceof TimestampType) {
long microSeconds = vector.getLong(index);
- return new org.joda.time.Instant(microSeconds / 1000L);
+ return Instant.ofEpochSecond(
+ Math.floorDiv(microSeconds, 1_000_000L),
+ Math.floorMod(microSeconds, 1_000_000L) * 1_000L);
+ } else if (type instanceof TimestampNTZType) {
+ long microSeconds = vector.getLong(index);
+ return LocalDateTime.ofEpochSecond(
+ Math.floorDiv(microSeconds, 1_000_000L),
+ (int) (Math.floorMod(microSeconds, 1_000_000L) * 1_000L),
+ ZoneOffset.UTC);
} else if (type instanceof DateType) {
- // Convert days since epoch to milliseconds since epoch.
int daysSinceEpoch = vector.getInt(index);
- return new org.joda.time.Instant(daysSinceEpoch * 24L * 60L * 60L *
1000L);
+ return LocalDate.ofEpochDay(daysSinceEpoch);
} else if (type instanceof ArrayType) {
ArrayValue arrayValue = vector.getArray(index);
int size = arrayValue.getSize();
diff --git
a/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOIT.java
b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOIT.java
index ab7fd8e24f4..602c9b7f151 100644
---
a/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOIT.java
+++
b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOIT.java
@@ -53,6 +53,7 @@ import org.apache.beam.sdk.extensions.gcp.options.GcpOptions;
import org.apache.beam.sdk.managed.Managed;
import org.apache.beam.sdk.options.ExperimentalOptions;
import org.apache.beam.sdk.schemas.Schema;
+import org.apache.beam.sdk.schemas.logicaltypes.Timestamp;
import org.apache.beam.sdk.testing.PAssert;
import org.apache.beam.sdk.testing.TestPipeline;
import org.apache.beam.sdk.transforms.DoFn;
@@ -61,7 +62,6 @@ import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.Row;
import
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap;
import org.apache.hadoop.conf.Configuration;
-import org.joda.time.Instant;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -386,7 +386,8 @@ public class DeltaIOIT {
.addField("name", Schema.FieldType.STRING)
.addField(DeltaIO.CHANGE_TYPE_COLUMN, Schema.FieldType.STRING)
.addField(DeltaIO.COMMIT_VERSION_COLUMN, Schema.FieldType.INT64)
- .addField(DeltaIO.COMMIT_TIMESTAMP_COLUMN,
Schema.FieldType.DATETIME)
+ .addField(
+ DeltaIO.COMMIT_TIMESTAMP_COLUMN,
Schema.FieldType.logicalType(Timestamp.MICROS))
.build();
StructType cdcWriteDeltaSchema =
new StructType()
@@ -398,15 +399,21 @@ public class DeltaIOIT {
Row cdcRow1 =
Row.withSchema(cdcWriteSchema)
- .addValues(0, "name_0", "delete", 1L, new Instant(123456789000L))
+ .addValues(0, "name_0", "delete", 1L,
java.time.Instant.ofEpochMilli(123456789000L))
.build();
Row cdcRow2 =
Row.withSchema(cdcWriteSchema)
- .addValues(1, "name_1", "update_preimage", 1L, new
Instant(123456789000L))
+ .addValues(
+ 1, "name_1", "update_preimage", 1L,
java.time.Instant.ofEpochMilli(123456789000L))
.build();
Row cdcRow3 =
Row.withSchema(cdcWriteSchema)
- .addValues(1, "name_1_updated", "update_postimage", 1L, new
Instant(123456789000L))
+ .addValues(
+ 1,
+ "name_1_updated",
+ "update_postimage",
+ 1L,
+ java.time.Instant.ofEpochMilli(123456789000L))
.build();
DeltaWriteTestUtils.writeCdcCommit(
diff --git
a/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java
b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java
index 1b7f73566c1..342c4b94761 100644
---
a/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java
+++
b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java
@@ -32,6 +32,7 @@ import io.delta.kernel.types.MapType;
import io.delta.kernel.types.StringType;
import io.delta.kernel.types.StructField;
import io.delta.kernel.types.StructType;
+import io.delta.kernel.types.TimestampNTZType;
import io.delta.kernel.types.TimestampType;
import java.io.File;
import java.nio.charset.StandardCharsets;
@@ -49,6 +50,8 @@ import org.apache.beam.sdk.io.delta.DeltaIO.ReadRows;
import org.apache.beam.sdk.io.parquet.ParquetIO;
import org.apache.beam.sdk.managed.Managed;
import org.apache.beam.sdk.schemas.Schema;
+import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes;
+import org.apache.beam.sdk.schemas.logicaltypes.Timestamp;
import org.apache.beam.sdk.testing.PAssert;
import org.apache.beam.sdk.testing.TestPipeline;
import org.apache.beam.sdk.transforms.Count;
@@ -62,7 +65,6 @@ import org.apache.beam.sdk.values.PCollectionRowTuple;
import org.apache.beam.sdk.values.Row;
import org.apache.beam.sdk.values.ValueKind;
import
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap;
-import org.joda.time.Instant;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
@@ -519,6 +521,7 @@ public class DeltaIOTest {
new StructField("boolean", BooleanType.BOOLEAN, false),
new StructField("binary", BinaryType.BINARY, false),
new StructField("timestamp", TimestampType.TIMESTAMP, false),
+ new StructField("timestamp_ntz",
TimestampNTZType.TIMESTAMP_NTZ, false),
new StructField("date", DateType.DATE, false),
new StructField("array", new ArrayType(StringType.STRING,
true), false),
new StructField(
@@ -542,8 +545,9 @@ public class DeltaIOTest {
.addField("double", Schema.FieldType.DOUBLE)
.addField("boolean", Schema.FieldType.BOOLEAN)
.addField("binary", Schema.FieldType.BYTES)
- .addField("timestamp", Schema.FieldType.DATETIME)
- .addField("date", Schema.FieldType.DATETIME)
+ .addField("timestamp",
Schema.FieldType.logicalType(Timestamp.MICROS))
+ .addField("timestamp_ntz",
Schema.FieldType.logicalType(SqlTypes.DATETIME))
+ .addField("date", Schema.FieldType.logicalType(SqlTypes.DATE))
.addField("array",
Schema.FieldType.iterable(Schema.FieldType.STRING))
.addField("map", Schema.FieldType.map(Schema.FieldType.STRING,
Schema.FieldType.INT32))
.addField("struct", Schema.FieldType.row(nestedSchema))
@@ -909,7 +913,8 @@ public class DeltaIOTest {
.addField("name", Schema.FieldType.STRING)
.addField(DeltaIO.CHANGE_TYPE_COLUMN, Schema.FieldType.STRING)
.addField(DeltaIO.COMMIT_VERSION_COLUMN, Schema.FieldType.INT64)
- .addField(DeltaIO.COMMIT_TIMESTAMP_COLUMN,
Schema.FieldType.DATETIME)
+ .addField(
+ DeltaIO.COMMIT_TIMESTAMP_COLUMN,
Schema.FieldType.logicalType(Timestamp.MICROS))
.build();
StructType cdcWriteDeltaSchema =
new StructType()
@@ -920,15 +925,20 @@ public class DeltaIOTest {
Row cdcRow1 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-1", "update_preimage", 1L, new
Instant(123456789000L))
+ .addValues(
+ "row-1", "update_preimage", 1L,
java.time.Instant.ofEpochMilli(123456789000L))
.build();
Row cdcRow2 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-1-updated", "update_postimage", 1L, new
Instant(123456789000L))
+ .addValues(
+ "row-1-updated",
+ "update_postimage",
+ 1L,
+ java.time.Instant.ofEpochMilli(123456789000L))
.build();
Row cdcRow3 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-2", "delete", 1L, new Instant(123456789000L))
+ .addValues("row-2", "delete", 1L,
java.time.Instant.ofEpochMilli(123456789000L))
.build();
DeltaWriteTestUtils.writeCdcCommit(
@@ -986,7 +996,8 @@ public class DeltaIOTest {
.addField("name", Schema.FieldType.STRING)
.addField(DeltaIO.CHANGE_TYPE_COLUMN, Schema.FieldType.STRING)
.addField(DeltaIO.COMMIT_VERSION_COLUMN, Schema.FieldType.INT64)
- .addField(DeltaIO.COMMIT_TIMESTAMP_COLUMN,
Schema.FieldType.DATETIME)
+ .addField(
+ DeltaIO.COMMIT_TIMESTAMP_COLUMN,
Schema.FieldType.logicalType(Timestamp.MICROS))
.build();
StructType cdcWriteDeltaSchema =
new StructType()
@@ -997,7 +1008,7 @@ public class DeltaIOTest {
Row cdcRow =
Row.withSchema(cdcWriteSchema)
- .addValues("row-3", "insert", 1L, new Instant(123456789000L))
+ .addValues("row-3", "insert", 1L,
java.time.Instant.ofEpochMilli(123456789000L))
.build();
Row appendRow = Row.withSchema(tableSchema).addValues("row-3").build();
@@ -1063,7 +1074,8 @@ public class DeltaIOTest {
.addField("name", Schema.FieldType.STRING)
.addField(DeltaIO.CHANGE_TYPE_COLUMN, Schema.FieldType.STRING)
.addField(DeltaIO.COMMIT_VERSION_COLUMN, Schema.FieldType.INT64)
- .addField(DeltaIO.COMMIT_TIMESTAMP_COLUMN,
Schema.FieldType.DATETIME)
+ .addField(
+ DeltaIO.COMMIT_TIMESTAMP_COLUMN,
Schema.FieldType.logicalType(Timestamp.MICROS))
.build();
StructType cdcWriteDeltaSchema =
new StructType()
@@ -1074,15 +1086,20 @@ public class DeltaIOTest {
Row cdcRow1 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-1", "update_preimage", 1L, new
Instant(123456789000L))
+ .addValues(
+ "row-1", "update_preimage", 1L,
java.time.Instant.ofEpochMilli(123456789000L))
.build();
Row cdcRow2 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-1-updated", "update_postimage", 1L, new
Instant(123456789000L))
+ .addValues(
+ "row-1-updated",
+ "update_postimage",
+ 1L,
+ java.time.Instant.ofEpochMilli(123456789000L))
.build();
Row cdcRow3 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-2", "delete", 1L, new Instant(123456789000L))
+ .addValues("row-2", "delete", 1L,
java.time.Instant.ofEpochMilli(123456789000L))
.build();
DeltaWriteTestUtils.writeCdcCommit(
@@ -1147,7 +1164,8 @@ public class DeltaIOTest {
.addField("name", Schema.FieldType.STRING)
.addField(DeltaIO.CHANGE_TYPE_COLUMN, Schema.FieldType.STRING)
.addField(DeltaIO.COMMIT_VERSION_COLUMN, Schema.FieldType.INT64)
- .addField(DeltaIO.COMMIT_TIMESTAMP_COLUMN,
Schema.FieldType.DATETIME)
+ .addField(
+ DeltaIO.COMMIT_TIMESTAMP_COLUMN,
Schema.FieldType.logicalType(Timestamp.MICROS))
.build();
StructType cdcWriteDeltaSchema =
new StructType()
@@ -1158,15 +1176,20 @@ public class DeltaIOTest {
Row cdcRow1 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-1", "update_preimage", 1L, new
Instant(123456789000L))
+ .addValues(
+ "row-1", "update_preimage", 1L,
java.time.Instant.ofEpochMilli(123456789000L))
.build();
Row cdcRow2 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-1-updated", "update_postimage", 1L, new
Instant(123456789000L))
+ .addValues(
+ "row-1-updated",
+ "update_postimage",
+ 1L,
+ java.time.Instant.ofEpochMilli(123456789000L))
.build();
Row cdcRow3 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-2", "delete", 1L, new Instant(123456789000L))
+ .addValues("row-2", "delete", 1L,
java.time.Instant.ofEpochMilli(123456789000L))
.build();
DeltaWriteTestUtils.writeCdcCommit(
@@ -1450,7 +1473,8 @@ public class DeltaIOTest {
.addField("name", Schema.FieldType.STRING)
.addField(DeltaIO.CHANGE_TYPE_COLUMN, Schema.FieldType.STRING)
.addField(DeltaIO.COMMIT_VERSION_COLUMN, Schema.FieldType.INT64)
- .addField(DeltaIO.COMMIT_TIMESTAMP_COLUMN,
Schema.FieldType.DATETIME)
+ .addField(
+ DeltaIO.COMMIT_TIMESTAMP_COLUMN,
Schema.FieldType.logicalType(Timestamp.MICROS))
.build();
StructType cdcWriteDeltaSchema =
new StructType()
@@ -1461,15 +1485,20 @@ public class DeltaIOTest {
Row cdcRow1 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-1", "update_preimage", 1L, new
Instant(123456789000L))
+ .addValues(
+ "row-1", "update_preimage", 1L,
java.time.Instant.ofEpochMilli(123456789000L))
.build();
Row cdcRow2 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-1-updated", "update_postimage", 1L, new
Instant(123456789000L))
+ .addValues(
+ "row-1-updated",
+ "update_postimage",
+ 1L,
+ java.time.Instant.ofEpochMilli(123456789000L))
.build();
Row cdcRow3 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-2", "delete", 1L, new Instant(123456789000L))
+ .addValues("row-2", "delete", 1L,
java.time.Instant.ofEpochMilli(123456789000L))
.build();
DeltaWriteTestUtils.writeCdcCommit(
@@ -1532,7 +1561,8 @@ public class DeltaIOTest {
.addField("name", Schema.FieldType.STRING)
.addField(DeltaIO.CHANGE_TYPE_COLUMN, Schema.FieldType.STRING)
.addField(DeltaIO.COMMIT_VERSION_COLUMN, Schema.FieldType.INT64)
- .addField(DeltaIO.COMMIT_TIMESTAMP_COLUMN,
Schema.FieldType.DATETIME)
+ .addField(
+ DeltaIO.COMMIT_TIMESTAMP_COLUMN,
Schema.FieldType.logicalType(Timestamp.MICROS))
.build();
StructType cdcWriteDeltaSchema =
new StructType()
@@ -1543,15 +1573,20 @@ public class DeltaIOTest {
Row cdcRow1 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-1", "update_preimage", 1L, new
Instant(200000000000L))
+ .addValues(
+ "row-1", "update_preimage", 1L,
java.time.Instant.ofEpochMilli(200000000000L))
.build();
Row cdcRow2 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-1-updated", "update_postimage", 1L, new
Instant(200000000000L))
+ .addValues(
+ "row-1-updated",
+ "update_postimage",
+ 1L,
+ java.time.Instant.ofEpochMilli(200000000000L))
.build();
Row cdcRow3 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-2", "delete", 1L, new Instant(200000000000L))
+ .addValues("row-2", "delete", 1L,
java.time.Instant.ofEpochMilli(200000000000L))
.build();
DeltaWriteTestUtils.writeCdcCommit(
@@ -1645,7 +1680,8 @@ public class DeltaIOTest {
.addField("name", Schema.FieldType.STRING)
.addField(DeltaIO.CHANGE_TYPE_COLUMN, Schema.FieldType.STRING)
.addField(DeltaIO.COMMIT_VERSION_COLUMN, Schema.FieldType.INT64)
- .addField(DeltaIO.COMMIT_TIMESTAMP_COLUMN,
Schema.FieldType.DATETIME)
+ .addField(
+ DeltaIO.COMMIT_TIMESTAMP_COLUMN,
Schema.FieldType.logicalType(Timestamp.MICROS))
.build();
StructType cdcWriteDeltaSchema =
new StructType()
@@ -1656,15 +1692,20 @@ public class DeltaIOTest {
Row cdcRow1 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-1", "update_preimage", 1L, new
Instant(200000000000L))
+ .addValues(
+ "row-1", "update_preimage", 1L,
java.time.Instant.ofEpochMilli(200000000000L))
.build();
Row cdcRow2 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-1-updated", "update_postimage", 1L, new
Instant(200000000000L))
+ .addValues(
+ "row-1-updated",
+ "update_postimage",
+ 1L,
+ java.time.Instant.ofEpochMilli(200000000000L))
.build();
Row cdcRow3 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-2", "delete", 1L, new Instant(200000000000L))
+ .addValues("row-2", "delete", 1L,
java.time.Instant.ofEpochMilli(200000000000L))
.build();
DeltaWriteTestUtils.writeCdcCommit(
@@ -1691,15 +1732,20 @@ public class DeltaIOTest {
// 4. Write parquet files for Version 3 (commit with updates and deletes)
Row cdcRow4 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-3", "update_preimage", 3L, new
Instant(400000000000L))
+ .addValues(
+ "row-3", "update_preimage", 3L,
java.time.Instant.ofEpochMilli(400000000000L))
.build();
Row cdcRow5 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-3-updated", "update_postimage", 3L, new
Instant(400000000000L))
+ .addValues(
+ "row-3-updated",
+ "update_postimage",
+ 3L,
+ java.time.Instant.ofEpochMilli(400000000000L))
.build();
Row cdcRow6 =
Row.withSchema(cdcWriteSchema)
- .addValues("row-1-updated", "delete", 3L, new
Instant(400000000000L))
+ .addValues("row-1-updated", "delete", 3L,
java.time.Instant.ofEpochMilli(400000000000L))
.build();
DeltaWriteTestUtils.writeCdcCommit(
@@ -1777,13 +1823,21 @@ public class DeltaIOTest {
private static final class FormatRowWithMetadata extends DoFn<Row, String> {
@ProcessElement
public void process(@Element Row row, OutputReceiver<String> out) {
- out.output(
- String.format(
- "%s:%s:v%d:t%d",
- row.getString("name"),
- row.getString(DeltaIO.CHANGE_TYPE_COLUMN),
- row.getInt64(DeltaIO.COMMIT_VERSION_COLUMN),
- row.getDateTime(DeltaIO.COMMIT_TIMESTAMP_COLUMN).getMillis()));
+ Object tsVal = row.getValue(DeltaIO.COMMIT_TIMESTAMP_COLUMN);
+ if (tsVal instanceof java.time.Instant) {
+ long millis = ((java.time.Instant) tsVal).toEpochMilli();
+ out.output(
+ String.format(
+ "%s:%s:v%d:t%d",
+ row.getString("name"),
+ row.getString(DeltaIO.CHANGE_TYPE_COLUMN),
+ row.getInt64(DeltaIO.COMMIT_VERSION_COLUMN),
+ millis));
+ } else {
+ throw new RuntimeException(
+ "Expected 'COMMIT_TIMESTAMP_COLUMN' to be of type
'java.time.Instant' but received: "
+ + tsVal);
+ }
}
}
@@ -1811,10 +1865,15 @@ public class DeltaIOTest {
private static final class FormatRowTimestampMetadata extends DoFn<Row,
String> {
@ProcessElement
public void process(@Element Row row, OutputReceiver<String> out) {
- out.output(
- row.getString("name")
- + ":"
- + row.getDateTime(DeltaIO.COMMIT_TIMESTAMP_COLUMN).getMillis());
+ Object tsVal = row.getValue(DeltaIO.COMMIT_TIMESTAMP_COLUMN);
+ if (tsVal instanceof java.time.Instant) {
+ long millis = ((java.time.Instant) tsVal).toEpochMilli();
+ out.output(row.getString("name") + ":" + millis);
+ } else {
+ throw new RuntimeException(
+ "Expected 'COMMIT_TIMESTAMP_COLUMN' to be of type
'java.time.Instant' but received: "
+ + tsVal);
+ }
}
}
}
diff --git
a/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaWriteTestUtils.java
b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaWriteTestUtils.java
index 55646de749f..90f987859d7 100644
---
a/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaWriteTestUtils.java
+++
b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaWriteTestUtils.java
@@ -31,11 +31,13 @@ import io.delta.kernel.engine.Engine;
import io.delta.kernel.internal.data.GenericRow;
import io.delta.kernel.types.BooleanType;
import io.delta.kernel.types.DataType;
+import io.delta.kernel.types.DateType;
import io.delta.kernel.types.LongType;
import io.delta.kernel.types.MapType;
import io.delta.kernel.types.StringType;
import io.delta.kernel.types.StructField;
import io.delta.kernel.types.StructType;
+import io.delta.kernel.types.TimestampNTZType;
import io.delta.kernel.types.TimestampType;
import io.delta.kernel.utils.CloseableIterable;
import io.delta.kernel.utils.CloseableIterator;
@@ -50,10 +52,9 @@ import java.util.Optional;
import javax.annotation.Nullable;
import org.apache.beam.sdk.schemas.Schema;
import org.apache.beam.sdk.values.Row;
-import org.joda.time.Instant;
/** Utility class for writing test commits (appends and CDC actions) to Delta
tables in tests. */
-final class DeltaWriteTestUtils {
+public final class DeltaWriteTestUtils {
private DeltaWriteTestUtils() {}
@@ -134,18 +135,63 @@ final class DeltaWriteTestUtils {
@Override
public int getInt(int rowId) {
+ if (dataType instanceof DateType) {
+ Object val = rows.get(rowId).getValue(fieldIndex);
+ if (val instanceof java.time.LocalDate) {
+ return (int) ((java.time.LocalDate) val).toEpochDay();
+ }
+ }
return rows.get(rowId).getInt32(fieldIndex);
}
@Override
public long getLong(int rowId) {
if (dataType instanceof TimestampType) {
- Instant instant =
rows.get(rowId).getDateTime(fieldIndex).toInstant();
- return instant.getMillis() * 1000L;
+ Object val = rows.get(rowId).getValue(fieldIndex);
+ if (val instanceof java.time.Instant) {
+ java.time.Instant inst = (java.time.Instant) val;
+ return inst.getEpochSecond() * 1_000_000L + inst.getNano() / 1000L;
+ } else {
+ throw new RuntimeException(
+ "Unexpected value for field " + rowId + " of type
'TimestampType': " + val);
+ }
+ }
+ if (dataType instanceof TimestampNTZType) {
+ Object val = rows.get(rowId).getValue(fieldIndex);
+ if (val instanceof java.time.LocalDateTime) {
+ java.time.Instant inst =
+ ((java.time.LocalDateTime)
val).toInstant(java.time.ZoneOffset.UTC);
+ return inst.getEpochSecond() * 1_000_000L + inst.getNano() / 1000L;
+ }
}
return rows.get(rowId).getInt64(fieldIndex);
}
+ @Override
+ public double getDouble(int rowId) {
+ return rows.get(rowId).getDouble(fieldIndex);
+ }
+
+ @Override
+ public float getFloat(int rowId) {
+ return rows.get(rowId).getFloat(fieldIndex);
+ }
+
+ @Override
+ public short getShort(int rowId) {
+ return rows.get(rowId).getInt16(fieldIndex);
+ }
+
+ @Override
+ public byte getByte(int rowId) {
+ return rows.get(rowId).getByte(fieldIndex);
+ }
+
+ @Override
+ public byte[] getBinary(int rowId) {
+ return rows.get(rowId).getBytes(fieldIndex);
+ }
+
@Override
public String getString(int rowId) {
return rows.get(rowId).getString(fieldIndex);
@@ -165,7 +211,7 @@ final class DeltaWriteTestUtils {
* @return the list of names of the written Parquet data files
* @throws Exception if any error occurs during write or commit
*/
- static List<String> writeAppendCommit(
+ public static List<String> writeAppendCommit(
Engine engine,
String tablePath,
long expectedVersion,
@@ -262,7 +308,7 @@ final class DeltaWriteTestUtils {
* @param cdcWriteSchema the schema used for writing the CDC files
* @throws Exception if any error occurs during write or commit
*/
- static void writeCdcCommit(
+ public static void writeCdcCommit(
Engine engine,
String tablePath,
long expectedVersion,
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 60d9ed6060d..33f7447f320 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -397,5 +397,8 @@ include("it:clickhouse")
findProject(":it:clickhouse")?.name = "clickhouse"
include("sdks:java:extensions:sql:iceberg")
findProject(":sdks:java:extensions:sql:iceberg")?.name = "iceberg"
+include("sdks:java:extensions:sql:delta")
+findProject(":sdks:java:extensions:sql:delta")?.name = "delta"
include("examples:java:iceberg")
findProject(":examples:java:iceberg")?.name = "iceberg"
+