This is an automated email from the ASF dual-hosted git repository.

amoghj pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git


The following commit(s) were added to refs/heads/main by this push:
     new 73b179c3c1 Spark 3.5, Arrow: Support for Row lineage when using the 
Parquet Vectorized reader (#12928)
73b179c3c1 is described below

commit 73b179c3c130e54499d45a9203f63b58cc38e552
Author: Amogh Jahagirdar <[email protected]>
AuthorDate: Thu Jun 5 16:00:36 2025 -0700

    Spark 3.5, Arrow: Support for Row lineage when using the Parquet Vectorized 
reader (#12928)
---
 .../iceberg/arrow/vectorized/VectorHolder.java     |   7 +-
 .../arrow/vectorized/VectorizedArrowReader.java    | 211 ++++++++++++++++++++-
 .../arrow/vectorized/VectorizedReaderBuilder.java  |  41 ++--
 .../TestRowLevelOperationsWithLineage.java         |  79 +++++++-
 .../apache/iceberg/spark/data/GenericsHelpers.java |  12 +-
 .../org/apache/iceberg/spark/data/TestHelpers.java |  27 ---
 .../vectorized/TestParquetVectorizedReads.java     |  79 +++++++-
 7 files changed, 388 insertions(+), 68 deletions(-)

diff --git 
a/arrow/src/main/java/org/apache/iceberg/arrow/vectorized/VectorHolder.java 
b/arrow/src/main/java/org/apache/iceberg/arrow/vectorized/VectorHolder.java
index 8919c3b6f7..0245b6bba2 100644
--- a/arrow/src/main/java/org/apache/iceberg/arrow/vectorized/VectorHolder.java
+++ b/arrow/src/main/java/org/apache/iceberg/arrow/vectorized/VectorHolder.java
@@ -73,7 +73,7 @@ public class VectorHolder {
     icebergField = field;
   }
 
-  private VectorHolder(FieldVector vec, Types.NestedField field, 
NullabilityHolder nulls) {
+  VectorHolder(FieldVector vec, Types.NestedField field, NullabilityHolder 
nulls) {
     columnDescriptor = null;
     vector = vec;
     isDictionaryEncoded = false;
@@ -167,6 +167,11 @@ public class VectorHolder {
     }
   }
 
+  public static VectorHolder vectorHolder(
+      FieldVector vector, Types.NestedField icebergField, NullabilityHolder 
nulls) {
+    return new VectorHolder(vector, icebergField, nulls);
+  }
+
   public static class DeletedVectorHolder extends VectorHolder {
     private final int numRows;
 
diff --git 
a/arrow/src/main/java/org/apache/iceberg/arrow/vectorized/VectorizedArrowReader.java
 
b/arrow/src/main/java/org/apache/iceberg/arrow/vectorized/VectorizedArrowReader.java
index c71c8b1df5..b9509f1999 100644
--- 
a/arrow/src/main/java/org/apache/iceberg/arrow/vectorized/VectorizedArrowReader.java
+++ 
b/arrow/src/main/java/org/apache/iceberg/arrow/vectorized/VectorizedArrowReader.java
@@ -461,6 +461,53 @@ public class VectorizedArrowReader implements 
VectorizedReader<VectorHolder> {
     return new PositionVectorReader(true);
   }
 
+  public static VectorizedArrowReader rowIds(Long baseRowId, 
VectorizedArrowReader idReader) {
+    if (baseRowId != null) {
+      return new RowIdVectorReader(baseRowId, idReader);
+    } else {
+      return nulls();
+    }
+  }
+
+  public static VectorizedArrowReader lastUpdated(
+      Long baseRowId, Long fileLastUpdated, VectorizedArrowReader seqReader) {
+    if (fileLastUpdated != null && baseRowId != null) {
+      return new LastUpdatedSeqVectorReader(fileLastUpdated, seqReader);
+    } else {
+      return nulls();
+    }
+  }
+
+  public static VectorizedReader<?> replaceWithMetadataReader(
+      Types.NestedField icebergField,
+      VectorizedReader<?> reader,
+      Map<Integer, ?> idToConstant,
+      boolean setArrowValidityVector) {
+    int id = icebergField.fieldId();
+    if (id == MetadataColumns.ROW_ID.fieldId()) {
+      Long baseRowId = (Long) idToConstant.get(id);
+      return rowIds(baseRowId, (VectorizedArrowReader) reader);
+    } else if (id == MetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER.fieldId()) {
+      Long baseRowId = (Long) 
idToConstant.get(MetadataColumns.ROW_ID.fieldId());
+      Long fileSeqNumber = (Long) idToConstant.get(id);
+      return VectorizedArrowReader.lastUpdated(
+          baseRowId, fileSeqNumber, (VectorizedArrowReader) reader);
+    } else if (idToConstant.containsKey(id)) {
+      // containsKey is used because the constant may be null
+      return new ConstantVectorReader<>(icebergField, idToConstant.get(id));
+    } else if (id == MetadataColumns.ROW_POSITION.fieldId()) {
+      if (setArrowValidityVector) {
+        return positionsWithSetArrowValidityVector();
+      } else {
+        return VectorizedArrowReader.positions();
+      }
+    } else if (id == MetadataColumns.IS_DELETED.fieldId()) {
+      return new DeletedVectorReader();
+    }
+
+    return reader;
+  }
+
   private static final class NullVectorReader extends VectorizedArrowReader {
     private static final NullVectorReader INSTANCE = new NullVectorReader();
 
@@ -530,12 +577,6 @@ public class VectorizedArrowReader implements 
VectorizedReader<VectorHolder> {
       return vector;
     }
 
-    private static NullabilityHolder newNullabilityHolder(int size) {
-      NullabilityHolder nullabilityHolder = new NullabilityHolder(size);
-      nullabilityHolder.setNotNulls(0, size);
-      return nullabilityHolder;
-    }
-
     @Override
     public void setRowGroupInfo(
         PageReadStore source, Map<ColumnPath, ColumnChunkMetaData> metadata) {
@@ -567,6 +608,164 @@ public class VectorizedArrowReader implements 
VectorizedReader<VectorHolder> {
     }
   }
 
+  private static final class RowIdVectorReader extends VectorizedArrowReader {
+    private static final Field ROW_ID_ARROW_FIELD = 
ArrowSchemaUtil.convert(MetadataColumns.ROW_ID);
+
+    private final long firstRowId;
+    private final VectorizedReader<VectorHolder> idReader;
+    private final VectorizedReader<VectorHolder> posReader;
+    private NullabilityHolder nulls;
+
+    private RowIdVectorReader(long firstRowId, VectorizedArrowReader idReader) 
{
+      this.firstRowId = firstRowId;
+      this.idReader = idReader != null ? idReader : nulls();
+      this.posReader = new PositionVectorReader(true);
+    }
+
+    @Override
+    public VectorHolder read(VectorHolder reuse, int numValsToRead) {
+      FieldVector positions = null;
+      FieldVector ids = null;
+
+      try {
+        positions = posReader.read(null, numValsToRead).vector();
+        VectorHolder idsHolder = idReader.read(null, numValsToRead);
+        ids = idsHolder.vector();
+        ArrowVectorAccessor<?, String, ?, ?> idsAccessor =
+            ids == null ? null : 
ArrowVectorAccessors.getVectorAccessor(idsHolder);
+
+        BigIntVector rowIds = allocateBigIntVector(ROW_ID_ARROW_FIELD, 
numValsToRead);
+        ArrowBuf dataBuffer = rowIds.getDataBuffer();
+        for (int i = 0; i < numValsToRead; i += 1) {
+          long bufferOffset = (long) i * Long.BYTES;
+          if (idsAccessor == null || isNull(idsHolder, i)) {
+            long rowId = firstRowId + (Long) positions.getObject(i);
+            dataBuffer.setLong(bufferOffset, rowId);
+          } else {
+            long materializedRowId = idsAccessor.getLong(i);
+            dataBuffer.setLong(bufferOffset, materializedRowId);
+          }
+        }
+
+        rowIds.setValueCount(numValsToRead);
+        return VectorHolder.vectorHolder(rowIds, MetadataColumns.ROW_ID, 
nulls);
+      } finally {
+        if (positions != null) {
+          positions.close();
+        }
+
+        if (ids != null) {
+          ids.close();
+        }
+      }
+    }
+
+    @Override
+    public void setRowGroupInfo(
+        PageReadStore source, Map<ColumnPath, ColumnChunkMetaData> metadata) {
+      idReader.setRowGroupInfo(source, metadata);
+      posReader.setRowGroupInfo(source, metadata);
+    }
+
+    @Override
+    public void setBatchSize(int batchSize) {
+      if (nulls == null || nulls.size() < batchSize) {
+        this.nulls = newNullabilityHolder(batchSize);
+      }
+
+      idReader.setBatchSize(batchSize);
+      posReader.setBatchSize(batchSize);
+    }
+
+    @Override
+    public void close() {
+      // don't close result vectors as they are not owned by readers
+    }
+  }
+
+  private static final class LastUpdatedSeqVectorReader extends 
VectorizedArrowReader {
+    private static final Field LAST_UPDATED_SEQ =
+        ArrowSchemaUtil.convert(MetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER);
+
+    private final long lastUpdatedSeq;
+    private final VectorizedReader<VectorHolder> seqReader;
+    private NullabilityHolder nulls;
+
+    private LastUpdatedSeqVectorReader(
+        long lastUpdatedSeq, VectorizedReader<VectorHolder> seqReader) {
+      this.lastUpdatedSeq = lastUpdatedSeq;
+      this.seqReader = seqReader == null ? nulls() : seqReader;
+    }
+
+    @Override
+    public VectorHolder read(VectorHolder reuse, int numValsToRead) {
+      FieldVector seqNumbers = null;
+      try {
+        VectorHolder seqNumbersHolder = seqReader.read(null, numValsToRead);
+        seqNumbers = seqNumbersHolder.vector();
+        ArrowVectorAccessor<?, String, ?, ?> seqAccessor =
+            seqNumbers == null ? null : 
ArrowVectorAccessors.getVectorAccessor(seqNumbersHolder);
+
+        BigIntVector lastUpdatedSequenceNumbers =
+            allocateBigIntVector(LAST_UPDATED_SEQ, numValsToRead);
+        ArrowBuf dataBuffer = lastUpdatedSequenceNumbers.getDataBuffer();
+        for (int i = 0; i < numValsToRead; i += 1) {
+          long bufferOffset = (long) i * Long.BYTES;
+          if (seqAccessor == null || isNull(seqNumbersHolder, i)) {
+            dataBuffer.setLong(bufferOffset, lastUpdatedSeq);
+          } else {
+            long materializedSeqNumber = seqAccessor.getLong(i);
+            dataBuffer.setLong(bufferOffset, materializedSeqNumber);
+          }
+        }
+
+        lastUpdatedSequenceNumbers.setValueCount(numValsToRead);
+        return VectorHolder.vectorHolder(
+            lastUpdatedSequenceNumbers, 
MetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER, nulls);
+      } finally {
+        if (seqNumbers != null) {
+          seqNumbers.close();
+        }
+      }
+    }
+
+    @Override
+    public void setRowGroupInfo(
+        PageReadStore source, Map<ColumnPath, ColumnChunkMetaData> metadata) {
+      seqReader.setRowGroupInfo(source, metadata);
+    }
+
+    @Override
+    public void setBatchSize(int batchSize) {
+      if (nulls == null || nulls.size() < batchSize) {
+        this.nulls = newNullabilityHolder(batchSize);
+      }
+
+      seqReader.setBatchSize(batchSize);
+    }
+
+    @Override
+    public void close() {
+      // don't close result vectors as they are not owned by readers
+    }
+  }
+
+  private static boolean isNull(VectorHolder holder, int index) {
+    return holder.nullabilityHolder().isNullAt(index) == 1;
+  }
+
+  private static BigIntVector allocateBigIntVector(Field field, int 
valueCount) {
+    BigIntVector vector = (BigIntVector) 
field.createVector(ArrowAllocation.rootAllocator());
+    vector.allocateNew(valueCount);
+    return vector;
+  }
+
+  private static NullabilityHolder newNullabilityHolder(int size) {
+    NullabilityHolder nullabilityHolder = new NullabilityHolder(size);
+    nullabilityHolder.setNotNulls(0, size);
+    return nullabilityHolder;
+  }
+
   /**
    * A Dummy Vector Reader which doesn't actually read files, instead it 
returns a dummy
    * VectorHolder which indicates the constant value which should be used for 
this column.
diff --git 
a/arrow/src/main/java/org/apache/iceberg/arrow/vectorized/VectorizedReaderBuilder.java
 
b/arrow/src/main/java/org/apache/iceberg/arrow/vectorized/VectorizedReaderBuilder.java
index 398f42eb1c..391c356197 100644
--- 
a/arrow/src/main/java/org/apache/iceberg/arrow/vectorized/VectorizedReaderBuilder.java
+++ 
b/arrow/src/main/java/org/apache/iceberg/arrow/vectorized/VectorizedReaderBuilder.java
@@ -24,11 +24,9 @@ import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.stream.IntStream;
 import org.apache.arrow.memory.BufferAllocator;
-import org.apache.iceberg.MetadataColumns;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.arrow.ArrowAllocation;
 import 
org.apache.iceberg.arrow.vectorized.VectorizedArrowReader.ConstantVectorReader;
-import 
org.apache.iceberg.arrow.vectorized.VectorizedArrowReader.DeletedVectorReader;
 import org.apache.iceberg.parquet.TypeWithSchemaVisitor;
 import org.apache.iceberg.parquet.VectorizedReader;
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
@@ -101,33 +99,26 @@ public class VectorizedReaderBuilder extends 
TypeWithSchemaVisitor<VectorizedRea
         Lists.newArrayListWithExpectedSize(icebergFields.size());
 
     for (Types.NestedField field : icebergFields) {
-      int id = field.fieldId();
-      VectorizedReader<?> reader = readersById.get(id);
-      if (idToConstant.containsKey(id)) {
-        reorderedFields.add(constantReader(field, idToConstant.get(id)));
-      } else if (id == MetadataColumns.ROW_POSITION.fieldId()) {
-        if (setArrowValidityVector) {
-          
reorderedFields.add(VectorizedArrowReader.positionsWithSetArrowValidityVector());
-        } else {
-          reorderedFields.add(VectorizedArrowReader.positions());
-        }
-      } else if (id == MetadataColumns.IS_DELETED.fieldId()) {
-        reorderedFields.add(new DeletedVectorReader());
-      } else if (reader != null) {
-        reorderedFields.add(reader);
-      } else if (field.initialDefault() != null) {
-        reorderedFields.add(
-            constantReader(field, convert.apply(field.type(), 
field.initialDefault())));
-      } else if (field.isOptional()) {
-        reorderedFields.add(VectorizedArrowReader.nulls());
-      } else {
-        throw new IllegalArgumentException(
-            String.format("Missing required field: %s", field.name()));
-      }
+      VectorizedReader<?> reader =
+          VectorizedArrowReader.replaceWithMetadataReader(
+              field, readersById.get(field.fieldId()), idToConstant, 
setArrowValidityVector);
+      reorderedFields.add(defaultReader(field, reader));
     }
     return vectorizedReader(reorderedFields);
   }
 
+  private VectorizedReader<?> defaultReader(Types.NestedField field, 
VectorizedReader<?> reader) {
+    if (reader != null) {
+      return reader;
+    } else if (field.initialDefault() != null) {
+      return constantReader(field, convert.apply(field.type(), 
field.initialDefault()));
+    } else if (field.isOptional()) {
+      return VectorizedArrowReader.nulls();
+    }
+
+    throw new IllegalArgumentException(String.format("Missing required field: 
%s", field.name()));
+  }
+
   private <T> ConstantVectorReader<T> constantReader(Types.NestedField field, 
T constant) {
     return new ConstantVectorReader<>(field, constant);
   }
diff --git 
a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRowLevelOperationsWithLineage.java
 
b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRowLevelOperationsWithLineage.java
index 62c7690641..34f1f5d341 100644
--- 
a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRowLevelOperationsWithLineage.java
+++ 
b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRowLevelOperationsWithLineage.java
@@ -91,7 +91,6 @@ public abstract class TestRowLevelOperationsWithLineage 
extends SparkRowLevelOpe
     assumeThat(formatVersion).isGreaterThanOrEqualTo(3);
     // ToDo: Remove these as row lineage inheritance gets implemented in the 
other readers
     assumeThat(fileFormat).isEqualTo(FileFormat.PARQUET);
-    assumeThat(vectorized).isFalse();
   }
 
   @AfterEach
@@ -396,6 +395,84 @@ public abstract class TestRowLevelOperationsWithLineage 
extends SparkRowLevelOpe
         rowsWithLineage());
   }
 
+  @TestTemplate
+  public void testMergeWithManyRecords() throws NoSuchTableException, 
ParseException, IOException {
+    createAndInitTable("id INT, data STRING", null);
+    createBranchIfNeeded();
+    Table table = loadIcebergTable(spark, tableName);
+
+    int numRecords = 25000;
+    int startingId = 100;
+
+    List<Record> initialRecords = Lists.newArrayList();
+    int rowId = 0;
+    for (int id = 100; id < startingId + numRecords; id++) {
+      initialRecords.add(createRecord(SCHEMA, id, "data_" + id, rowId++, 1L));
+    }
+
+    appendUnpartitionedRecords(table, initialRecords);
+    createOrReplaceView(
+        "source",
+        "id int, data string",
+        "{ \"id\": 101, \"data\": \"updated_data_101\" }\n "
+            + "{ \"id\": 26000, \"data\": \"data_26000\" }\n");
+    sql(
+        "MERGE INTO %s AS t USING source AS s "
+            + "ON t.id == s.id "
+            + "WHEN MATCHED THEN "
+            + "  UPDATE SET t.data = s.data "
+            + "WHEN NOT MATCHED THEN "
+            + "  INSERT *",
+        commitTarget());
+
+    Snapshot updateSnapshot = latestSnapshot(table);
+    long updateSnapshotFirstRowId = updateSnapshot.firstRowId();
+    List<Object[]> allRows = rowsWithLineageAndFilePos();
+    List<Object[]> carriedOverAndUpdatedRows =
+        allRows.stream()
+            .filter(row -> (long) row[3] < updateSnapshotFirstRowId)
+            .collect(Collectors.toList());
+
+    int newlyInsertedId = 26000;
+    int updatedId = 101;
+    List<Object[]> expectedCarriedOverAndUpdatedRows =
+        ImmutableList.<Object[]>builder()
+            .add(row(1L, 100, "data_100", 0L, ANY, ANY))
+            .add(row(updateSnapshot.sequenceNumber(), updatedId, 
"updated_data_101", 1L, ANY, ANY))
+            .addAll(
+                // Every record with higher ids than the updated excluding the 
new row should be a
+                // carry over
+                initialRecords.stream()
+                    .filter(
+                        initialRecord -> {
+                          int id = initialRecord.get(0, Integer.class);
+                          return id > updatedId && id != newlyInsertedId;
+                        })
+                    .map(this::recordToExpectedRow)
+                    .collect(Collectors.toList()))
+            .build();
+
+    assertEquals(
+        "Rows which are carried over or updated should have expected lineage",
+        expectedCarriedOverAndUpdatedRows,
+        carriedOverAndUpdatedRows);
+
+    Object[] newRow =
+        Iterables.getOnlyElement(
+            allRows.stream()
+                .filter(row -> (long) row[3] >= updateSnapshotFirstRowId)
+                .collect(Collectors.toList()));
+    assertAddedRowLineage(row(updateSnapshot.sequenceNumber(), 26000, 
"data_26000"), newRow);
+  }
+
+  private Object[] recordToExpectedRow(Record record) {
+    int id = record.get(0, Integer.class);
+    String data = record.get(1, String.class);
+    long rowId = record.get(2, Long.class);
+    long lastUpdated = record.get(3, Long.class);
+    return row(lastUpdated, id, data, rowId, ANY, ANY);
+  }
+
   private List<Object[]> rowsWithLineageAndFilePos() {
     return sql(
         "SELECT s._last_updated_sequence_number, s.id, s.data, s._row_id, 
files.first_row_id, s._pos FROM %s"
diff --git 
a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/data/GenericsHelpers.java
 
b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/data/GenericsHelpers.java
index aed9f2c2af..e03b827cbe 100644
--- 
a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/data/GenericsHelpers.java
+++ 
b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/data/GenericsHelpers.java
@@ -73,11 +73,15 @@ public class GenericsHelpers {
   }
 
   public static void assertEqualsBatch(
-      Types.StructType struct, Iterator<Record> expectedRecords, ColumnarBatch 
batch) {
-    for (int rowId = 0; rowId < batch.numRows(); rowId++) {
-      InternalRow row = batch.getRow(rowId);
+      Types.StructType struct,
+      Iterator<Record> expectedRecords,
+      ColumnarBatch batch,
+      Map<Integer, Object> idToConstant,
+      Integer batchFirstRowPos) {
+    for (int rowPos = 0; rowPos < batch.numRows(); rowPos++) {
+      InternalRow row = batch.getRow(rowPos);
       Record expectedRecord = expectedRecords.next();
-      assertEqualsUnsafe(struct, expectedRecord, row);
+      assertEqualsUnsafe(struct, expectedRecord, row, idToConstant, 
batchFirstRowPos + rowPos);
     }
   }
 
diff --git 
a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/data/TestHelpers.java 
b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/data/TestHelpers.java
index 7baad225eb..82ebb1d950 100644
--- 
a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/data/TestHelpers.java
+++ 
b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/data/TestHelpers.java
@@ -113,33 +113,6 @@ public class TestHelpers {
     }
   }
 
-  public static void assertEqualsBatch(
-      Types.StructType struct, Iterator<Record> expected, ColumnarBatch batch) 
{
-    for (int rowId = 0; rowId < batch.numRows(); rowId++) {
-      InternalRow row = batch.getRow(rowId);
-      Record rec = expected.next();
-
-      List<Types.NestedField> fields = struct.fields();
-      for (int readPos = 0; readPos < fields.size(); readPos += 1) {
-        Types.NestedField field = fields.get(readPos);
-        Field writeField = rec.getSchema().getField(field.name());
-
-        Type fieldType = field.type();
-        Object actualValue = row.isNullAt(readPos) ? null : row.get(readPos, 
convert(fieldType));
-
-        Object expectedValue;
-        if (writeField != null) {
-          int writePos = writeField.pos();
-          expectedValue = rec.get(writePos);
-        } else {
-          expectedValue = field.initialDefault();
-        }
-
-        assertEqualsUnsafe(fieldType, expectedValue, actualValue);
-      }
-    }
-  }
-
   public static void assertEqualsBatchWithRows(
       Types.StructType struct, Iterator<Row> expected, ColumnarBatch batch) {
     for (int rowId = 0; rowId < batch.numRows(); rowId++) {
diff --git 
a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/data/parquet/vectorized/TestParquetVectorizedReads.java
 
b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/data/parquet/vectorized/TestParquetVectorizedReads.java
index 9d443de748..ad8f80caf8 100644
--- 
a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/data/parquet/vectorized/TestParquetVectorizedReads.java
+++ 
b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/data/parquet/vectorized/TestParquetVectorizedReads.java
@@ -27,6 +27,8 @@ import static org.assertj.core.api.Assumptions.assumeThat;
 import java.io.File;
 import java.io.IOException;
 import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
 import org.apache.iceberg.Files;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.data.RandomGenericData;
@@ -37,6 +39,7 @@ import org.apache.iceberg.io.FileAppender;
 import org.apache.iceberg.parquet.Parquet;
 import org.apache.iceberg.relocated.com.google.common.base.Function;
 import org.apache.iceberg.relocated.com.google.common.base.Strings;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
 import org.apache.iceberg.relocated.com.google.common.collect.Iterables;
 import org.apache.iceberg.relocated.com.google.common.collect.Lists;
 import org.apache.iceberg.relocated.com.google.common.collect.Maps;
@@ -77,6 +80,13 @@ public class TestParquetVectorizedReads extends AvroDataTest 
{
         IDENTITY);
   }
 
+  @Override
+  protected void writeAndValidate(Schema writeSchema, Schema expectedSchema, 
List<Record> records)
+      throws IOException {
+    writeAndValidate(
+        writeSchema, expectedSchema, true, BATCH_SIZE, records.size(), 
records, ID_TO_CONSTANT);
+  }
+
   @Override
   protected boolean supportsDefaultValues() {
     return true;
@@ -87,6 +97,11 @@ public class TestParquetVectorizedReads extends AvroDataTest 
{
     return false;
   }
 
+  @Override
+  protected boolean supportsRowLineage() {
+    return true;
+  }
+
   private void writeAndValidate(
       Schema schema, int numRecords, long seed, float nullPercentage, boolean 
reuseContainers)
       throws IOException {
@@ -104,6 +119,29 @@ public class TestParquetVectorizedReads extends 
AvroDataTest {
       int batchSize,
       Function<Record, Record> transform)
       throws IOException {
+    writeAndValidate(
+        writeSchema,
+        expectedSchema,
+        numRecords,
+        seed,
+        nullPercentage,
+        reuseContainers,
+        batchSize,
+        transform,
+        ImmutableMap.of());
+  }
+
+  private void writeAndValidate(
+      Schema writeSchema,
+      Schema expectedSchema,
+      int numRecords,
+      long seed,
+      float nullPercentage,
+      boolean reuseContainers,
+      int batchSize,
+      Function<Record, Record> transform,
+      Map<Integer, Object> idToConstant)
+      throws IOException {
     // Write test data
     assumeThat(
             TypeUtil.find(
@@ -115,6 +153,25 @@ public class TestParquetVectorizedReads extends 
AvroDataTest {
     Iterable<Record> expected =
         generateData(writeSchema, numRecords, seed, nullPercentage, transform);
 
+    writeAndValidate(
+        writeSchema,
+        expectedSchema,
+        reuseContainers,
+        batchSize,
+        numRecords,
+        expected,
+        idToConstant);
+  }
+
+  private void writeAndValidate(
+      Schema writeSchema,
+      Schema expectedSchema,
+      boolean reuseContainers,
+      int batchSize,
+      int numRecords,
+      Iterable<Record> expected,
+      Map<Integer, Object> idToConstant)
+      throws IOException {
     // write a test parquet file using iceberg writer
     File testFile = File.createTempFile("junit", null, temp.toFile());
     assertThat(testFile.delete()).as("Delete should succeed").isTrue();
@@ -123,7 +180,8 @@ public class TestParquetVectorizedReads extends 
AvroDataTest {
       writer.addAll(expected);
     }
 
-    assertRecordsMatch(expectedSchema, numRecords, expected, testFile, 
reuseContainers, batchSize);
+    assertRecordsMatch(
+        expectedSchema, numRecords, expected, testFile, reuseContainers, 
batchSize, idToConstant);
   }
 
   protected int getNumRows() {
@@ -165,14 +223,26 @@ public class TestParquetVectorizedReads extends 
AvroDataTest {
       boolean reuseContainers,
       int batchSize)
       throws IOException {
+    assertRecordsMatch(
+        schema, expectedSize, expected, testFile, reuseContainers, batchSize, 
ImmutableMap.of());
+  }
+
+  void assertRecordsMatch(
+      Schema schema,
+      int expectedSize,
+      Iterable<Record> expected,
+      File testFile,
+      boolean reuseContainers,
+      int batchSize,
+      Map<Integer, Object> idToConstant)
+      throws IOException {
     Parquet.ReadBuilder readBuilder =
         Parquet.read(Files.localInput(testFile))
             .project(schema)
             .recordsPerBatch(batchSize)
             .createBatchedReaderFunc(
                 type ->
-                    VectorizedSparkParquetReaders.buildReader(
-                        schema, type, Maps.newHashMap(), null));
+                    VectorizedSparkParquetReaders.buildReader(schema, type, 
idToConstant, null));
     if (reuseContainers) {
       readBuilder.reuseContainers();
     }
@@ -182,8 +252,9 @@ public class TestParquetVectorizedReads extends 
AvroDataTest {
       int numRowsRead = 0;
       while (batches.hasNext()) {
         ColumnarBatch batch = batches.next();
+        GenericsHelpers.assertEqualsBatch(
+            schema.asStruct(), expectedIter, batch, idToConstant, numRowsRead);
         numRowsRead += batch.numRows();
-        GenericsHelpers.assertEqualsBatch(schema.asStruct(), expectedIter, 
batch);
       }
       assertThat(numRowsRead).isEqualTo(expectedSize);
     }

Reply via email to