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 a83754a5360 Implements the Delta Lake source with support for 
splitting (#38706)
a83754a5360 is described below

commit a83754a5360f1b5aab0e74da335548adde324e41
Author: Chamikara Jayalath <[email protected]>
AuthorDate: Wed Jun 3 11:20:20 2026 -0700

    Implements the Delta Lake source with support for splitting (#38706)
---
 sdks/java/io/delta/build.gradle                    |  44 ++
 .../org/apache/beam/sdk/io/delta/BeamEngine.java   |  55 ++
 .../beam/sdk/io/delta/BeamParquetHandler.java      | 376 +++++++++++
 .../beam/sdk/io/delta/CreateReadTasksDoFn.java     | 140 ++++
 .../java/org/apache/beam/sdk/io/delta/DeltaIO.java |  92 ++-
 .../apache/beam/sdk/io/delta/DeltaReadTask.java    |  88 +++
 .../beam/sdk/io/delta/DeltaReadTaskTracker.java    |  56 ++
 .../apache/beam/sdk/io/delta/DeltaSourceDoFn.java  | 455 +++++++++++++
 .../apache/beam/sdk/io/delta/SerializableRow.java  | 544 ++++++++++++++++
 .../beam/sdk/io/delta/SerializableStructType.java  |  69 ++
 .../org/apache/beam/sdk/io/delta/DeltaIOTest.java  | 712 ++++++++++++++++++++-
 .../beam/sdk/io/delta/SerializableRowTest.java     | 498 ++++++++++++++
 12 files changed, 3125 insertions(+), 4 deletions(-)

diff --git a/sdks/java/io/delta/build.gradle b/sdks/java/io/delta/build.gradle
index 617965b3bc4..c07aef6981b 100644
--- a/sdks/java/io/delta/build.gradle
+++ b/sdks/java/io/delta/build.gradle
@@ -26,6 +26,10 @@ applyJavaNature(
 description = "Apache Beam :: SDKs :: Java :: IO :: Delta Lake"
 ext.summary = "Integration with Delta Lake."
 
+// We need to override the GCS bigdataos connector version to prevent 
conflicts.
+def bigdataoss_gcs_connector_version = "4.0.4"
+
+def parquet_version = "1.16.0"
 
 dependencies {
     implementation project(path: ":sdks:java:core", configuration: "shadow")
@@ -35,5 +39,45 @@ dependencies {
     permitUnusedDeclared library.java.delta_kernel_api
     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"
+
+    // We need to override the GCS connector version to prevent conflicts with
+    // latest Hadoop.
+    implementation 
"com.google.cloud.bigdataoss:gcs-connector:$bigdataoss_gcs_connector_version"
+    implementation 
"com.google.cloud.bigdataoss:util-hadoop:$bigdataoss_gcs_connector_version"
+    implementation 
"com.google.cloud.bigdataoss:gcsio:$bigdataoss_gcs_connector_version"
+    implementation 
"com.google.cloud.bigdataoss:util:$bigdataoss_gcs_connector_version"
+    permitUnusedDeclared 
"com.google.cloud.bigdataoss:gcs-connector:$bigdataoss_gcs_connector_version"
+    permitUnusedDeclared 
"com.google.cloud.bigdataoss:util-hadoop:$bigdataoss_gcs_connector_version"
+    permitUnusedDeclared 
"com.google.cloud.bigdataoss:gcsio:$bigdataoss_gcs_connector_version"
+    permitUnusedDeclared 
"com.google.cloud.bigdataoss:util:$bigdataoss_gcs_connector_version"
+
+    // For Avro conversions
+    testImplementation project(":sdks:java:extensions:avro")
+
+    testImplementation library.java.avro
     testImplementation library.java.junit
+    testImplementation library.java.hamcrest
+    testImplementation "org.apache.parquet:parquet-avro:$parquet_version"
+    testImplementation project(":sdks:java:io:parquet")
+    testImplementation project(":sdks:java:managed")
+    testRuntimeOnly "org.yaml:snakeyaml:2.0"
+    testImplementation project(path: ":runners:direct-java", configuration: 
"shadow")
+}
+
+configurations.all {
+    // Exclude conflicting logging frameworks
+    exclude group: "org.apache.logging.log4j", module: "log4j-slf4j2-impl"
+    exclude group: "org.apache.logging.log4j", module: "log4j-slf4j-impl"
+    exclude group: "org.slf4j", module: "slf4j-reload4j"
+
+    // Force overriding for all configurations
+    resolutionStrategy.force 
"com.google.cloud.bigdataoss:gcs-connector:$bigdataoss_gcs_connector_version"
+    resolutionStrategy.force 
"com.google.cloud.bigdataoss:util-hadoop:$bigdataoss_gcs_connector_version"
+    resolutionStrategy.force 
"com.google.cloud.bigdataoss:gcsio:$bigdataoss_gcs_connector_version"
+    resolutionStrategy.force 
"com.google.cloud.bigdataoss:util:$bigdataoss_gcs_connector_version"
 }
diff --git 
a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/BeamEngine.java 
b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/BeamEngine.java
new file mode 100644
index 00000000000..de82d8d01b8
--- /dev/null
+++ 
b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/BeamEngine.java
@@ -0,0 +1,55 @@
+/*
+ * 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.io.delta;
+
+import io.delta.kernel.engine.Engine;
+import io.delta.kernel.engine.ExpressionHandler;
+import io.delta.kernel.engine.FileSystemClient;
+import io.delta.kernel.engine.JsonHandler;
+import io.delta.kernel.engine.ParquetHandler;
+
+/** A Beam specific {@link Engine} wrapper that provides a custom {@link 
ParquetHandler}. */
+public class BeamEngine implements Engine {
+  private final Engine delegate;
+  private final ParquetHandler parquetHandler;
+
+  public BeamEngine(Engine delegate, ParquetHandler parquetHandler) {
+    this.delegate = delegate;
+    this.parquetHandler = parquetHandler;
+  }
+
+  @Override
+  public ExpressionHandler getExpressionHandler() {
+    return delegate.getExpressionHandler();
+  }
+
+  @Override
+  public JsonHandler getJsonHandler() {
+    return delegate.getJsonHandler();
+  }
+
+  @Override
+  public FileSystemClient getFileSystemClient() {
+    return delegate.getFileSystemClient();
+  }
+
+  @Override
+  public ParquetHandler getParquetHandler() {
+    return parquetHandler;
+  }
+}
diff --git 
a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/BeamParquetHandler.java
 
b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/BeamParquetHandler.java
new file mode 100644
index 00000000000..0327bddac28
--- /dev/null
+++ 
b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/BeamParquetHandler.java
@@ -0,0 +1,376 @@
+/*
+ * 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.io.delta;
+
+import io.delta.kernel.data.FilteredColumnarBatch;
+import 
io.delta.kernel.defaults.internal.parquet.ParquetFileReader.BatchReadSupport;
+import io.delta.kernel.engine.FileReadResult;
+import io.delta.kernel.engine.ParquetHandler;
+import io.delta.kernel.expressions.Column;
+import io.delta.kernel.expressions.Predicate;
+import io.delta.kernel.types.MetadataColumnSpec;
+import io.delta.kernel.types.StructType;
+import io.delta.kernel.utils.CloseableIterator;
+import io.delta.kernel.utils.DataFileStatus;
+import io.delta.kernel.utils.FileStatus;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.Set;
+import org.apache.beam.sdk.io.range.OffsetRange;
+import org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
+import org.apache.parquet.column.page.PageReadStore;
+import org.apache.parquet.filter2.compat.FilterCompat;
+import org.apache.parquet.format.converter.ParquetMetadataConverter;
+import org.apache.parquet.hadoop.ParquetFileReader;
+import org.apache.parquet.hadoop.api.InitContext;
+import org.apache.parquet.hadoop.api.ReadSupport;
+import org.apache.parquet.hadoop.metadata.FileMetaData;
+import org.apache.parquet.hadoop.metadata.ParquetMetadata;
+import org.apache.parquet.hadoop.util.HadoopInputFile;
+import org.apache.parquet.io.ColumnIOFactory;
+import org.apache.parquet.io.MessageColumnIO;
+import org.apache.parquet.io.RecordReader;
+import org.apache.parquet.io.api.RecordMaterializer;
+import org.apache.parquet.schema.MessageType;
+
+/**
+ * A Beam specific {@link ParquetHandler} that delegates row group claiming to 
a {@link
+ * DeltaReadTaskTracker}.
+ */
+public class BeamParquetHandler implements ParquetHandler {
+  private final Configuration conf;
+  private final ParquetHandler delegate;
+  private final RestrictionTracker<OffsetRange, Long> tracker;
+  private static final long DEFAULT_START_RG_INDEX = 0L;
+
+  public BeamParquetHandler(
+      Configuration conf, ParquetHandler delegate, 
RestrictionTracker<OffsetRange, Long> tracker) {
+    this.conf = conf;
+    this.delegate = delegate;
+    this.tracker = tracker;
+  }
+
+  private boolean claimFailed = false;
+
+  /**
+   * A method that is expected to be called after the first file processing is 
done. It returns
+   * whether the last file process resulted in a claim failure. This allows 
the caller to skip
+   * trying to read the remaining files of the task which would result in 
claim failures for each
+   * row group within them.
+   *
+   * @return true, if the last file process resulted in a claim failure. 
Returns false otherwise.
+   */
+  public boolean hasClaimFailed() {
+    return claimFailed;
+  }
+
+  @Override
+  public CloseableIterator<FileReadResult> readParquetFiles(
+      CloseableIterator<FileStatus> fileIter,
+      StructType physicalSchema,
+      Optional<Predicate> predicate)
+      throws IOException {
+    return readParquetFiles(fileIter, physicalSchema, predicate, 
DEFAULT_START_RG_INDEX);
+  }
+
+  /**
+   * Reads Parquet files starting from a given row group index.
+   *
+   * <p>This takes the {@code RestrictionTracker} referenced by the current 
{@code ParquetReader}
+   * into consideration when reading by performing the following.
+   *
+   * <p>* Skips blocks of the set of files till the given start row group 
index or the start point
+   * of the {@code RestrictionTracker}, whatever is higher. * Invokes {@code 
tryClaim} when reading
+   * a specific block stops reading if a {@code tryClaim} fails. * Stops 
reading if the end of the
+   * range of the {@code RestrictionTracker} is reached.
+   *
+   * <p>If {@code tryClaim} fails during reading, subsequent {@code 
hasClaimFailed} calls will
+   * return {@code true}, so the caller can skip reading subsequent files that 
are in the range
+   * being considered for reading.
+   */
+  public CloseableIterator<FileReadResult> readParquetFiles(
+      CloseableIterator<FileStatus> fileIter,
+      StructType physicalSchema,
+      Optional<Predicate> predicate,
+      long startRgIndex)
+      throws IOException {
+
+    List<CloseableIterator<FileReadResult>> results = new ArrayList<>();
+    boolean hasRowIndexCol = 
physicalSchema.contains(MetadataColumnSpec.ROW_INDEX);
+
+    long currentRgIndex = startRgIndex;
+
+    try {
+      while (fileIter.hasNext()) {
+        if (currentRgIndex >= tracker.currentRestriction().getTo()) {
+          // Skipping all blocks for the remaining files since they are 
located after the
+          // end index of the tracker. Since currentRgIndex is monotonically 
increasing,
+          // we can break the loop immediately to avoid extremely expensive 
network I/O.
+          break;
+        }
+
+        FileStatus fileStatus = fileIter.next();
+        Path hadoopPath = new Path(fileStatus.getPath());
+        ParquetMetadata metadata =
+            ParquetFileReader.readFooter(conf, hadoopPath, 
ParquetMetadataConverter.NO_FILTER);
+        long fileBlocks = metadata.getBlocks().size();
+
+        if (currentRgIndex + fileBlocks <= 
tracker.currentRestriction().getFrom()) {
+          // Skipping all blocks for the current file since they are located 
before the
+          // start index of the tracker.
+          currentRgIndex += fileBlocks;
+          continue;
+        }
+
+        results.add(
+            readParquetFileDirect(
+                fileStatus,
+                hadoopPath,
+                metadata,
+                physicalSchema,
+                hasRowIndexCol,
+                currentRgIndex,
+                fileBlocks));
+
+        currentRgIndex += fileBlocks;
+      }
+    } finally {
+      fileIter.close();
+    }
+
+    return combineResults(results);
+  }
+
+  // Reads the correct set of blocks that belong to the given Parquet file that
+  // are within range for the current `RestrictionTracker`. If the current file
+  // has some blocks that are within the tracker's range and some that are
+  // outside,
+  // this will only read the blocks that are within the range.
+  private CloseableIterator<FileReadResult> readParquetFileDirect(
+      FileStatus fileStatus,
+      Path hadoopPath,
+      ParquetMetadata metadata,
+      StructType physicalSchema,
+      boolean hasRowIndexCol,
+      long startRgIndex,
+      long fileBlocks) {
+
+    return new CloseableIterator<FileReadResult>() {
+      @javax.annotation.Nullable private ParquetFileReader reader = null;
+      @javax.annotation.Nullable private BatchReadSupport readSupport = null;
+      @javax.annotation.Nullable private RecordMaterializer<Object> 
recordConverter = null;
+      @javax.annotation.Nullable private MessageColumnIO columnIO = null;
+
+      private long currentRgOffset = 0;
+      @javax.annotation.Nullable private RecordReader<Object> 
currentRecordReader = null;
+      private long currentRgTotalRows = 0;
+      private long currentRgRowOffset = 0;
+      private long currentRgStartingRowIndex = 0;
+
+      @javax.annotation.Nullable private FileReadResult nextResult = null;
+      private boolean isDone = false;
+
+      private void initReaderIfRequired() throws IOException {
+        if (reader != null) {
+          return;
+        }
+        HadoopInputFile inputFile = HadoopInputFile.fromPath(hadoopPath, conf);
+        ParquetFileReader localReader = ParquetFileReader.open(inputFile);
+        reader = localReader;
+
+        FileMetaData fileMetaData = metadata.getFileMetaData();
+        MessageType fileSchema = fileMetaData.getSchema();
+        Map<String, Set<String>> keyValueMetadata = new HashMap<>();
+        if (fileMetaData.getKeyValueMetaData() != null) {
+          for (Map.Entry<String, String> entry : 
fileMetaData.getKeyValueMetaData().entrySet()) {
+            keyValueMetadata.put(entry.getKey(), 
Collections.singleton(entry.getValue()));
+          }
+        }
+
+        BatchReadSupport localReadSupport = new BatchReadSupport(1024, 
physicalSchema);
+        readSupport = localReadSupport;
+        ReadSupport.ReadContext readContext =
+            localReadSupport.init(new InitContext(conf, keyValueMetadata, 
fileSchema));
+        RecordMaterializer<Object> localRecordConverter =
+            localReadSupport.prepareForRead(
+                conf, fileMetaData.getKeyValueMetaData(), fileSchema, 
readContext);
+        recordConverter = localRecordConverter;
+        localReader.setRequestedSchema(readContext.getRequestedSchema());
+
+        ColumnIOFactory columnIOFactory = new 
ColumnIOFactory(fileMetaData.getCreatedBy());
+        columnIO = 
columnIOFactory.getColumnIO(readContext.getRequestedSchema(), fileSchema, true);
+      }
+
+      @Override
+      public boolean hasNext() {
+        if (isDone) {
+          return false;
+        }
+        if (nextResult != null) {
+          return true;
+        }
+
+        try {
+          initReaderIfRequired();
+          ParquetFileReader localReader = reader;
+          BatchReadSupport localReadSupport = readSupport;
+          MessageColumnIO localColumnIO = columnIO;
+          RecordMaterializer<Object> localRecordConverter = recordConverter;
+          if (localReader == null
+              || localReadSupport == null
+              || localColumnIO == null
+              || localRecordConverter == null) {
+            throw new IllegalStateException("Reader not initialized");
+          }
+
+          while (true) {
+            RecordReader<Object> localRecordReader = currentRecordReader;
+            if (localRecordReader != null && currentRgRowOffset < 
currentRgTotalRows) {
+              int batchSize = (int) Math.min(1024L, currentRgTotalRows - 
currentRgRowOffset);
+              for (int i = 0; i < batchSize; i++) {
+                localRecordReader.read();
+                long rowIndex =
+                    hasRowIndexCol ? (currentRgStartingRowIndex + 
currentRgRowOffset + i) : -1L;
+                localReadSupport.finalizeCurrentRow(rowIndex);
+              }
+              currentRgRowOffset += batchSize;
+              io.delta.kernel.data.ColumnarBatch batch =
+                  localReadSupport.getDataAsColumnarBatch(batchSize);
+              nextResult = new FileReadResult(batch, fileStatus.getPath());
+              return true;
+            }
+
+            currentRecordReader = null;
+            if (currentRgOffset >= fileBlocks) {
+              isDone = true;
+              return false;
+            }
+
+            // Checking the range for specific row groups.
+            long rgIndex = startRgIndex + currentRgOffset;
+            if (rgIndex < tracker.currentRestriction().getFrom()) {
+              // Skip till we get to the first block to read.
+              localReader.skipNextRowGroup();
+              currentRgOffset++;
+              continue;
+            } else if (rgIndex >= tracker.currentRestriction().getTo()) {
+              // Once we are past the end index of the tracker we don't have 
to read any more
+              // blocks.
+              isDone = true;
+              return false;
+            }
+
+            // We only read the row group if it's within the range for the
+            // RestrictionTracker.
+            if (tracker.tryClaim(rgIndex)) {
+              PageReadStore pages = localReader.readNextRowGroup();
+              currentRecordReader =
+                  localColumnIO.getRecordReader(pages, localRecordConverter, 
FilterCompat.NOOP);
+              currentRgTotalRows = pages.getRowCount();
+              currentRgRowOffset = 0;
+              currentRgStartingRowIndex = pages.getRowIndexOffset().orElse(0L);
+              currentRgOffset++;
+            } else {
+              // Mark claim failed for the current row group, so we stop 
processing
+              // the remaining row groups in the source.
+              claimFailed = true;
+              isDone = true;
+              return false;
+            }
+          }
+        } catch (IOException e) {
+          throw new RuntimeException(e);
+        }
+      }
+
+      @Override
+      public FileReadResult next() {
+        if (!hasNext()) {
+          throw new NoSuchElementException();
+        }
+        FileReadResult res = nextResult;
+        if (res == null) {
+          throw new NoSuchElementException();
+        }
+        nextResult = null;
+        return res;
+      }
+
+      @Override
+      public void close() throws IOException {
+        if (reader != null) {
+          reader.close();
+        }
+      }
+    };
+  }
+
+  @Override
+  public void writeParquetFileAtomically(
+      String filePath, CloseableIterator<FilteredColumnarBatch> data) throws 
IOException {
+    delegate.writeParquetFileAtomically(filePath, data);
+  }
+
+  @Override
+  public CloseableIterator<DataFileStatus> writeParquetFiles(
+      String filePath, CloseableIterator<FilteredColumnarBatch> data, 
List<Column> statsColumns)
+      throws IOException {
+    return delegate.writeParquetFiles(filePath, data, statsColumns);
+  }
+
+  private static CloseableIterator<FileReadResult> combineResults(
+      List<CloseableIterator<FileReadResult>> iterators) {
+    return new CloseableIterator<FileReadResult>() {
+      private int currentIdx = 0;
+
+      @Override
+      public boolean hasNext() {
+        while (currentIdx < iterators.size()) {
+          if (iterators.get(currentIdx).hasNext()) {
+            return true;
+          }
+          currentIdx++;
+        }
+        return false;
+      }
+
+      @Override
+      public FileReadResult next() {
+        if (!hasNext()) {
+          throw new NoSuchElementException();
+        }
+        return iterators.get(currentIdx).next();
+      }
+
+      @Override
+      public void close() throws IOException {
+        for (CloseableIterator<FileReadResult> it : iterators) {
+          it.close();
+        }
+      }
+    };
+  }
+}
diff --git 
a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/CreateReadTasksDoFn.java
 
b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/CreateReadTasksDoFn.java
new file mode 100644
index 00000000000..36c9a1a47f8
--- /dev/null
+++ 
b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/CreateReadTasksDoFn.java
@@ -0,0 +1,140 @@
+/*
+ * 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.io.delta;
+
+import io.delta.kernel.Scan;
+import io.delta.kernel.Snapshot;
+import io.delta.kernel.Table;
+import io.delta.kernel.data.FilteredColumnarBatch;
+import io.delta.kernel.data.Row;
+import io.delta.kernel.defaults.engine.DefaultEngine;
+import io.delta.kernel.engine.Engine;
+import io.delta.kernel.internal.InternalScanFileUtils;
+import io.delta.kernel.utils.CloseableIterator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.beam.sdk.transforms.DoFn;
+import org.apache.hadoop.conf.Configuration;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** A DoFn that reads the Delta log and outputs a list of DeltaReadTask 
records to read. */
+class CreateReadTasksDoFn extends DoFn<String, DeltaReadTask> {
+  private static final long MAX_TASK_SIZE_BYTES = 1024L * 1024L * 1024L; // 1 
GB
+  private final @Nullable Map<String, String> hadoopConfig;
+
+  public CreateReadTasksDoFn(@Nullable Map<String, String> hadoopConfig) {
+    this.hadoopConfig = hadoopConfig;
+  }
+
+  @ProcessElement
+  public void processElement(@Element String tablePath, 
OutputReceiver<DeltaReadTask> out)
+      throws Exception {
+    Configuration conf = new Configuration();
+    if (hadoopConfig != null) {
+      for (Map.Entry<String, String> entry : hadoopConfig.entrySet()) {
+        conf.set(entry.getKey(), entry.getValue());
+      }
+    }
+    Engine engine = DefaultEngine.create(conf);
+    Table table = Table.forPath(engine, tablePath);
+    Snapshot snapshot = table.getLatestSnapshot(engine);
+    Scan scan = snapshot.getScanBuilder().build();
+    Row scanState = scan.getScanState(engine);
+    SerializableRow serializableScanState = new SerializableRow(scanState);
+
+    List<SerializableRow> currentGroup = new ArrayList<>();
+    List<List<Long>> currentGroupRowGroupSizes = new ArrayList<>();
+    long currentGroupSize = 0L;
+
+    try (CloseableIterator<FilteredColumnarBatch> scanFiles = 
scan.getScanFiles(engine)) {
+      while (scanFiles.hasNext()) {
+        FilteredColumnarBatch batch = scanFiles.next();
+        try (CloseableIterator<Row> rows = batch.getRows()) {
+          while (rows.hasNext()) {
+            Row scanFileRow = rows.next();
+            SerializableRow fileRow = new SerializableRow(scanFileRow);
+            long fileSize = 
InternalScanFileUtils.getAddFileStatus(fileRow).getSize();
+            List<Long> fileRowGroupSizes = getRowGroupSizes(fileRow, conf);
+
+            if (fileSize >= MAX_TASK_SIZE_BYTES) {
+              if (!currentGroup.isEmpty()) {
+                DeltaReadTask readTask =
+                    new DeltaReadTask(
+                        currentGroup, serializableScanState, 
currentGroupRowGroupSizes);
+                out.output(readTask);
+                currentGroup = new ArrayList<>();
+                currentGroupRowGroupSizes = new ArrayList<>();
+                currentGroupSize = 0L;
+              }
+
+              DeltaReadTask readTask =
+                  new DeltaReadTask(
+                      Collections.singletonList(fileRow),
+                      serializableScanState,
+                      Collections.singletonList(fileRowGroupSizes));
+              out.output(readTask);
+            } else {
+              if (currentGroupSize + fileSize > MAX_TASK_SIZE_BYTES) {
+                DeltaReadTask readTask =
+                    new DeltaReadTask(
+                        currentGroup, serializableScanState, 
currentGroupRowGroupSizes);
+                out.output(readTask);
+                currentGroup = new ArrayList<>();
+                currentGroupRowGroupSizes = new ArrayList<>();
+                currentGroup.add(fileRow);
+                currentGroupRowGroupSizes.add(fileRowGroupSizes);
+                currentGroupSize = fileSize;
+              } else {
+                currentGroup.add(fileRow);
+                currentGroupRowGroupSizes.add(fileRowGroupSizes);
+                currentGroupSize += fileSize;
+              }
+            }
+          }
+        }
+      }
+    }
+
+    if (!currentGroup.isEmpty()) {
+      DeltaReadTask readTask =
+          new DeltaReadTask(currentGroup, serializableScanState, 
currentGroupRowGroupSizes);
+      out.output(readTask);
+    }
+  }
+
+  private List<Long> getRowGroupSizes(SerializableRow scanFileRow, 
Configuration conf) {
+    List<Long> sizes = new ArrayList<>();
+    String pathStr = 
InternalScanFileUtils.getAddFileStatus(scanFileRow).getPath();
+    try {
+      org.apache.hadoop.fs.Path hadoopPath = new 
org.apache.hadoop.fs.Path(pathStr);
+      org.apache.parquet.hadoop.metadata.ParquetMetadata metadata =
+          org.apache.parquet.hadoop.ParquetFileReader.readFooter(
+              conf,
+              hadoopPath,
+              
org.apache.parquet.format.converter.ParquetMetadataConverter.NO_FILTER);
+      for (org.apache.parquet.hadoop.metadata.BlockMetaData block : 
metadata.getBlocks()) {
+        sizes.add(block.getTotalByteSize());
+      }
+    } catch (java.io.IOException e) {
+      throw new RuntimeException("Failed to read Parquet footer for " + 
pathStr, e);
+    }
+    return sizes;
+  }
+}
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 6c5df4728b4..c511a7380dc 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
@@ -18,12 +18,33 @@
 package org.apache.beam.sdk.io.delta;
 
 import com.google.auto.value.AutoValue;
+import io.delta.kernel.Table;
+import io.delta.kernel.defaults.engine.DefaultEngine;
+import io.delta.kernel.engine.Engine;
+import io.delta.kernel.types.ArrayType;
+import io.delta.kernel.types.BinaryType;
+import io.delta.kernel.types.BooleanType;
+import io.delta.kernel.types.DataType;
+import io.delta.kernel.types.DateType;
+import io.delta.kernel.types.DoubleType;
+import io.delta.kernel.types.FloatType;
+import io.delta.kernel.types.IntegerType;
+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.TimestampType;
 import java.util.Map;
 import org.apache.beam.sdk.annotations.Internal;
+import org.apache.beam.sdk.schemas.Schema;
+import org.apache.beam.sdk.transforms.Create;
 import org.apache.beam.sdk.transforms.PTransform;
+import org.apache.beam.sdk.transforms.ParDo;
 import org.apache.beam.sdk.values.PBegin;
 import org.apache.beam.sdk.values.PCollection;
 import org.apache.beam.sdk.values.Row;
+import org.apache.hadoop.conf.Configuration;
 import org.checkerframework.checker.nullness.qual.Nullable;
 
 /**
@@ -83,9 +104,74 @@ public class DeltaIO {
 
     @Override
     public PCollection<Row> expand(PBegin input) {
-      // TODO(https://github.com/apache/beam/issues/38551): Implement 
expansion for
-      // Delta Lake ReadRows
-      throw new UnsupportedOperationException("Not implemented yet.");
+      String path = getTablePath();
+      if (path == null) {
+        throw new IllegalArgumentException("Table path must be set.");
+      }
+
+      Configuration conf = new Configuration();
+      Map<String, String> hadoopConfig = getHadoopConfig();
+      if (hadoopConfig != null) {
+        for (Map.Entry<String, String> entry : hadoopConfig.entrySet()) {
+          conf.set(entry.getKey(), entry.getValue());
+        }
+      }
+      Engine engine = DefaultEngine.create(conf);
+      Table table = Table.forPath(engine, path);
+      io.delta.kernel.Snapshot snapshot = table.getLatestSnapshot(engine);
+      StructType deltaSchema = snapshot.getSchema();
+      if (deltaSchema == null) {
+        throw new IllegalStateException("Table schema is null.");
+      }
+      Schema beamSchema = convertToBeamSchema(deltaSchema);
+
+      return input
+          .apply("Create Path", Create.of(path))
+          .apply("Plan Files", ParDo.of(new CreateReadTasksDoFn(hadoopConfig)))
+          .apply("Read Logical Data", ParDo.of(new 
DeltaSourceDoFn(hadoopConfig)))
+          .setRowSchema(beamSchema);
+    }
+
+    static Schema convertToBeamSchema(StructType deltaSchema) {
+      Schema.Builder builder = Schema.builder();
+      for (StructField field : deltaSchema.fields()) {
+        builder.addField(field.getName(), 
convertToBeamFieldType(field.getDataType()));
+      }
+      return builder.build();
+    }
+
+    static Schema.FieldType convertToBeamFieldType(DataType deltaType) {
+      if (deltaType instanceof StringType) {
+        return Schema.FieldType.STRING;
+      } else if (deltaType instanceof IntegerType) {
+        return Schema.FieldType.INT32;
+      } else if (deltaType instanceof LongType) {
+        return Schema.FieldType.INT64;
+      } else if (deltaType instanceof FloatType) {
+        return Schema.FieldType.FLOAT;
+      } else if (deltaType instanceof DoubleType) {
+        return Schema.FieldType.DOUBLE;
+      } else if (deltaType instanceof BooleanType) {
+        return Schema.FieldType.BOOLEAN;
+      } else if (deltaType instanceof BinaryType) {
+        return Schema.FieldType.BYTES;
+      } else if (deltaType instanceof TimestampType) {
+        return Schema.FieldType.DATETIME;
+      } else if (deltaType instanceof DateType) {
+        return Schema.FieldType.DATETIME;
+      } else if (deltaType instanceof ArrayType) {
+        DataType elementType = ((ArrayType) deltaType).getElementType();
+        return Schema.FieldType.iterable(convertToBeamFieldType(elementType));
+      } else if (deltaType instanceof MapType) {
+        DataType keyType = ((MapType) deltaType).getKeyType();
+        DataType valueType = ((MapType) deltaType).getValueType();
+        return Schema.FieldType.map(
+            convertToBeamFieldType(keyType), 
convertToBeamFieldType(valueType));
+      } else if (deltaType instanceof StructType) {
+        return Schema.FieldType.row(convertToBeamSchema((StructType) 
deltaType));
+      } else {
+        throw new UnsupportedOperationException("Unsupported Delta type: " + 
deltaType.getClass());
+      }
     }
   }
 }
diff --git 
a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadTask.java
 
b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadTask.java
new file mode 100644
index 00000000000..10dbce43a75
--- /dev/null
+++ 
b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadTask.java
@@ -0,0 +1,88 @@
+/*
+ * 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.io.delta;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Objects;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A serializable task containing the necessary metadata to read a group of 
files in a Delta table.
+ * Packs both the {@code scanFileRows} (representing the physical files and 
deletion vectors) and
+ * {@code scanStateRow} (containing snapshot-level read schemas, 
configuration, and options).
+ */
+public class DeltaReadTask implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  private final List<SerializableRow> scanFileRows;
+  private final SerializableRow scanStateRow;
+  private final List<List<Long>> rowGroupSizesPerFile;
+
+  public DeltaReadTask(
+      List<SerializableRow> scanFileRows,
+      SerializableRow scanStateRow,
+      List<List<Long>> rowGroupSizesPerFile) {
+    this.scanFileRows = scanFileRows;
+    this.scanStateRow = scanStateRow;
+    this.rowGroupSizesPerFile = rowGroupSizesPerFile;
+  }
+
+  public List<SerializableRow> getScanFileRows() {
+    return scanFileRows;
+  }
+
+  public SerializableRow getScanStateRow() {
+    return scanStateRow;
+  }
+
+  public List<List<Long>> getRowGroupSizesPerFile() {
+    return rowGroupSizesPerFile;
+  }
+
+  @Override
+  public boolean equals(@Nullable Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof DeltaReadTask)) {
+      return false;
+    }
+    DeltaReadTask that = (DeltaReadTask) o;
+    return Objects.equals(scanFileRows, that.scanFileRows)
+        && Objects.equals(scanStateRow, that.scanStateRow)
+        && Objects.equals(rowGroupSizesPerFile, that.rowGroupSizesPerFile);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(scanFileRows, scanStateRow, rowGroupSizesPerFile);
+  }
+
+  @Override
+  public String toString() {
+    return "DeltaReadTask{"
+        + "scanFileRows="
+        + scanFileRows
+        + ", scanStateRow="
+        + scanStateRow
+        + ", rowGroupSizesPerFile="
+        + rowGroupSizesPerFile
+        + '}';
+  }
+}
diff --git 
a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadTaskTracker.java
 
b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadTaskTracker.java
new file mode 100644
index 00000000000..c81a2a23171
--- /dev/null
+++ 
b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadTaskTracker.java
@@ -0,0 +1,56 @@
+/*
+ * 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.io.delta;
+
+import java.util.List;
+import org.apache.beam.sdk.io.range.OffsetRange;
+import org.apache.beam.sdk.transforms.splittabledofn.OffsetRangeTracker;
+
+/**
+ * A {@link org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker} 
for tracking progress
+ * across Parquet row groups represented by a {@link DeltaReadTask}.
+ */
+public class DeltaReadTaskTracker extends OffsetRangeTracker {
+  private final List<Long> rowGroupSizes;
+
+  public DeltaReadTaskTracker(OffsetRange restriction, List<Long> 
rowGroupSizes) {
+    super(restriction);
+    this.rowGroupSizes = rowGroupSizes;
+  }
+
+  @Override
+  public Progress getProgress() {
+    long workCompleted = 0L;
+    long workRemaining = 0L;
+    long from = range.getFrom();
+    long to = range.getTo();
+    long attempted = lastAttemptedOffset == null ? (from - 1) : 
lastAttemptedOffset;
+
+    for (int i = (int) from; i < (int) to; i++) {
+      // Upper bound of the range is the number of row groups.
+      if (i < rowGroupSizes.size()) {
+        if (i <= attempted) {
+          workCompleted += rowGroupSizes.get(i);
+        } else {
+          workRemaining += rowGroupSizes.get(i);
+        }
+      }
+    }
+    return Progress.from((double) workCompleted, (double) workRemaining);
+  }
+}
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
new file mode 100644
index 00000000000..bd53c3c9d04
--- /dev/null
+++ 
b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaSourceDoFn.java
@@ -0,0 +1,455 @@
+/*
+ * 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.io.delta;
+
+import io.delta.kernel.Scan;
+import io.delta.kernel.data.ArrayValue;
+import io.delta.kernel.data.ColumnVector;
+import io.delta.kernel.data.ColumnarBatch;
+import io.delta.kernel.data.FilteredColumnarBatch;
+import io.delta.kernel.data.MapValue;
+import io.delta.kernel.defaults.engine.DefaultEngine;
+import io.delta.kernel.engine.Engine;
+import io.delta.kernel.engine.FileReadResult;
+import io.delta.kernel.internal.InternalScanFileUtils;
+import io.delta.kernel.internal.data.ScanStateRow;
+import io.delta.kernel.internal.util.Utils;
+import io.delta.kernel.types.ArrayType;
+import io.delta.kernel.types.BinaryType;
+import io.delta.kernel.types.BooleanType;
+import io.delta.kernel.types.ByteType;
+import io.delta.kernel.types.DataType;
+import io.delta.kernel.types.DateType;
+import io.delta.kernel.types.DoubleType;
+import io.delta.kernel.types.FloatType;
+import io.delta.kernel.types.IntegerType;
+import io.delta.kernel.types.LongType;
+import io.delta.kernel.types.MapType;
+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.TimestampType;
+import io.delta.kernel.utils.CloseableIterator;
+import io.delta.kernel.utils.FileStatus;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.beam.sdk.io.range.OffsetRange;
+import org.apache.beam.sdk.schemas.Schema;
+import org.apache.beam.sdk.transforms.DoFn;
+import org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker;
+import org.apache.beam.sdk.values.Row;
+import org.apache.hadoop.conf.Configuration;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A Splittable DoFn that processes {@link DeltaReadTask} elements, performs 
logical reads, and
+ * supports dynamic work rebalancing.
+ */
[email protected]
+class DeltaSourceDoFn extends DoFn<DeltaReadTask, Row> {
+  @Nullable Map<String, String> hadoopConfig;
+  private transient @Nullable Engine engine;
+  private transient @Nullable Configuration conf;
+
+  public DeltaSourceDoFn(@Nullable Map<String, String> hadoopConfig) {
+    this.hadoopConfig = hadoopConfig;
+  }
+
+  private synchronized Configuration getConfiguration() {
+    Configuration localConf = conf;
+    if (localConf == null) {
+      localConf = new Configuration();
+      if (hadoopConfig != null) {
+        for (Map.Entry<String, String> entry : hadoopConfig.entrySet()) {
+          localConf.set(entry.getKey(), entry.getValue());
+        }
+      }
+      conf = localConf;
+    }
+    return localConf;
+  }
+
+  // Returns the sizes of the row groups for a given DeltaReadTask.
+  private List<Long> getRowGroupSizes(DeltaReadTask task) {
+    List<Long> sizes = new ArrayList<>();
+    for (List<Long> fileSizes : task.getRowGroupSizesPerFile()) {
+      sizes.addAll(fileSizes);
+    }
+    return sizes;
+  }
+
+  @GetInitialRestriction
+  public OffsetRange getInitialRestriction(@Element DeltaReadTask task) {
+    List<Long> rowGroupSizes = getRowGroupSizes(task);
+    // Note that we use the number of row groups, `rowGroupSizes.size()`, here
+    // as the upper bound not the byte size of row groups.
+    return new OffsetRange(0L, rowGroupSizes.size());
+  }
+
+  @NewTracker
+  public DeltaReadTaskTracker newTracker(
+      @Restriction OffsetRange restriction, @Element DeltaReadTask task) {
+    return new DeltaReadTaskTracker(restriction, getRowGroupSizes(task));
+  }
+
+  @Setup
+  public void setUp() {
+    engine = DefaultEngine.create(getConfiguration());
+  }
+
+  @ProcessElement
+  public ProcessContinuation processElement(
+      @Element DeltaReadTask task,
+      RestrictionTracker<OffsetRange, Long> tracker,
+      OutputReceiver<Row> out)
+      throws Exception {
+
+    SerializableRow scanStateRow = task.getScanStateRow();
+    StructType physicalSchema = 
ScanStateRow.getPhysicalDataReadSchema(scanStateRow);
+    StructType logicalSchema = ScanStateRow.getLogicalSchema(scanStateRow);
+    Schema beamSchema = DeltaIO.ReadRows.convertToBeamSchema(logicalSchema);
+
+    Engine currentEngine = engine;
+    if (currentEngine == null) {
+      throw new IllegalArgumentException("Expected the engine to not be null");
+    }
+
+    // `BeamParquetHandler` takes a reference to the `RestrictionTracker` so 
that it
+    // can perform `getFrom`, `getTo`, `tryClaim` requests to return the 
correct set
+    // of row groups that map to the current restriction.
+    BeamParquetHandler parquetHandler =
+        new BeamParquetHandler(getConfiguration(), 
currentEngine.getParquetHandler(), tracker);
+    BeamEngine beamEngine = new BeamEngine(currentEngine, parquetHandler);
+
+    long currentStartRgIndex = 0L;
+
+    // We have to go through files in the `DeltaReadTask` in order so that the
+    // `RestrictionTracker`
+    // can correctly handle the range of the current split.
+    List<SerializableRow> scanFileRows = task.getScanFileRows();
+    List<List<Long>> rowGroupSizesPerFile = task.getRowGroupSizesPerFile();
+    for (int i = 0; i < scanFileRows.size(); i++) {
+      if (currentStartRgIndex >= tracker.currentRestriction().getTo()) {
+        // Breaking early to prevent metadata reads.
+        break;
+      }
+      SerializableRow scanFileRow = scanFileRows.get(i);
+      FileStatus fileStatus = 
InternalScanFileUtils.getAddFileStatus(scanFileRow);
+
+      long fileBlocks = rowGroupSizesPerFile.get(i).size();
+
+      try (CloseableIterator<FileReadResult> fileReadResults =
+          parquetHandler.readParquetFiles(
+              Utils.singletonCloseableIterator(fileStatus),
+              physicalSchema,
+              Optional.empty(),
+              currentStartRgIndex)) {
+
+        // Get the correct set of physical data for the current file that are 
within the
+        // range for the current `RestrictionTracker`.
+        CloseableIterator<ColumnarBatch> physicalData =
+            new CloseableIterator<ColumnarBatch>() {
+              @Override
+              public void close() throws java.io.IOException {}
+
+              @Override
+              public boolean hasNext() {
+                return fileReadResults.hasNext();
+              }
+
+              @Override
+              public ColumnarBatch next() {
+                return fileReadResults.next().getData();
+              }
+            };
+
+        // Convert physical data to logical data.
+        try (CloseableIterator<FilteredColumnarBatch> logicalBatches =
+            Scan.transformPhysicalData(beamEngine, scanStateRow, scanFileRow, 
physicalData)) {
+
+          while (logicalBatches.hasNext()) {
+            FilteredColumnarBatch batch = logicalBatches.next();
+            try (CloseableIterator<io.delta.kernel.data.Row> logicalRows = 
batch.getRows()) {
+              while (logicalRows.hasNext()) {
+                io.delta.kernel.data.Row deltaRow = logicalRows.next();
+                Row beamRow = toBeamRow(deltaRow, beamSchema);
+                out.output(beamRow);
+              }
+            }
+          }
+        }
+      }
+
+      // Advance the total number of blocked handled so far.
+      currentStartRgIndex += fileBlocks;
+
+      // If the tryClaim failed during processing of the current file, there's 
no need
+      // to look at the rest of the files within the task.
+      if (parquetHandler.hasClaimFailed()) {
+        break;
+      }
+    }
+    return ProcessContinuation.stop();
+  }
+
+  // Convert Delta `Row` to Beam `Row`.
+  private static Row toBeamRow(io.delta.kernel.data.Row deltaRow, Schema 
beamSchema) {
+    Row.Builder builder = Row.withSchema(beamSchema);
+    StructType deltaSchema = deltaRow.getSchema();
+    List<StructField> fields = deltaSchema.fields();
+    for (int i = 0; i < fields.size(); i++) {
+      StructField field = fields.get(i);
+      builder.addValue(getFieldValue(deltaRow, i, field.getDataType()));
+    }
+    return builder.build();
+  }
+
+  // Returns the value at a specific index in a given row.
+  private static @Nullable Object getFieldValue(
+      io.delta.kernel.data.Row row, int index, DataType type) {
+    if (row.isNullAt(index)) {
+      return null;
+    }
+    if (type instanceof BooleanType) {
+      return row.getBoolean(index);
+    } else if (type instanceof ByteType) {
+      return (int) row.getByte(index);
+    } else if (type instanceof ShortType) {
+      return (int) row.getShort(index);
+    } else if (type instanceof IntegerType) {
+      return row.getInt(index);
+    } else if (type instanceof LongType) {
+      return row.getLong(index);
+    } else if (type instanceof FloatType) {
+      return row.getFloat(index);
+    } else if (type instanceof DoubleType) {
+      return row.getDouble(index);
+    } else if (type instanceof StringType) {
+      return row.getString(index);
+    } else if (type instanceof BinaryType) {
+      return row.getBinary(index);
+    } else if (type instanceof TimestampType) {
+      long microSeconds = row.getLong(index);
+      return new org.joda.time.Instant(microSeconds / 1000L);
+    } else if (type instanceof DateType) {
+      int daysSinceEpoch = row.getInt(index);
+      return new org.joda.time.Instant(daysSinceEpoch * 86400000L);
+    } else if (type instanceof ArrayType) {
+      ArrayValue arrayValue = row.getArray(index);
+      int size = arrayValue.getSize();
+      ColumnVector elements = arrayValue.getElements();
+      DataType elementType = ((ArrayType) type).getElementType();
+      List<@Nullable Object> list = new ArrayList<>(size);
+      for (int i = 0; i < size; i++) {
+        list.add(getVectorValue(elements, i, elementType));
+      }
+      return list;
+    } else if (type instanceof MapType) {
+      MapValue mapValue = row.getMap(index);
+      int size = mapValue.getSize();
+      ColumnVector keys = mapValue.getKeys();
+      ColumnVector values = mapValue.getValues();
+      DataType keyType = ((MapType) type).getKeyType();
+      DataType valueType = ((MapType) type).getValueType();
+      Map<Object, @Nullable Object> map = new LinkedHashMap<>(size);
+      for (int i = 0; i < size; i++) {
+        Object key = getVectorValue(keys, i, keyType);
+        if (key != null) {
+          map.put(key, getVectorValue(values, i, valueType));
+        }
+      }
+      return map;
+    } else if (type instanceof StructType) {
+      io.delta.kernel.data.Row nestedRow = row.getStruct(index);
+      Schema nestedBeamSchema = 
DeltaIO.ReadRows.convertToBeamSchema((StructType) type);
+      return toBeamRow(nestedRow, nestedBeamSchema);
+    }
+    throw new UnsupportedOperationException("Unsupported type: " + 
type.getClass());
+  }
+
+  // Returns the value at a specific index in a given column vector.
+  private static @Nullable Object getVectorValue(ColumnVector vector, int 
index, DataType type) {
+    if (vector.isNullAt(index)) {
+      return null;
+    }
+    if (type instanceof BooleanType) {
+      return vector.getBoolean(index);
+    } else if (type instanceof ByteType) {
+      return (int) vector.getByte(index);
+    } else if (type instanceof ShortType) {
+      return (int) vector.getShort(index);
+    } else if (type instanceof IntegerType) {
+      return vector.getInt(index);
+    } else if (type instanceof LongType) {
+      return vector.getLong(index);
+    } else if (type instanceof FloatType) {
+      return vector.getFloat(index);
+    } else if (type instanceof DoubleType) {
+      return vector.getDouble(index);
+    } else if (type instanceof StringType) {
+      return vector.getString(index);
+    } else if (type instanceof BinaryType) {
+      return vector.getBinary(index);
+    } else if (type instanceof TimestampType) {
+      long microSeconds = vector.getLong(index);
+      return new org.joda.time.Instant(microSeconds / 1000L);
+    } 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);
+    } else if (type instanceof ArrayType) {
+      ArrayValue arrayValue = vector.getArray(index);
+      int size = arrayValue.getSize();
+      ColumnVector elements = arrayValue.getElements();
+      DataType elementType = ((ArrayType) type).getElementType();
+      List<@Nullable Object> list = new ArrayList<>(size);
+      for (int i = 0; i < size; i++) {
+        list.add(getVectorValue(elements, i, elementType));
+      }
+      return list;
+    } else if (type instanceof MapType) {
+      MapValue mapValue = vector.getMap(index);
+      int size = mapValue.getSize();
+      ColumnVector keys = mapValue.getKeys();
+      ColumnVector values = mapValue.getValues();
+      DataType keyType = ((MapType) type).getKeyType();
+      DataType valueType = ((MapType) type).getValueType();
+      Map<Object, @Nullable Object> map = new LinkedHashMap<>(size);
+      for (int i = 0; i < size; i++) {
+        Object key = getVectorValue(keys, i, keyType);
+        if (key != null) {
+          map.put(key, getVectorValue(values, i, valueType));
+        }
+      }
+      return map;
+    } else if (type instanceof StructType) {
+      StructType structType = (StructType) type;
+      int numFields = structType.fields().size();
+      ColumnVector[] childVectors = new ColumnVector[numFields];
+      for (int i = 0; i < numFields; i++) {
+        childVectors[i] = vector.getChild(i);
+      }
+      io.delta.kernel.data.Row nestedRow = new VectorRow(structType, 
childVectors, index);
+      Schema nestedBeamSchema = 
DeltaIO.ReadRows.convertToBeamSchema(structType);
+      return toBeamRow(nestedRow, nestedBeamSchema);
+    }
+    throw new UnsupportedOperationException("Unsupported vector type: " + 
type.getClass());
+  }
+
+  // A new new Delta Row type to efficiently convert columnar data from Delta 
Lake
+  // to Beam Rows. We use this to store columnar vectors without creating
+  // additinal memory copies for all fields but return values of the specific
+  // row index.
+  private static class VectorRow implements io.delta.kernel.data.Row {
+    private final StructType schema;
+    private final ColumnVector[] fields;
+    private final int rowIndex;
+
+    VectorRow(StructType schema, ColumnVector[] fields, int rowIndex) {
+      this.schema = schema;
+      this.fields = fields;
+      this.rowIndex = rowIndex;
+    }
+
+    @Override
+    public StructType getSchema() {
+      return schema;
+    }
+
+    @Override
+    public boolean isNullAt(int ord) {
+      return fields[ord].isNullAt(rowIndex);
+    }
+
+    @Override
+    public boolean getBoolean(int ord) {
+      return fields[ord].getBoolean(rowIndex);
+    }
+
+    @Override
+    public byte getByte(int ord) {
+      return fields[ord].getByte(rowIndex);
+    }
+
+    @Override
+    public short getShort(int ord) {
+      return fields[ord].getShort(rowIndex);
+    }
+
+    @Override
+    public int getInt(int ord) {
+      return fields[ord].getInt(rowIndex);
+    }
+
+    @Override
+    public long getLong(int ord) {
+      return fields[ord].getLong(rowIndex);
+    }
+
+    @Override
+    public float getFloat(int ord) {
+      return fields[ord].getFloat(rowIndex);
+    }
+
+    @Override
+    public double getDouble(int ord) {
+      return fields[ord].getDouble(rowIndex);
+    }
+
+    @Override
+    public String getString(int ord) {
+      return fields[ord].getString(rowIndex);
+    }
+
+    @Override
+    public byte[] getBinary(int ord) {
+      return fields[ord].getBinary(rowIndex);
+    }
+
+    @Override
+    public BigDecimal getDecimal(int ord) {
+      return fields[ord].getDecimal(rowIndex);
+    }
+
+    @Override
+    public io.delta.kernel.data.Row getStruct(int ord) {
+      StructType childSchema = (StructType) 
schema.fields().get(ord).getDataType();
+      int numFields = childSchema.fields().size();
+      ColumnVector[] childFields = new ColumnVector[numFields];
+      for (int j = 0; j < numFields; j++) {
+        childFields[j] = fields[ord].getChild(j);
+      }
+      return new VectorRow(childSchema, childFields, rowIndex);
+    }
+
+    @Override
+    public ArrayValue getArray(int ord) {
+      return fields[ord].getArray(rowIndex);
+    }
+
+    @Override
+    public MapValue getMap(int ord) {
+      return fields[ord].getMap(rowIndex);
+    }
+  }
+}
diff --git 
a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/SerializableRow.java
 
b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/SerializableRow.java
new file mode 100644
index 00000000000..308956383e7
--- /dev/null
+++ 
b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/SerializableRow.java
@@ -0,0 +1,544 @@
+/*
+ * 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.io.delta;
+
+import io.delta.kernel.data.ArrayValue;
+import io.delta.kernel.data.ColumnVector;
+import io.delta.kernel.data.MapValue;
+import io.delta.kernel.data.Row;
+import io.delta.kernel.types.ArrayType;
+import io.delta.kernel.types.BinaryType;
+import io.delta.kernel.types.BooleanType;
+import io.delta.kernel.types.ByteType;
+import io.delta.kernel.types.DataType;
+import io.delta.kernel.types.DateType;
+import io.delta.kernel.types.DecimalType;
+import io.delta.kernel.types.DoubleType;
+import io.delta.kernel.types.FloatType;
+import io.delta.kernel.types.IntegerType;
+import io.delta.kernel.types.LongType;
+import io.delta.kernel.types.MapType;
+import io.delta.kernel.types.ShortType;
+import io.delta.kernel.types.StringType;
+import io.delta.kernel.types.StructType;
+import io.delta.kernel.types.TimestampType;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A serializable wrapper for Delta {@link Row} that implements the {@link 
Row} interface itself,
+ * allowing worker nodes to access serialized Row objects using standard Delta 
Kernel APIs.
+ */
+public class SerializableRow implements Row, Serializable {
+  private static final long serialVersionUID = 1L;
+
+  private final SerializableStructType schema;
+  private final @Nullable Object[] values;
+
+  public SerializableRow(Row row) {
+    this.schema = new SerializableStructType(row.getSchema());
+    StructType structType = row.getSchema();
+    int numFields = structType.fields().size();
+    this.values = new Object[numFields];
+    for (int i = 0; i < numFields; i++) {
+      DataType type = structType.fields().get(i).getDataType();
+      this.values[i] = getValue(row, i, type);
+    }
+  }
+
+  @Override
+  public StructType getSchema() {
+    return schema.get();
+  }
+
+  @Override
+  public boolean isNullAt(int ord) {
+    return values == null || values[ord] == null;
+  }
+
+  @Override
+  public boolean getBoolean(int ord) {
+    Object val = Objects.requireNonNull(values)[ord];
+    return val != null ? (Boolean) val : false;
+  }
+
+  @Override
+  public byte getByte(int ord) {
+    Object val = Objects.requireNonNull(values)[ord];
+    return val != null ? (Byte) val : 0;
+  }
+
+  @Override
+  public short getShort(int ord) {
+    Object val = Objects.requireNonNull(values)[ord];
+    return val != null ? (Short) val : 0;
+  }
+
+  @Override
+  public int getInt(int ord) {
+    Object val = Objects.requireNonNull(values)[ord];
+    return val != null ? (Integer) val : 0;
+  }
+
+  @Override
+  public long getLong(int ord) {
+    Object val = Objects.requireNonNull(values)[ord];
+    return val != null ? (Long) val : 0L;
+  }
+
+  @Override
+  public float getFloat(int ord) {
+    Object val = Objects.requireNonNull(values)[ord];
+    return val != null ? (Float) val : 0.0f;
+  }
+
+  @Override
+  public double getDouble(int ord) {
+    Object val = Objects.requireNonNull(values)[ord];
+    return val != null ? (Double) val : 0.0d;
+  }
+
+  @Override
+  @SuppressWarnings("nullness")
+  public String getString(int ord) {
+    return (String) Objects.requireNonNull(values)[ord];
+  }
+
+  @Override
+  @SuppressWarnings("nullness")
+  public byte[] getBinary(int ord) {
+    return (byte[]) Objects.requireNonNull(values)[ord];
+  }
+
+  @Override
+  @SuppressWarnings("nullness")
+  public BigDecimal getDecimal(int ord) {
+    return (BigDecimal) Objects.requireNonNull(values)[ord];
+  }
+
+  @Override
+  @SuppressWarnings("nullness")
+  public Row getStruct(int ord) {
+    return (Row) Objects.requireNonNull(values)[ord];
+  }
+
+  @Override
+  @SuppressWarnings({"unchecked", "nullness"})
+  public ArrayValue getArray(int ord) {
+    Object val = Objects.requireNonNull(values)[ord];
+    if (val == null) {
+      return null;
+    }
+    DataType elementType =
+        ((ArrayType) 
getSchema().fields().get(ord).getDataType()).getElementType();
+    return new SerializableArrayValue((List<@Nullable Object>) val, 
elementType);
+  }
+
+  @Override
+  @SuppressWarnings({"unchecked", "nullness"})
+  public MapValue getMap(int ord) {
+    Object val = Objects.requireNonNull(values)[ord];
+    if (val == null) {
+      return null;
+    }
+    MapType mapType = (MapType) getSchema().fields().get(ord).getDataType();
+    return new SerializableMapValue(
+        (Map<Object, @Nullable Object>) val, mapType.getKeyType(), 
mapType.getValueType());
+  }
+
+  @Override
+  public boolean equals(@Nullable Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof SerializableRow)) {
+      return false;
+    }
+    SerializableRow that = (SerializableRow) o;
+    return Objects.equals(schema, that.schema) && 
java.util.Arrays.deepEquals(values, that.values);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(schema, java.util.Arrays.deepHashCode(values));
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append("SerializableRow{schema=").append(schema).append(", values=[");
+    if (values != null) {
+      for (int i = 0; i < values.length; i++) {
+        if (i > 0) {
+          sb.append(", ");
+        }
+        sb.append(values[i]);
+      }
+    }
+    sb.append("]}");
+    return sb.toString();
+  }
+
+  private static @Nullable Object getValue(Row row, int index, DataType type) {
+    if (row.isNullAt(index)) {
+      return null;
+    }
+    if (type instanceof BooleanType) {
+      return row.getBoolean(index);
+    } else if (type instanceof ByteType) {
+      return row.getByte(index);
+    } else if (type instanceof ShortType) {
+      return row.getShort(index);
+    } else if (type instanceof IntegerType) {
+      return row.getInt(index);
+    } else if (type instanceof LongType) {
+      return row.getLong(index);
+    } else if (type instanceof FloatType) {
+      return row.getFloat(index);
+    } else if (type instanceof DoubleType) {
+      return row.getDouble(index);
+    } else if (type instanceof StringType) {
+      return row.getString(index);
+    } else if (type instanceof BinaryType) {
+      return row.getBinary(index);
+    } else if (type instanceof DecimalType) {
+      return row.getDecimal(index);
+    } else if (type instanceof StructType) {
+      return new SerializableRow(row.getStruct(index));
+    } else if (type instanceof DateType) {
+      return row.getInt(index);
+    } else if (type instanceof TimestampType) {
+      return row.getLong(index);
+    } else if (type instanceof ArrayType) {
+      ArrayValue arr = row.getArray(index);
+      return convertArray(arr, (ArrayType) type);
+    } else if (type instanceof MapType) {
+      MapValue map = row.getMap(index);
+      return convertMap(map, (MapType) type);
+    }
+    throw new IllegalArgumentException("Unsupported type: " + type);
+  }
+
+  private static @Nullable Object getVectorValue(ColumnVector vector, int 
index, DataType type) {
+    if (vector.isNullAt(index)) {
+      return null;
+    }
+    if (type instanceof BooleanType) {
+      return vector.getBoolean(index);
+    } else if (type instanceof ByteType) {
+      return vector.getByte(index);
+    } else if (type instanceof ShortType) {
+      return vector.getShort(index);
+    } else if (type instanceof IntegerType) {
+      return vector.getInt(index);
+    } else if (type instanceof LongType) {
+      return vector.getLong(index);
+    } else if (type instanceof FloatType) {
+      return vector.getFloat(index);
+    } else if (type instanceof DoubleType) {
+      return vector.getDouble(index);
+    } else if (type instanceof StringType) {
+      return vector.getString(index);
+    } else if (type instanceof BinaryType) {
+      return vector.getBinary(index);
+    } else if (type instanceof DecimalType) {
+      return vector.getDecimal(index);
+    } else if (type instanceof StructType) {
+      StructType structType = (StructType) type;
+      int numFields = structType.fields().size();
+      ColumnVector[] childFields = new ColumnVector[numFields];
+      for (int j = 0; j < numFields; j++) {
+        childFields[j] = vector.getChild(j);
+      }
+      return new SerializableRow(new VectorRow(structType, childFields, 
index));
+    } else if (type instanceof DateType) {
+      return vector.getInt(index);
+    } else if (type instanceof TimestampType) {
+      return vector.getLong(index);
+    } else if (type instanceof ArrayType) {
+      ArrayValue arr = vector.getArray(index);
+      return convertArray(arr, (ArrayType) type);
+    } else if (type instanceof MapType) {
+      MapValue map = vector.getMap(index);
+      return convertMap(map, (MapType) type);
+    }
+    throw new IllegalArgumentException("Unsupported vector type: " + type);
+  }
+
+  private static List<@Nullable Object> convertArray(ArrayValue arr, ArrayType 
arrayType) {
+    int size = arr.getSize();
+    ColumnVector elements = arr.getElements();
+    DataType elementType = arrayType.getElementType();
+    List<@Nullable Object> result = new ArrayList<>(size);
+    for (int i = 0; i < size; i++) {
+      result.add(getVectorValue(elements, i, elementType));
+    }
+    return result;
+  }
+
+  private static Map<Object, @Nullable Object> convertMap(MapValue map, 
MapType mapType) {
+    int size = map.getSize();
+    ColumnVector keys = map.getKeys();
+    ColumnVector values = map.getValues();
+    DataType keyType = mapType.getKeyType();
+    DataType valueType = mapType.getValueType();
+    Map<Object, @Nullable Object> result = new LinkedHashMap<>(size);
+    for (int i = 0; i < size; i++) {
+      Object key = getVectorValue(keys, i, keyType);
+      if (key != null) {
+        result.put(key, getVectorValue(values, i, valueType));
+      }
+    }
+    return result;
+  }
+
+  private static class VectorRow implements Row {
+    private final StructType schema;
+    private final ColumnVector[] fields;
+    private final int rowIndex;
+
+    VectorRow(StructType schema, ColumnVector[] fields, int rowIndex) {
+      this.schema = schema;
+      this.fields = fields;
+      this.rowIndex = rowIndex;
+    }
+
+    @Override
+    public StructType getSchema() {
+      return schema;
+    }
+
+    @Override
+    public boolean isNullAt(int ord) {
+      return fields[ord].isNullAt(rowIndex);
+    }
+
+    @Override
+    public boolean getBoolean(int ord) {
+      return fields[ord].getBoolean(rowIndex);
+    }
+
+    @Override
+    public byte getByte(int ord) {
+      return fields[ord].getByte(rowIndex);
+    }
+
+    @Override
+    public short getShort(int ord) {
+      return fields[ord].getShort(rowIndex);
+    }
+
+    @Override
+    public int getInt(int ord) {
+      return fields[ord].getInt(rowIndex);
+    }
+
+    @Override
+    public long getLong(int ord) {
+      return fields[ord].getLong(rowIndex);
+    }
+
+    @Override
+    public float getFloat(int ord) {
+      return fields[ord].getFloat(rowIndex);
+    }
+
+    @Override
+    public double getDouble(int ord) {
+      return fields[ord].getDouble(rowIndex);
+    }
+
+    @Override
+    public String getString(int ord) {
+      return fields[ord].getString(rowIndex);
+    }
+
+    @Override
+    public byte[] getBinary(int ord) {
+      return fields[ord].getBinary(rowIndex);
+    }
+
+    @Override
+    public BigDecimal getDecimal(int ord) {
+      return fields[ord].getDecimal(rowIndex);
+    }
+
+    @Override
+    public Row getStruct(int ord) {
+      StructType childSchema = (StructType) 
schema.fields().get(ord).getDataType();
+      int numFields = childSchema.fields().size();
+      ColumnVector[] childFields = new ColumnVector[numFields];
+      for (int j = 0; j < numFields; j++) {
+        childFields[j] = fields[ord].getChild(j);
+      }
+      return new VectorRow(childSchema, childFields, rowIndex);
+    }
+
+    @Override
+    public ArrayValue getArray(int ord) {
+      return fields[ord].getArray(rowIndex);
+    }
+
+    @Override
+    public MapValue getMap(int ord) {
+      return fields[ord].getMap(rowIndex);
+    }
+  }
+
+  private static class ListColumnVector implements ColumnVector {
+    private final DataType dataType;
+    private final List<@Nullable Object> list;
+
+    @SuppressWarnings("unchecked")
+    ListColumnVector(DataType dataType, List<?> list) {
+      this.dataType = dataType;
+      this.list = (List<@Nullable Object>) list;
+    }
+
+    @Override
+    public DataType getDataType() {
+      return dataType;
+    }
+
+    @Override
+    public int getSize() {
+      return list.size();
+    }
+
+    @Override
+    public boolean isNullAt(int rowId) {
+      return list.get(rowId) == null;
+    }
+
+    @Override
+    public boolean getBoolean(int rowId) {
+      Object val = list.get(rowId);
+      return val != null ? (Boolean) val : false;
+    }
+
+    @Override
+    public byte getByte(int rowId) {
+      Object val = list.get(rowId);
+      return val != null ? (Byte) val : 0;
+    }
+
+    @Override
+    public short getShort(int rowId) {
+      Object val = list.get(rowId);
+      return val != null ? (Short) val : 0;
+    }
+
+    @Override
+    public int getInt(int rowId) {
+      Object val = list.get(rowId);
+      return val != null ? (Integer) val : 0;
+    }
+
+    @Override
+    public long getLong(int rowId) {
+      Object val = list.get(rowId);
+      return val != null ? (Long) val : 0L;
+    }
+
+    @Override
+    public float getFloat(int rowId) {
+      Object val = list.get(rowId);
+      return val != null ? (Float) val : 0.0f;
+    }
+
+    @Override
+    public double getDouble(int rowId) {
+      Object val = list.get(rowId);
+      return val != null ? (Double) val : 0.0d;
+    }
+
+    @Override
+    @SuppressWarnings("nullness")
+    public String getString(int rowId) {
+      return (String) list.get(rowId);
+    }
+
+    @Override
+    @SuppressWarnings("nullness")
+    public byte[] getBinary(int rowId) {
+      return (byte[]) list.get(rowId);
+    }
+
+    @Override
+    @SuppressWarnings("nullness")
+    public BigDecimal getDecimal(int rowId) {
+      return (BigDecimal) list.get(rowId);
+    }
+
+    @Override
+    public void close() {}
+  }
+
+  private static class SerializableArrayValue implements ArrayValue {
+    private final List<?> list;
+    private final DataType elementType;
+
+    SerializableArrayValue(List<?> list, DataType elementType) {
+      this.list = list;
+      this.elementType = elementType;
+    }
+
+    @Override
+    public int getSize() {
+      return list.size();
+    }
+
+    @Override
+    public ColumnVector getElements() {
+      return new ListColumnVector(elementType, list);
+    }
+  }
+
+  private static class SerializableMapValue implements MapValue {
+    private final Map<?, ?> map;
+    private final DataType keyType;
+    private final DataType valueType;
+
+    SerializableMapValue(Map<?, ?> map, DataType keyType, DataType valueType) {
+      this.map = map;
+      this.keyType = keyType;
+      this.valueType = valueType;
+    }
+
+    @Override
+    public int getSize() {
+      return map.size();
+    }
+
+    @Override
+    public ColumnVector getKeys() {
+      return new ListColumnVector(keyType, new ArrayList<>(map.keySet()));
+    }
+
+    @Override
+    public ColumnVector getValues() {
+      return new ListColumnVector(valueType, new ArrayList<>(map.values()));
+    }
+  }
+}
diff --git 
a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/SerializableStructType.java
 
b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/SerializableStructType.java
new file mode 100644
index 00000000000..08a27ac5530
--- /dev/null
+++ 
b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/SerializableStructType.java
@@ -0,0 +1,69 @@
+/*
+ * 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.io.delta;
+
+import io.delta.kernel.internal.types.DataTypeJsonSerDe;
+import io.delta.kernel.types.StructType;
+import java.io.Serializable;
+import java.util.Objects;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A serializable wrapper for {@link StructType} using the Delta Kernel JSON
+ * serializer/deserializer.
+ */
+public class SerializableStructType implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  private final String jsonSchema;
+  private transient StructType structType;
+
+  public SerializableStructType(StructType structType) {
+    this.structType = Objects.requireNonNull(structType, "structType cannot be 
null");
+    this.jsonSchema = DataTypeJsonSerDe.serializeStructType(structType);
+  }
+
+  public StructType get() {
+    if (structType == null) {
+      structType = DataTypeJsonSerDe.deserializeStructType(jsonSchema);
+    }
+    return structType;
+  }
+
+  @Override
+  public boolean equals(@Nullable Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof SerializableStructType)) {
+      return false;
+    }
+    SerializableStructType that = (SerializableStructType) o;
+    return Objects.equals(jsonSchema, that.jsonSchema);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(jsonSchema);
+  }
+
+  @Override
+  public String toString() {
+    return jsonSchema;
+  }
+}
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 4ab932fc5d8..ef6dd660c60 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
@@ -17,18 +17,57 @@
  */
 package org.apache.beam.sdk.io.delta;
 
+import io.delta.kernel.types.ArrayType;
+import io.delta.kernel.types.BinaryType;
+import io.delta.kernel.types.BooleanType;
+import io.delta.kernel.types.DateType;
+import io.delta.kernel.types.DoubleType;
+import io.delta.kernel.types.FloatType;
+import io.delta.kernel.types.IntegerType;
+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.TimestampType;
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.util.HashMap;
 import java.util.Map;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.beam.sdk.extensions.avro.coders.AvroCoder;
+import org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils;
+import org.apache.beam.sdk.io.Compression;
+import org.apache.beam.sdk.io.FileIO;
 import org.apache.beam.sdk.io.delta.DeltaIO.ReadRows;
+import org.apache.beam.sdk.io.parquet.ParquetIO;
+import org.apache.beam.sdk.schemas.Schema;
+import org.apache.beam.sdk.testing.PAssert;
+import org.apache.beam.sdk.testing.TestPipeline;
+import org.apache.beam.sdk.transforms.Count;
+import org.apache.beam.sdk.transforms.Create;
+import org.apache.beam.sdk.transforms.ParDo;
+import org.apache.beam.sdk.transforms.windowing.BoundedWindow;
+import org.apache.beam.sdk.transforms.windowing.PaneInfo;
+import org.apache.beam.sdk.values.PCollection;
+import org.apache.beam.sdk.values.Row;
 import org.junit.Assert;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-/** Unit tests for the {@link DeltaIO}. */
+/** Unit and local integration tests for {@link DeltaIO}. */
 @RunWith(JUnit4.class)
 public class DeltaIOTest {
 
+  @Rule public TestPipeline writePipeline = TestPipeline.create();
+  @Rule public TestPipeline readPipeline = TestPipeline.create();
+  @Rule public TestPipeline filteringPipeline = TestPipeline.create();
+  @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
+
   @Test
   public void testReadRowsBuilderAndGetters() {
     String tablePath = "/path/to/table";
@@ -59,4 +98,675 @@ public class DeltaIOTest {
     Assert.assertNull(readRows.getTimestamp());
     Assert.assertNull(readRows.getHadoopConfig());
   }
+
+  @Test
+  public void testPrintScanStateSchema() throws Exception {
+    File tableDir = tempFolder.newFolder("delta-table-schema");
+    File logDir = new File(tableDir, "_delta_log");
+    logDir.mkdirs();
+    File commitFile = new File(logDir, "00000000000000000000.json");
+
+    String commitContent =
+        "{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n"
+            + 
"{\"metaData\":{\"id\":\"test-id\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"name\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdAt\":123456789}}\n"
+            + 
"{\"add\":{\"path\":\"part-00000.parquet\",\"partitionValues\":{},\"size\":100,\"modificationTime\":123456789,\"dataChange\":true}}";
+
+    Files.write(commitFile.toPath(), 
commitContent.getBytes(StandardCharsets.UTF_8));
+
+    io.delta.kernel.defaults.engine.DefaultEngine engine =
+        io.delta.kernel.defaults.engine.DefaultEngine.create(
+            new org.apache.hadoop.conf.Configuration());
+    io.delta.kernel.Table table = io.delta.kernel.Table.forPath(engine, 
tableDir.getAbsolutePath());
+    io.delta.kernel.Snapshot snapshot = table.getLatestSnapshot(engine);
+    io.delta.kernel.Scan scan = snapshot.getScanBuilder().build();
+
+    io.delta.kernel.data.Row scanState = scan.getScanState(engine);
+    System.err.println("SCAN STATE SCHEMA: " + 
scanState.getSchema().toString());
+
+    try 
(io.delta.kernel.utils.CloseableIterator<io.delta.kernel.data.FilteredColumnarBatch>
+        scanFiles = scan.getScanFiles(engine)) {
+      while (scanFiles.hasNext()) {
+        io.delta.kernel.data.FilteredColumnarBatch batch = scanFiles.next();
+        try (io.delta.kernel.utils.CloseableIterator<io.delta.kernel.data.Row> 
rows =
+            batch.getRows()) {
+          while (rows.hasNext()) {
+            io.delta.kernel.data.Row row = rows.next();
+            verifySerialization(row);
+          }
+        }
+      }
+    }
+  }
+
+  private void verifySerialization(io.delta.kernel.data.Row row) throws 
Exception {
+    SerializableRow serializableRow = new SerializableRow(row);
+
+    // Serialize using standard Java Serialization
+    java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
+    try (java.io.ObjectOutputStream oos = new 
java.io.ObjectOutputStream(baos)) {
+      oos.writeObject(serializableRow);
+    }
+
+    byte[] bytes = baos.toByteArray();
+
+    // Deserialize
+    SerializableRow deserializedRow;
+    java.io.ByteArrayInputStream bais = new 
java.io.ByteArrayInputStream(bytes);
+    try (java.io.ObjectInputStream ois = new java.io.ObjectInputStream(bais)) {
+      deserializedRow = (SerializableRow) ois.readObject();
+    }
+
+    // Assert equals
+    org.junit.Assert.assertEquals(serializableRow, deserializedRow);
+    org.junit.Assert.assertEquals(
+        row.getSchema().toString(), deserializedRow.getSchema().toString());
+
+    // Deep verify fields
+    io.delta.kernel.types.StructType schema = row.getSchema();
+    for (int i = 0; i < schema.fields().size(); i++) {
+      org.junit.Assert.assertEquals(row.isNullAt(i), 
deserializedRow.isNullAt(i));
+      if (!row.isNullAt(i)) {
+        io.delta.kernel.types.DataType type = 
schema.fields().get(i).getDataType();
+        if (type instanceof io.delta.kernel.types.StringType) {
+          org.junit.Assert.assertEquals(row.getString(i), 
deserializedRow.getString(i));
+        } else if (type instanceof io.delta.kernel.types.LongType) {
+          org.junit.Assert.assertEquals(row.getLong(i), 
deserializedRow.getLong(i));
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testCreateReadTasksDoFn() throws Exception {
+    File tableDir = tempFolder.newFolder("delta-table");
+    File logDir = new File(tableDir, "_delta_log");
+    logDir.mkdirs();
+    File commitFile = new File(logDir, "00000000000000000000.json");
+
+    String commitContent =
+        "{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n"
+            + 
"{\"metaData\":{\"id\":\"test-id\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"name\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdAt\":123456789}}\n"
+            + 
"{\"add\":{\"path\":\"part-00000.parquet\",\"partitionValues\":{},\"size\":100,\"modificationTime\":123456789,\"dataChange\":true}}";
+
+    Files.write(commitFile.toPath(), 
commitContent.getBytes(StandardCharsets.UTF_8));
+
+    Schema schema = Schema.builder().addField("name", 
Schema.FieldType.STRING).build();
+    Row dummyRow = Row.withSchema(schema).addValues("test-name").build();
+    writeParquetFile(new File(tableDir, "part-00000.parquet"), dummyRow);
+
+    PCollection<DeltaReadTask> output =
+        writePipeline
+            .apply(Create.of(tableDir.getAbsolutePath()))
+            .apply(ParDo.of(new CreateReadTasksDoFn(null)));
+
+    PCollection<String> paths =
+        output.apply(
+            org.apache.beam.sdk.transforms.MapElements.into(
+                    org.apache.beam.sdk.values.TypeDescriptors.strings())
+                .via(
+                    task ->
+                        
io.delta.kernel.internal.InternalScanFileUtils.getAddFileStatus(
+                                task.getScanFileRows().get(0))
+                            .getPath()));
+
+    PAssert.that(paths)
+        .containsInAnyOrder("file:" + tableDir.getAbsolutePath() + 
"/part-00000.parquet");
+
+    writePipeline.run().waitUntilFinish();
+  }
+
+  @Test
+  public void testCreateReadTasksDoFnGrouping() throws Exception {
+    File tableDir = tempFolder.newFolder("delta-table-grouping");
+    File logDir = new File(tableDir, "_delta_log");
+    logDir.mkdirs();
+    File commitFile = new File(logDir, "00000000000000000000.json");
+
+    String commitContent =
+        "{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n"
+            + 
"{\"metaData\":{\"id\":\"test-id\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"name\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdAt\":123456789}}\n"
+            + 
"{\"add\":{\"path\":\"part-00001.parquet\",\"partitionValues\":{},\"size\":400000000,\"modificationTime\":123456789,\"dataChange\":true}}\n"
+            + 
"{\"add\":{\"path\":\"part-00002.parquet\",\"partitionValues\":{},\"size\":400000000,\"modificationTime\":123456789,\"dataChange\":true}}\n"
+            + 
"{\"add\":{\"path\":\"part-00003.parquet\",\"partitionValues\":{},\"size\":1200000000,\"modificationTime\":123456789,\"dataChange\":true}}\n"
+            + 
"{\"add\":{\"path\":\"part-00004.parquet\",\"partitionValues\":{},\"size\":100,\"modificationTime\":123456789,\"dataChange\":true}}";
+
+    Files.write(commitFile.toPath(), 
commitContent.getBytes(StandardCharsets.UTF_8));
+
+    Schema schema = Schema.builder().addField("name", 
Schema.FieldType.STRING).build();
+    Row dummyRow = Row.withSchema(schema).addValues("test-name").build();
+    writeParquetFile(new File(tableDir, "part-00001.parquet"), dummyRow);
+    writeParquetFile(new File(tableDir, "part-00002.parquet"), dummyRow);
+    writeParquetFile(new File(tableDir, "part-00003.parquet"), dummyRow);
+    writeParquetFile(new File(tableDir, "part-00004.parquet"), dummyRow);
+
+    PCollection<DeltaReadTask> output =
+        writePipeline
+            .apply("Create Grouping Input", 
Create.of(tableDir.getAbsolutePath()))
+            .apply("Plan Grouped Files", ParDo.of(new 
CreateReadTasksDoFn(null)));
+
+    PCollection<String> taskDescriptions =
+        output.apply(
+            org.apache.beam.sdk.transforms.MapElements.into(
+                    org.apache.beam.sdk.values.TypeDescriptors.strings())
+                .via(
+                    task -> {
+                      StringBuilder sb = new StringBuilder();
+                      for (SerializableRow row : task.getScanFileRows()) {
+                        if (sb.length() > 0) {
+                          sb.append(",");
+                        }
+                        String fullPath =
+                            
io.delta.kernel.internal.InternalScanFileUtils.getAddFileStatus(row)
+                                .getPath();
+                        String filename = 
fullPath.substring(fullPath.lastIndexOf('/') + 1);
+                        sb.append(filename);
+                      }
+                      return sb.toString();
+                    }));
+
+    PAssert.that(taskDescriptions)
+        .containsInAnyOrder(
+            "part-00001.parquet,part-00002.parquet", "part-00003.parquet", 
"part-00004.parquet");
+
+    writePipeline.run().waitUntilFinish();
+  }
+
+  @Test
+  public void testFullPipelineRead() throws Exception {
+    File tableDir = tempFolder.newFolder("delta-table-full");
+
+    // 1. Write a Parquet file using Beam
+    Schema schema = Schema.builder().addField("name", 
Schema.FieldType.STRING).build();
+    Row row = Row.withSchema(schema).addValues("test-name").build();
+
+    org.apache.avro.Schema avroSchema = AvroUtils.toAvroSchema(schema);
+    GenericRecord record = AvroUtils.toGenericRecord(row, avroSchema);
+
+    writePipeline
+        .apply("Create Input", 
Create.of(record).withCoder(AvroCoder.of(avroSchema)))
+        .apply(
+            "Write Parquet",
+            FileIO.<GenericRecord>write()
+                .via(ParquetIO.sink(avroSchema))
+                .to(tableDir.getAbsolutePath() + "/")
+                .withNaming(
+                    (BoundedWindow window,
+                        PaneInfo paneInfo,
+                        int numShards,
+                        int shardIndex,
+                        Compression compression) -> "part-00000.parquet"));
+
+    writePipeline.run().waitUntilFinish();
+
+    System.out.println("FILES IN TABLE DIR:");
+    for (File f : tableDir.listFiles()) {
+      System.out.println(
+          " - " + f.getName() + " (size=" + f.length() + ", isDir=" + 
f.isDirectory() + ")");
+      if (f.isDirectory()) {
+        for (File sub : f.listFiles()) {
+          System.out.println("   - " + sub.getName() + " (size=" + 
sub.length() + ")");
+        }
+      }
+    }
+
+    File parquetFile = new File(tableDir, "part-00000.parquet");
+    byte[] fileBytes = Files.readAllBytes(parquetFile.toPath());
+    System.out.println("PARQUET FILE LENGTH: " + fileBytes.length);
+    if (fileBytes.length >= 8) {
+      System.out.println(
+          "PARQUET FIRST 4 BYTES: "
+              + fileBytes[0]
+              + ", "
+              + fileBytes[1]
+              + ", "
+              + fileBytes[2]
+              + ", "
+              + fileBytes[3]
+              + " ('"
+              + (char) fileBytes[0]
+              + (char) fileBytes[1]
+              + (char) fileBytes[2]
+              + (char) fileBytes[3]
+              + "')");
+      int len = fileBytes.length;
+      System.out.println(
+          "PARQUET LAST 4 BYTES: "
+              + fileBytes[len - 4]
+              + ", "
+              + fileBytes[len - 3]
+              + ", "
+              + fileBytes[len - 2]
+              + ", "
+              + fileBytes[len - 1]
+              + " ('"
+              + (char) fileBytes[len - 4]
+              + (char) fileBytes[len - 3]
+              + (char) fileBytes[len - 2]
+              + (char) fileBytes[len - 1]
+              + "')");
+    }
+
+    // 2. Create the Delta log
+    File logDir = new File(tableDir, "_delta_log");
+    logDir.mkdirs();
+    File commitFile = new File(logDir, "00000000000000000000.json");
+
+    String commitContent =
+        "{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n"
+            + 
"{\"metaData\":{\"id\":\"test-id\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"name\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdAt\":123456789}}\n"
+            + 
"{\"add\":{\"path\":\"part-00000.parquet\",\"partitionValues\":{},\"size\":"
+            + fileBytes.length
+            + ",\"modificationTime\":123456789,\"dataChange\":true}}";
+
+    Files.write(commitFile.toPath(), 
commitContent.getBytes(StandardCharsets.UTF_8));
+
+    // 3. Read it using DeltaIO
+    PCollection<Row> output =
+        
readPipeline.apply(DeltaIO.readRows().from(tableDir.getAbsolutePath()));
+
+    PAssert.that(output).containsInAnyOrder(row);
+
+    readPipeline.run().waitUntilFinish();
+  }
+
+  private byte[] writeParquetFile(File file, Row row) throws Exception {
+    org.apache.avro.Schema avroSchema =
+        
org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.toAvroSchema(row.getSchema());
+    org.apache.avro.generic.GenericRecord record =
+        
org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.toGenericRecord(
+            row, avroSchema);
+    org.apache.hadoop.fs.Path path = new 
org.apache.hadoop.fs.Path(file.getAbsolutePath());
+    try 
(org.apache.parquet.hadoop.ParquetWriter<org.apache.avro.generic.GenericRecord> 
writer =
+        
org.apache.parquet.avro.AvroParquetWriter.<org.apache.avro.generic.GenericRecord>builder(
+                path)
+            .withSchema(avroSchema)
+            .withConf(new org.apache.hadoop.conf.Configuration())
+            .build()) {
+      writer.write(record);
+    }
+    return java.nio.file.Files.readAllBytes(file.toPath());
+  }
+
+  @Test
+  @org.junit.Ignore("Manual integration test with external local table")
+  public void testReadingLocalTable() throws Exception {
+    PCollection<Row> output =
+        readPipeline.apply(
+            DeltaIO.readRows()
+                
.from("/Users/chamikara/testing/delta_lake/test_repo/test_table_1_gb"));
+    PCollection<Long> counted = output.apply(Count.globally());
+
+    counted
+        .apply(
+            "Convert to String",
+            org.apache.beam.sdk.transforms.MapElements.into(
+                    org.apache.beam.sdk.values.TypeDescriptors.strings())
+                .via(String::valueOf))
+        .apply(
+            "Write to File",
+            org.apache.beam.sdk.io.TextIO.write()
+                
.to("/Users/chamikara/testing/delta_lake/test_repo_pipeline_output/output")
+                .withSuffix(".txt")
+                .withoutSharding());
+
+    readPipeline.run().waitUntilFinish();
+  }
+
+  @Test
+  public void testConvertToBeamSchema() {
+    StructType deltaSchema =
+        new StructType(
+            java.util.Arrays.asList(
+                new StructField("string", StringType.STRING, false),
+                new StructField("integer", IntegerType.INTEGER, false),
+                new StructField("long", LongType.LONG, false),
+                new StructField("float", FloatType.FLOAT, false),
+                new StructField("double", DoubleType.DOUBLE, false),
+                new StructField("boolean", BooleanType.BOOLEAN, false),
+                new StructField("binary", BinaryType.BINARY, false),
+                new StructField("timestamp", TimestampType.TIMESTAMP, false),
+                new StructField("date", DateType.DATE, false),
+                new StructField("array", new ArrayType(StringType.STRING, 
true), false),
+                new StructField(
+                    "map", new MapType(StringType.STRING, IntegerType.INTEGER, 
true), false),
+                new StructField(
+                    "struct",
+                    new StructType(
+                        java.util.Arrays.asList(
+                            new StructField("nested_string", 
StringType.STRING, false))),
+                    false)));
+
+    Schema nestedSchema =
+        Schema.builder().addField("nested_string", 
Schema.FieldType.STRING).build();
+
+    Schema expectedSchema =
+        Schema.builder()
+            .addField("string", Schema.FieldType.STRING)
+            .addField("integer", Schema.FieldType.INT32)
+            .addField("long", Schema.FieldType.INT64)
+            .addField("float", Schema.FieldType.FLOAT)
+            .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("array", 
Schema.FieldType.iterable(Schema.FieldType.STRING))
+            .addField("map", Schema.FieldType.map(Schema.FieldType.STRING, 
Schema.FieldType.INT32))
+            .addField("struct", Schema.FieldType.row(nestedSchema))
+            .build();
+
+    Schema actualSchema = DeltaIO.ReadRows.convertToBeamSchema(deltaSchema);
+    org.junit.Assert.assertEquals(expectedSchema, actualSchema);
+  }
+
+  @Test
+  public void testDeltaReadTaskTracker() {
+    java.util.List<Long> sizes = java.util.Arrays.asList(100L, 200L, 300L);
+    org.apache.beam.sdk.io.range.OffsetRange range =
+        new org.apache.beam.sdk.io.range.OffsetRange(0L, 3L);
+    DeltaReadTaskTracker tracker = new DeltaReadTaskTracker(range, sizes);
+
+    org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker.Progress 
progress =
+        tracker.getProgress();
+    org.junit.Assert.assertEquals(0.0, progress.getWorkCompleted(), 0.001);
+    org.junit.Assert.assertEquals(600.0, progress.getWorkRemaining(), 0.001);
+
+    org.junit.Assert.assertTrue(tracker.tryClaim(0L));
+    progress = tracker.getProgress();
+    org.junit.Assert.assertEquals(100.0, progress.getWorkCompleted(), 0.001);
+    org.junit.Assert.assertEquals(500.0, progress.getWorkRemaining(), 0.001);
+
+    org.junit.Assert.assertTrue(tracker.tryClaim(1L));
+    progress = tracker.getProgress();
+    org.junit.Assert.assertEquals(300.0, progress.getWorkCompleted(), 0.001);
+    org.junit.Assert.assertEquals(300.0, progress.getWorkRemaining(), 0.001);
+
+    org.junit.Assert.assertTrue(tracker.tryClaim(2L));
+    progress = tracker.getProgress();
+    org.junit.Assert.assertEquals(600.0, progress.getWorkCompleted(), 0.001);
+    org.junit.Assert.assertEquals(0.0, progress.getWorkRemaining(), 0.001);
+
+    tracker.checkDone();
+  }
+
+  @Test
+  public void testBeamParquetHandler() {
+    java.util.List<Long> sizes = java.util.Arrays.asList(100L, 200L);
+    org.apache.beam.sdk.io.range.OffsetRange range =
+        new org.apache.beam.sdk.io.range.OffsetRange(0L, 2L);
+    DeltaReadTaskTracker tracker = new DeltaReadTaskTracker(range, sizes);
+
+    org.apache.hadoop.conf.Configuration conf = new 
org.apache.hadoop.conf.Configuration();
+    io.delta.kernel.engine.ParquetHandler dummyDelegate =
+        new io.delta.kernel.engine.ParquetHandler() {
+          @Override
+          public 
io.delta.kernel.utils.CloseableIterator<io.delta.kernel.engine.FileReadResult>
+              readParquetFiles(
+                  
io.delta.kernel.utils.CloseableIterator<io.delta.kernel.utils.FileStatus>
+                      fileIter,
+                  io.delta.kernel.types.StructType physicalSchema,
+                  java.util.Optional<io.delta.kernel.expressions.Predicate> 
predicate)
+                  throws java.io.IOException {
+            return new io.delta.kernel.utils.CloseableIterator<
+                io.delta.kernel.engine.FileReadResult>() {
+              @Override
+              public boolean hasNext() {
+                return false;
+              }
+
+              @Override
+              public io.delta.kernel.engine.FileReadResult next() {
+                throw new java.util.NoSuchElementException();
+              }
+
+              @Override
+              public void close() {}
+            };
+          }
+
+          @Override
+          public void writeParquetFileAtomically(
+              String filePath,
+              
io.delta.kernel.utils.CloseableIterator<io.delta.kernel.data.FilteredColumnarBatch>
+                  data)
+              throws java.io.IOException {}
+
+          @Override
+          public 
io.delta.kernel.utils.CloseableIterator<io.delta.kernel.utils.DataFileStatus>
+              writeParquetFiles(
+                  String filePath,
+                  io.delta.kernel.utils.CloseableIterator<
+                          io.delta.kernel.data.FilteredColumnarBatch>
+                      data,
+                  java.util.List<io.delta.kernel.expressions.Column> 
statsColumns)
+                  throws java.io.IOException {
+            return new io.delta.kernel.utils.CloseableIterator<
+                io.delta.kernel.utils.DataFileStatus>() {
+              @Override
+              public boolean hasNext() {
+                return false;
+              }
+
+              @Override
+              public io.delta.kernel.utils.DataFileStatus next() {
+                throw new java.util.NoSuchElementException();
+              }
+
+              @Override
+              public void close() {}
+            };
+          }
+        };
+
+    BeamParquetHandler handler = new BeamParquetHandler(conf, dummyDelegate, 
tracker);
+    org.junit.Assert.assertNotNull(handler);
+
+    BeamEngine beamEngine =
+        new 
BeamEngine(io.delta.kernel.defaults.engine.DefaultEngine.create(conf), handler);
+    org.junit.Assert.assertEquals(handler, beamEngine.getParquetHandler());
+  }
+
+  @Test
+  public void testBeamParquetHandlerWriteDelegation() throws Exception {
+    java.util.List<Long> sizes = java.util.Arrays.asList(100L);
+    org.apache.beam.sdk.io.range.OffsetRange range =
+        new org.apache.beam.sdk.io.range.OffsetRange(0L, 1L);
+    DeltaReadTaskTracker tracker = new DeltaReadTaskTracker(range, sizes);
+    org.apache.hadoop.conf.Configuration conf = new 
org.apache.hadoop.conf.Configuration();
+
+    boolean[] flags = new boolean[2];
+    io.delta.kernel.engine.ParquetHandler delegate =
+        new io.delta.kernel.engine.ParquetHandler() {
+          @Override
+          public 
io.delta.kernel.utils.CloseableIterator<io.delta.kernel.engine.FileReadResult>
+              readParquetFiles(
+                  
io.delta.kernel.utils.CloseableIterator<io.delta.kernel.utils.FileStatus>
+                      fileIter,
+                  io.delta.kernel.types.StructType physicalSchema,
+                  java.util.Optional<io.delta.kernel.expressions.Predicate> 
predicate) {
+            return null;
+          }
+
+          @Override
+          public void writeParquetFileAtomically(
+              String filePath,
+              
io.delta.kernel.utils.CloseableIterator<io.delta.kernel.data.FilteredColumnarBatch>
+                  data) {
+            flags[0] = true;
+          }
+
+          @Override
+          public 
io.delta.kernel.utils.CloseableIterator<io.delta.kernel.utils.DataFileStatus>
+              writeParquetFiles(
+                  String filePath,
+                  io.delta.kernel.utils.CloseableIterator<
+                          io.delta.kernel.data.FilteredColumnarBatch>
+                      data,
+                  java.util.List<io.delta.kernel.expressions.Column> 
statsColumns) {
+            flags[1] = true;
+            return new io.delta.kernel.utils.CloseableIterator<
+                io.delta.kernel.utils.DataFileStatus>() {
+              @Override
+              public boolean hasNext() {
+                return false;
+              }
+
+              @Override
+              public io.delta.kernel.utils.DataFileStatus next() {
+                throw new java.util.NoSuchElementException();
+              }
+
+              @Override
+              public void close() {}
+            };
+          }
+        };
+
+    BeamParquetHandler handler = new BeamParquetHandler(conf, delegate, 
tracker);
+    handler.writeParquetFileAtomically("path", null);
+    org.junit.Assert.assertTrue(flags[0]);
+
+    handler.writeParquetFiles("path", null, java.util.Collections.emptyList());
+    org.junit.Assert.assertTrue(flags[1]);
+  }
+
+  @Test
+  public void testBeamParquetHandlerReadFiltering() throws Exception {
+    File tableDir = tempFolder.newFolder("parquet-filtering-test");
+
+    Schema schema = Schema.builder().addField("name", 
Schema.FieldType.STRING).build();
+    Row row = Row.withSchema(schema).addValues("test-name").build();
+    org.apache.avro.Schema avroSchema = AvroUtils.toAvroSchema(schema);
+    GenericRecord record = AvroUtils.toGenericRecord(row, avroSchema);
+
+    filteringPipeline
+        .apply("Create Input", 
Create.of(record).withCoder(AvroCoder.of(avroSchema)))
+        .apply(
+            "Write Parquet",
+            FileIO.<GenericRecord>write()
+                .via(ParquetIO.sink(avroSchema))
+                .to(tableDir.getAbsolutePath() + "/")
+                .withNaming((w, p, n, s, c) -> "part-00000.parquet"));
+
+    filteringPipeline.run().waitUntilFinish();
+
+    File parquetFile = new File(tableDir, "part-00000.parquet");
+    io.delta.kernel.utils.FileStatus fileStatus =
+        io.delta.kernel.utils.FileStatus.of(
+            parquetFile.getAbsolutePath(), parquetFile.length(), 123456789L);
+
+    org.apache.hadoop.conf.Configuration conf = new 
org.apache.hadoop.conf.Configuration();
+    io.delta.kernel.types.StructType physicalSchema =
+        new io.delta.kernel.types.StructType(
+            java.util.Arrays.asList(
+                new io.delta.kernel.types.StructField(
+                    "name", io.delta.kernel.types.StringType.STRING, true)));
+
+    io.delta.kernel.engine.ParquetHandler dummyDelegate =
+        new io.delta.kernel.engine.ParquetHandler() {
+          @Override
+          public 
io.delta.kernel.utils.CloseableIterator<io.delta.kernel.engine.FileReadResult>
+              readParquetFiles(
+                  
io.delta.kernel.utils.CloseableIterator<io.delta.kernel.utils.FileStatus>
+                      fileIter,
+                  io.delta.kernel.types.StructType physicalSchema,
+                  java.util.Optional<io.delta.kernel.expressions.Predicate> 
predicate) {
+            return null;
+          }
+
+          @Override
+          public void writeParquetFileAtomically(
+              String filePath,
+              
io.delta.kernel.utils.CloseableIterator<io.delta.kernel.data.FilteredColumnarBatch>
+                  data) {}
+
+          @Override
+          public 
io.delta.kernel.utils.CloseableIterator<io.delta.kernel.utils.DataFileStatus>
+              writeParquetFiles(
+                  String filePath,
+                  io.delta.kernel.utils.CloseableIterator<
+                          io.delta.kernel.data.FilteredColumnarBatch>
+                      data,
+                  java.util.List<io.delta.kernel.expressions.Column> 
statsColumns) {
+            return null;
+          }
+        };
+
+    // Case A: Out of bounds before (tracker range [10, 20))
+    DeltaReadTaskTracker trackerA =
+        new DeltaReadTaskTracker(
+            new org.apache.beam.sdk.io.range.OffsetRange(10L, 20L),
+            java.util.Collections.singletonList(parquetFile.length()));
+    BeamParquetHandler handlerA = new BeamParquetHandler(conf, dummyDelegate, 
trackerA);
+    try 
(io.delta.kernel.utils.CloseableIterator<io.delta.kernel.engine.FileReadResult> 
iter =
+        handlerA.readParquetFiles(
+            
io.delta.kernel.internal.util.Utils.singletonCloseableIterator(fileStatus),
+            physicalSchema,
+            java.util.Optional.empty())) {
+      org.junit.Assert.assertFalse(iter.hasNext());
+      try {
+        iter.next();
+        org.junit.Assert.fail("Expected NoSuchElementException");
+      } catch (java.util.NoSuchElementException e) {
+        // expected
+      }
+    }
+
+    // Case B: Out of bounds after (tracker range [0, 0))
+    DeltaReadTaskTracker trackerB =
+        new DeltaReadTaskTracker(
+            new org.apache.beam.sdk.io.range.OffsetRange(0L, 0L),
+            java.util.Collections.singletonList(parquetFile.length()));
+    BeamParquetHandler handlerB = new BeamParquetHandler(conf, dummyDelegate, 
trackerB);
+    try 
(io.delta.kernel.utils.CloseableIterator<io.delta.kernel.engine.FileReadResult> 
iter =
+        handlerB.readParquetFiles(
+            
io.delta.kernel.internal.util.Utils.singletonCloseableIterator(fileStatus),
+            physicalSchema,
+            java.util.Optional.empty())) {
+      org.junit.Assert.assertFalse(iter.hasNext());
+    }
+
+    // Case C: Claim fails
+    DeltaReadTaskTracker trackerC =
+        new DeltaReadTaskTracker(
+            new org.apache.beam.sdk.io.range.OffsetRange(0L, 1L),
+            java.util.Collections.singletonList(parquetFile.length())) {
+          @Override
+          public boolean tryClaim(Long i) {
+            return false; // Simulate failure to claim
+          }
+        };
+    BeamParquetHandler handlerC = new BeamParquetHandler(conf, dummyDelegate, 
trackerC);
+    try 
(io.delta.kernel.utils.CloseableIterator<io.delta.kernel.engine.FileReadResult> 
iter =
+        handlerC.readParquetFiles(
+            
io.delta.kernel.internal.util.Utils.singletonCloseableIterator(fileStatus),
+            physicalSchema,
+            java.util.Optional.empty())) {
+      org.junit.Assert.assertFalse(iter.hasNext());
+    }
+
+    // Case D: Successful claim and read
+    DeltaReadTaskTracker trackerD =
+        new DeltaReadTaskTracker(
+            new org.apache.beam.sdk.io.range.OffsetRange(0L, 1L),
+            java.util.Collections.singletonList(parquetFile.length()));
+    BeamParquetHandler handlerD = new BeamParquetHandler(conf, dummyDelegate, 
trackerD);
+    try 
(io.delta.kernel.utils.CloseableIterator<io.delta.kernel.engine.FileReadResult> 
iter =
+        handlerD.readParquetFiles(
+            
io.delta.kernel.internal.util.Utils.singletonCloseableIterator(fileStatus),
+            physicalSchema,
+            java.util.Optional.empty())) {
+      org.junit.Assert.assertTrue(iter.hasNext());
+      io.delta.kernel.engine.FileReadResult res = iter.next();
+      org.junit.Assert.assertNotNull(res);
+      org.junit.Assert.assertNotNull(res.getData());
+      org.junit.Assert.assertFalse(iter.hasNext());
+      try {
+        iter.next();
+        org.junit.Assert.fail("Expected NoSuchElementException");
+      } catch (java.util.NoSuchElementException e) {
+        // expected
+      }
+    }
+  }
 }
diff --git 
a/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/SerializableRowTest.java
 
b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/SerializableRowTest.java
new file mode 100644
index 00000000000..69d423032ea
--- /dev/null
+++ 
b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/SerializableRowTest.java
@@ -0,0 +1,498 @@
+/*
+ * 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.io.delta;
+
+import io.delta.kernel.data.ArrayValue;
+import io.delta.kernel.data.ColumnVector;
+import io.delta.kernel.data.MapValue;
+import io.delta.kernel.data.Row;
+import io.delta.kernel.types.ArrayType;
+import io.delta.kernel.types.BinaryType;
+import io.delta.kernel.types.BooleanType;
+import io.delta.kernel.types.ByteType;
+import io.delta.kernel.types.DataType;
+import io.delta.kernel.types.DateType;
+import io.delta.kernel.types.DecimalType;
+import io.delta.kernel.types.DoubleType;
+import io.delta.kernel.types.FloatType;
+import io.delta.kernel.types.IntegerType;
+import io.delta.kernel.types.LongType;
+import io.delta.kernel.types.MapType;
+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.TimestampType;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class SerializableRowTest {
+
+  @Test
+  public void testAllTypesReadWriteSerialization() throws Exception {
+    StructType structSchema =
+        new StructType(Arrays.asList(new StructField("nested_int", 
IntegerType.INTEGER, false)));
+
+    StructType schema =
+        new StructType(
+            Arrays.asList(
+                new StructField("boolean", BooleanType.BOOLEAN, true),
+                new StructField("byte", ByteType.BYTE, true),
+                new StructField("short", ShortType.SHORT, true),
+                new StructField("integer", IntegerType.INTEGER, true),
+                new StructField("long", LongType.LONG, true),
+                new StructField("float", FloatType.FLOAT, true),
+                new StructField("double", DoubleType.DOUBLE, true),
+                new StructField("string", StringType.STRING, true),
+                new StructField("binary", BinaryType.BINARY, true),
+                new StructField("decimal", new DecimalType(10, 2), true),
+                new StructField("date", DateType.DATE, true),
+                new StructField("timestamp", TimestampType.TIMESTAMP, true),
+                new StructField("struct", structSchema, true),
+                new StructField("array", new ArrayType(StringType.STRING, 
true), true),
+                new StructField(
+                    "map", new MapType(StringType.STRING, IntegerType.INTEGER, 
true), true)));
+
+    Map<String, Object> values = new LinkedHashMap<>();
+    values.put("boolean", true);
+    values.put("byte", (byte) 1);
+    values.put("short", (short) 2);
+    values.put("integer", 3);
+    values.put("long", 4L);
+    values.put("float", 5.0f);
+    values.put("double", 6.0d);
+    values.put("string", "hello");
+    values.put("binary", new byte[] {7, 8});
+    values.put("decimal", new BigDecimal("9.20"));
+    values.put("date", 16543);
+    values.put("timestamp", 16543000000L);
+
+    Map<String, Object> nestedValues = new LinkedHashMap<>();
+    nestedValues.put("nested_int", 42);
+    values.put("struct", new FakeRow(structSchema, nestedValues));
+
+    values.put("array", Arrays.asList("a", "b", null));
+
+    Map<String, Object> mapValues = new LinkedHashMap<>();
+    mapValues.put("key1", 100);
+    mapValues.put("key2", null);
+    values.put("map", mapValues);
+
+    FakeRow originalRow = new FakeRow(schema, values);
+    SerializableRow serializableRow = new SerializableRow(originalRow);
+
+    verifyRowContents(serializableRow, schema, values);
+
+    // Test equals and hashCode
+    SerializableRow serializableRow2 = new SerializableRow(new FakeRow(schema, 
values));
+    Assert.assertEquals(serializableRow, serializableRow2);
+    Assert.assertEquals(serializableRow.hashCode(), 
serializableRow2.hashCode());
+    Assert.assertNotNull(serializableRow.toString());
+
+    // Serialize and Deserialize
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
+      oos.writeObject(serializableRow);
+    }
+
+    byte[] bytes = baos.toByteArray();
+    SerializableRow deserializedRow;
+    try (ObjectInputStream ois = new ObjectInputStream(new 
ByteArrayInputStream(bytes))) {
+      deserializedRow = (SerializableRow) ois.readObject();
+    }
+
+    verifyRowContents(deserializedRow, schema, values);
+    Assert.assertEquals(serializableRow, deserializedRow);
+  }
+
+  @Test
+  public void testNullValuesReadWriteSerialization() throws Exception {
+    StructType structSchema =
+        new StructType(Arrays.asList(new StructField("nested_int", 
IntegerType.INTEGER, true)));
+
+    StructType schema =
+        new StructType(
+            Arrays.asList(
+                new StructField("boolean", BooleanType.BOOLEAN, true),
+                new StructField("byte", ByteType.BYTE, true),
+                new StructField("short", ShortType.SHORT, true),
+                new StructField("integer", IntegerType.INTEGER, true),
+                new StructField("long", LongType.LONG, true),
+                new StructField("float", FloatType.FLOAT, true),
+                new StructField("double", DoubleType.DOUBLE, true),
+                new StructField("string", StringType.STRING, true),
+                new StructField("binary", BinaryType.BINARY, true),
+                new StructField("decimal", new DecimalType(10, 2), true),
+                new StructField("date", DateType.DATE, true),
+                new StructField("timestamp", TimestampType.TIMESTAMP, true),
+                new StructField("struct", structSchema, true),
+                new StructField("array", new ArrayType(StringType.STRING, 
true), true),
+                new StructField(
+                    "map", new MapType(StringType.STRING, IntegerType.INTEGER, 
true), true)));
+
+    Map<String, Object> nullValues = new LinkedHashMap<>();
+    for (StructField field : schema.fields()) {
+      nullValues.put(field.getName(), null);
+    }
+
+    FakeRow originalRow = new FakeRow(schema, nullValues);
+    SerializableRow serializableRow = new SerializableRow(originalRow);
+
+    verifyRowContents(serializableRow, schema, nullValues);
+
+    // Serialize and Deserialize
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
+      oos.writeObject(serializableRow);
+    }
+
+    byte[] bytes = baos.toByteArray();
+    SerializableRow deserializedRow;
+    try (ObjectInputStream ois = new ObjectInputStream(new 
ByteArrayInputStream(bytes))) {
+      deserializedRow = (SerializableRow) ois.readObject();
+    }
+
+    verifyRowContents(deserializedRow, schema, nullValues);
+    Assert.assertEquals(serializableRow, deserializedRow);
+  }
+
+  private void verifyRowContents(Row row, StructType schema, Map<String, 
Object> expectedValues) {
+    Assert.assertEquals(schema.toString(), row.getSchema().toString());
+    int i = 0;
+    for (StructField field : schema.fields()) {
+      Object expected = expectedValues.get(field.getName());
+      Assert.assertEquals(expected == null, row.isNullAt(i));
+      if (expected != null) {
+        DataType type = field.getDataType();
+        if (type instanceof BooleanType) {
+          Assert.assertEquals(expected, row.getBoolean(i));
+        } else if (type instanceof ByteType) {
+          Assert.assertEquals(expected, row.getByte(i));
+        } else if (type instanceof ShortType) {
+          Assert.assertEquals(expected, row.getShort(i));
+        } else if (type instanceof IntegerType) {
+          Assert.assertEquals(expected, row.getInt(i));
+        } else if (type instanceof LongType) {
+          Assert.assertEquals(expected, row.getLong(i));
+        } else if (type instanceof FloatType) {
+          Assert.assertEquals(expected, row.getFloat(i));
+        } else if (type instanceof DoubleType) {
+          Assert.assertEquals(expected, row.getDouble(i));
+        } else if (type instanceof StringType) {
+          Assert.assertEquals(expected, row.getString(i));
+        } else if (type instanceof BinaryType) {
+          Assert.assertArrayEquals((byte[]) expected, row.getBinary(i));
+        } else if (type instanceof DecimalType) {
+          Assert.assertEquals(expected, row.getDecimal(i));
+        } else if (type instanceof DateType) {
+          Assert.assertEquals(expected, row.getInt(i));
+        } else if (type instanceof TimestampType) {
+          Assert.assertEquals(expected, row.getLong(i));
+        } else if (type instanceof StructType) {
+          Row actualStruct = row.getStruct(i);
+          Assert.assertNotNull(actualStruct);
+          Row expectedStruct = (Row) expected;
+          Assert.assertEquals(
+              expectedStruct.getSchema().toString(), 
actualStruct.getSchema().toString());
+          for (int j = 0; j < expectedStruct.getSchema().fields().size(); j++) 
{
+            Assert.assertEquals(expectedStruct.isNullAt(j), 
actualStruct.isNullAt(j));
+            if (!expectedStruct.isNullAt(j)) {
+              Assert.assertEquals(expectedStruct.getInt(j), 
actualStruct.getInt(j));
+            }
+          }
+        } else if (type instanceof ArrayType) {
+          ArrayValue actualArray = row.getArray(i);
+          Assert.assertNotNull(actualArray);
+          List<?> expectedList = (List<?>) expected;
+          Assert.assertEquals(expectedList.size(), actualArray.getSize());
+          ColumnVector vector = actualArray.getElements();
+          for (int j = 0; j < expectedList.size(); j++) {
+            Assert.assertEquals(expectedList.get(j) == null, 
vector.isNullAt(j));
+            if (expectedList.get(j) != null) {
+              Assert.assertEquals(expectedList.get(j), vector.getString(j));
+            }
+          }
+        } else if (type instanceof MapType) {
+          MapValue actualMap = row.getMap(i);
+          Assert.assertNotNull(actualMap);
+          Map<?, ?> expectedMap = (Map<?, ?>) expected;
+          Assert.assertEquals(expectedMap.size(), actualMap.getSize());
+          ColumnVector keys = actualMap.getKeys();
+          ColumnVector valuesVector = actualMap.getValues();
+          int j = 0;
+          for (Map.Entry<?, ?> entry : expectedMap.entrySet()) {
+            Assert.assertEquals(entry.getKey(), keys.getString(j));
+            Assert.assertEquals(entry.getValue() == null, 
valuesVector.isNullAt(j));
+            if (entry.getValue() != null) {
+              Assert.assertEquals(entry.getValue(), valuesVector.getInt(j));
+            }
+            j++;
+          }
+        }
+      }
+      i++;
+    }
+  }
+
+  private static class FakeRow implements Row {
+    private final StructType schema;
+    private final Map<String, Object> values;
+
+    FakeRow(StructType schema, Map<String, Object> values) {
+      this.schema = schema;
+      this.values = values;
+    }
+
+    @Override
+    public StructType getSchema() {
+      return schema;
+    }
+
+    @Override
+    public boolean isNullAt(int ord) {
+      String fieldName = schema.fields().get(ord).getName();
+      return values.get(fieldName) == null;
+    }
+
+    @Override
+    public boolean getBoolean(int ord) {
+      String fieldName = schema.fields().get(ord).getName();
+      return (Boolean) values.get(fieldName);
+    }
+
+    @Override
+    public byte getByte(int ord) {
+      String fieldName = schema.fields().get(ord).getName();
+      return (Byte) values.get(fieldName);
+    }
+
+    @Override
+    public short getShort(int ord) {
+      String fieldName = schema.fields().get(ord).getName();
+      return (Short) values.get(fieldName);
+    }
+
+    @Override
+    public int getInt(int ord) {
+      String fieldName = schema.fields().get(ord).getName();
+      return (Integer) values.get(fieldName);
+    }
+
+    @Override
+    public long getLong(int ord) {
+      String fieldName = schema.fields().get(ord).getName();
+      return (Long) values.get(fieldName);
+    }
+
+    @Override
+    public float getFloat(int ord) {
+      String fieldName = schema.fields().get(ord).getName();
+      return (Float) values.get(fieldName);
+    }
+
+    @Override
+    public double getDouble(int ord) {
+      String fieldName = schema.fields().get(ord).getName();
+      return (Double) values.get(fieldName);
+    }
+
+    @Override
+    public String getString(int ord) {
+      String fieldName = schema.fields().get(ord).getName();
+      return (String) values.get(fieldName);
+    }
+
+    @Override
+    public byte[] getBinary(int ord) {
+      String fieldName = schema.fields().get(ord).getName();
+      return (byte[]) values.get(fieldName);
+    }
+
+    @Override
+    public BigDecimal getDecimal(int ord) {
+      String fieldName = schema.fields().get(ord).getName();
+      return (BigDecimal) values.get(fieldName);
+    }
+
+    @Override
+    public Row getStruct(int ord) {
+      String fieldName = schema.fields().get(ord).getName();
+      return (Row) values.get(fieldName);
+    }
+
+    @Override
+    public ArrayValue getArray(int ord) {
+      String fieldName = schema.fields().get(ord).getName();
+      List<?> list = (List<?>) values.get(fieldName);
+      if (list == null) {
+        return null;
+      }
+      DataType elementType = ((ArrayType) 
schema.fields().get(ord).getDataType()).getElementType();
+      return new FakeArrayValue(list, elementType);
+    }
+
+    @Override
+    public MapValue getMap(int ord) {
+      String fieldName = schema.fields().get(ord).getName();
+      Map<?, ?> map = (Map<?, ?>) values.get(fieldName);
+      if (map == null) {
+        return null;
+      }
+      MapType mapType = (MapType) schema.fields().get(ord).getDataType();
+      return new FakeMapValue(map, mapType.getKeyType(), 
mapType.getValueType());
+    }
+  }
+
+  private static class FakeArrayValue implements ArrayValue {
+    private final List<?> list;
+    private final DataType elementType;
+
+    FakeArrayValue(List<?> list, DataType elementType) {
+      this.list = list;
+      this.elementType = elementType;
+    }
+
+    @Override
+    public int getSize() {
+      return list.size();
+    }
+
+    @Override
+    public ColumnVector getElements() {
+      return new FakeColumnVector(elementType, list);
+    }
+  }
+
+  private static class FakeMapValue implements MapValue {
+    private final Map<?, ?> map;
+    private final DataType keyType;
+    private final DataType valueType;
+
+    FakeMapValue(Map<?, ?> map, DataType keyType, DataType valueType) {
+      this.map = map;
+      this.keyType = keyType;
+      this.valueType = valueType;
+    }
+
+    @Override
+    public int getSize() {
+      return map.size();
+    }
+
+    @Override
+    public ColumnVector getKeys() {
+      return new FakeColumnVector(keyType, new ArrayList<>(map.keySet()));
+    }
+
+    @Override
+    public ColumnVector getValues() {
+      return new FakeColumnVector(valueType, new ArrayList<>(map.values()));
+    }
+  }
+
+  private static class FakeColumnVector implements ColumnVector {
+    private final DataType dataType;
+    private final List<Object> list;
+
+    FakeColumnVector(DataType dataType, List<?> list) {
+      this.dataType = dataType;
+      this.list = new ArrayList<>(list);
+    }
+
+    @Override
+    public DataType getDataType() {
+      return dataType;
+    }
+
+    @Override
+    public int getSize() {
+      return list.size();
+    }
+
+    @Override
+    public boolean isNullAt(int rowId) {
+      return list.get(rowId) == null;
+    }
+
+    @Override
+    public boolean getBoolean(int rowId) {
+      return (Boolean) list.get(rowId);
+    }
+
+    @Override
+    public byte getByte(int rowId) {
+      return (Byte) list.get(rowId);
+    }
+
+    @Override
+    public short getShort(int rowId) {
+      return (Short) list.get(rowId);
+    }
+
+    @Override
+    public int getInt(int rowId) {
+      return (Integer) list.get(rowId);
+    }
+
+    @Override
+    public long getLong(int rowId) {
+      return (Long) list.get(rowId);
+    }
+
+    @Override
+    public float getFloat(int rowId) {
+      return (Float) list.get(rowId);
+    }
+
+    @Override
+    public double getDouble(int rowId) {
+      return (Double) list.get(rowId);
+    }
+
+    @Override
+    public String getString(int rowId) {
+      return (String) list.get(rowId);
+    }
+
+    @Override
+    public byte[] getBinary(int rowId) {
+      return (byte[]) list.get(rowId);
+    }
+
+    @Override
+    public BigDecimal getDecimal(int rowId) {
+      return (BigDecimal) list.get(rowId);
+    }
+
+    @Override
+    public void close() {}
+  }
+}

Reply via email to