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

pvary 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 22ed06b979 Data, Spark 4.2: Add format model TCK coverage for 
vectorized reads (#17610)
22ed06b979 is described below

commit 22ed06b9792c7161523e5ffa5dde4b199e0c6629
Author: Joy Haldar <[email protected]>
AuthorDate: Wed Nov 4 20:26:35 2026 +0530

    Data, Spark 4.2: Add format model TCK coverage for vectorized reads (#17610)
---
 .../apache/iceberg/data/BaseFormatModelTests.java  |  10 +-
 .../org/apache/iceberg/data/DataGenerator.java     |   2 +-
 .../org/apache/iceberg/data/DataGenerators.java    |   4 +-
 .../apache/iceberg/data/ReadFormatModelTests.java  | 286 ++++++++++++---------
 .../spark/data/TestSparkVectorizedFormatModel.java | 207 +++++++++++++++
 5 files changed, 382 insertions(+), 127 deletions(-)

diff --git 
a/data/src/test/java/org/apache/iceberg/data/BaseFormatModelTests.java 
b/data/src/test/java/org/apache/iceberg/data/BaseFormatModelTests.java
index e45ebbbedc..496f77a321 100644
--- a/data/src/test/java/org/apache/iceberg/data/BaseFormatModelTests.java
+++ b/data/src/test/java/org/apache/iceberg/data/BaseFormatModelTests.java
@@ -63,7 +63,7 @@ public abstract class BaseFormatModelTests<T> extends 
ReadFormatModelTests<T> {
   @FieldSource("FORMAT_AND_GENERATOR")
   void testDataWriterEngineWriteGenericRead(FileFormat fileFormat, 
DataGenerator dataGenerator)
       throws IOException {
-    Schema schema = supportedSchema(dataGenerator);
+    Schema schema = supportedSchema(fileFormat, dataGenerator);
     List<Record> genericRecords = project(dataGenerator.generateRecords(), 
schema);
     List<T> engineRecords = convertToEngineRecords(genericRecords, schema);
     writeEngineRecords(fileFormat, schema, engineRecords, 
engineSchema(schema));
@@ -75,7 +75,7 @@ public abstract class BaseFormatModelTests<T> extends 
ReadFormatModelTests<T> {
   @FieldSource("FORMAT_AND_GENERATOR")
   void testDataWriterEngineWriteWithoutEngineSchema(
       FileFormat fileFormat, DataGenerator dataGenerator) throws IOException {
-    Schema schema = supportedSchema(dataGenerator);
+    Schema schema = supportedSchema(fileFormat, dataGenerator);
     List<Record> genericRecords = project(dataGenerator.generateRecords(), 
schema);
     List<T> engineRecords = convertToEngineRecords(genericRecords, schema);
     writeEngineRecords(fileFormat, schema, engineRecords);
@@ -87,7 +87,7 @@ public abstract class BaseFormatModelTests<T> extends 
ReadFormatModelTests<T> {
   @FieldSource("FORMAT_AND_GENERATOR")
   void testDataWriterEngineWriteEngineRead(FileFormat fileFormat, 
DataGenerator dataGenerator)
       throws IOException {
-    Schema schema = supportedSchema(dataGenerator);
+    Schema schema = supportedSchema(fileFormat, dataGenerator);
     List<Record> genericRecords = project(dataGenerator.generateRecords(), 
schema);
     List<T> engineRecords = convertToEngineRecords(genericRecords, schema);
     writeEngineRecords(fileFormat, schema, engineRecords);
@@ -99,7 +99,7 @@ public abstract class BaseFormatModelTests<T> extends 
ReadFormatModelTests<T> {
   @FieldSource("FORMAT_AND_GENERATOR")
   void testEqualityDeleteWriterEngineWriteGenericRead(
       FileFormat fileFormat, DataGenerator dataGenerator) throws IOException {
-    Schema schema = supportedSchema(dataGenerator);
+    Schema schema = supportedSchema(fileFormat, dataGenerator);
     FileWriterBuilder<EqualityDeleteWriter<T>, Object> writerBuilder =
         FormatModelRegistry.equalityDeleteWriteBuilder(fileFormat, 
engineType(), encryptedFile);
 
@@ -135,7 +135,7 @@ public abstract class BaseFormatModelTests<T> extends 
ReadFormatModelTests<T> {
   @FieldSource("FORMAT_AND_GENERATOR")
   void testEqualityDeleteWriterEngineWriteWithoutEngineSchema(
       FileFormat fileFormat, DataGenerator dataGenerator) throws IOException {
-    Schema schema = supportedSchema(dataGenerator);
+    Schema schema = supportedSchema(fileFormat, dataGenerator);
     FileWriterBuilder<EqualityDeleteWriter<T>, Object> writerBuilder =
         FormatModelRegistry.equalityDeleteWriteBuilder(fileFormat, 
engineType(), encryptedFile);
 
diff --git a/data/src/test/java/org/apache/iceberg/data/DataGenerator.java 
b/data/src/test/java/org/apache/iceberg/data/DataGenerator.java
index ad068fbc68..7c969f44e2 100644
--- a/data/src/test/java/org/apache/iceberg/data/DataGenerator.java
+++ b/data/src/test/java/org/apache/iceberg/data/DataGenerator.java
@@ -22,7 +22,7 @@ import java.util.List;
 import org.apache.iceberg.Schema;
 
 /** Interface for generating test data with different schema types. */
-interface DataGenerator {
+public interface DataGenerator {
 
   /** Returns the Iceberg schema for this test data. */
   Schema schema();
diff --git a/data/src/test/java/org/apache/iceberg/data/DataGenerators.java 
b/data/src/test/java/org/apache/iceberg/data/DataGenerators.java
index cf2ab00d7c..bdfbe3acf8 100644
--- a/data/src/test/java/org/apache/iceberg/data/DataGenerators.java
+++ b/data/src/test/java/org/apache/iceberg/data/DataGenerators.java
@@ -32,9 +32,9 @@ import org.apache.iceberg.util.DateTimeUtil;
  * Test data generators for different schema types. Add new generators to ALL 
array to include them
  * in format model tests.
  */
-class DataGenerators {
+public class DataGenerators {
 
-  static final DataGenerator[] ALL =
+  public static final DataGenerator[] ALL =
       new DataGenerator[] {
         new Primitives(),
         new UUID(),
diff --git 
a/data/src/test/java/org/apache/iceberg/data/ReadFormatModelTests.java 
b/data/src/test/java/org/apache/iceberg/data/ReadFormatModelTests.java
index b8596c632b..971012e274 100644
--- a/data/src/test/java/org/apache/iceberg/data/ReadFormatModelTests.java
+++ b/data/src/test/java/org/apache/iceberg/data/ReadFormatModelTests.java
@@ -24,7 +24,6 @@ import static org.apache.iceberg.TestBase.SCHEMA;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.Assumptions.assumeThat;
-import static org.junit.jupiter.api.Assumptions.assumeFalse;
 
 import java.io.File;
 import java.io.IOException;
@@ -105,6 +104,8 @@ import org.junit.jupiter.params.provider.FieldSource;
  */
 public abstract class ReadFormatModelTests<T> {
 
+  @TempDir private File tableDir;
+
   protected abstract Class<T> engineType();
 
   protected abstract Object engineSchema(Schema schema);
@@ -119,19 +120,49 @@ public abstract class ReadFormatModelTests<T> {
     return false;
   }
 
-  @TempDir private File tableDir;
+  protected void assertRecordsEqual(
+      Schema schema, List<Record> expected, CloseableIterable<T> actual) {
+    int rowOffset = 0;
+
+    for (T recordOrBatch : actual) {
+      int numRows = countRows(List.of(recordOrBatch));
+      assertThat(rowOffset + numRows).isLessThanOrEqualTo(expected.size());
+      assertEquals(
+          schema,
+          convertToEngineRecords(expected.subList(rowOffset, rowOffset + 
numRows), schema),
+          List.of(recordOrBatch));
+
+      rowOffset += numRows;
+    }
+
+    assertThat(rowOffset).isEqualTo(expected.size());
+  }
 
   /**
    * Returns the set of type IDs that this engine cannot handle. The base 
implementation returns an
-   * empty collection; engines override to declare which top-level columns 
should be excluded from
-   * test schemas.
+   * empty collection, engines override to declare which types should be 
filtered from test schemas
+   * or cause tests to be skipped.
    */
   protected Collection<Type.TypeID> unsupportedTypeIds() {
     return Set.of();
   }
 
-  private Schema filterUnsupported(Schema schema) {
-    Collection<Type.TypeID> unsupportedTypeIds = unsupportedTypeIds();
+  /**
+   * Types unsupported by this engine for the given file format. Defaults to 
the format-independent
+   * set.
+   */
+  protected Collection<Type.TypeID> unsupportedTypeIds(FileFormat fileFormat) {
+    return unsupportedTypeIds();
+  }
+
+  /** Returns true if the schema contains any of the given types, ignoring the 
root struct. */
+  protected static boolean containsType(Schema schema, Collection<Type.TypeID> 
typeIds) {
+    Types.StructType root = schema.asStruct();
+    return TypeUtil.find(schema, type -> type != root && 
typeIds.contains(type.typeId())) != null;
+  }
+
+  private Schema filterUnsupported(FileFormat fileFormat, Schema schema) {
+    Collection<Type.TypeID> unsupportedTypeIds = 
unsupportedTypeIds(fileFormat);
     if (unsupportedTypeIds.isEmpty()) {
       return schema;
     }
@@ -145,8 +176,35 @@ public abstract class ReadFormatModelTests<T> {
     return kept.size() == schema.columns().size() ? schema : new Schema(kept);
   }
 
-  protected Schema supportedSchema(DataGenerator generator) {
-    Schema filtered = filterUnsupported(generator.schema());
+  protected void assumeSupportedTypes(FileFormat fileFormat, Schema schema) {
+    assumeThat(containsType(schema, unsupportedTypeIds(fileFormat)))
+        .as("Schema contains a type unsupported by this engine")
+        .isFalse();
+  }
+
+  /**
+   * Returns the number of rows in the read result, batch engines override to 
sum across batches.
+   */
+  protected int countRows(List<T> records) {
+    return records.size();
+  }
+
+  private ReadBuilder<T, ?> readBuilder(FileFormat fileFormat, InputFile 
inputFile) {
+    ReadBuilder<T, ?> builder =
+        FormatModelRegistry.readBuilder(fileFormat, engineType(), inputFile);
+    return supportsBatchReads() ? builder.recordsPerBatch(3) : builder;
+  }
+
+  protected void assertContainersNotReused(T first, T second) {
+    assertThat(second).isNotSameAs(first);
+  }
+
+  protected void assertContainersReused(T first, T second) {
+    assertThat(second).isSameAs(first);
+  }
+
+  protected Schema supportedSchema(FileFormat fileFormat, DataGenerator 
generator) {
+    Schema filtered = filterUnsupported(fileFormat, generator.schema());
     assumeThat(filtered.columns())
         .as("All columns of %s are unsupported by this engine", generator)
         .isNotEmpty();
@@ -254,22 +312,17 @@ public abstract class ReadFormatModelTests<T> {
   @FieldSource("FORMAT_AND_GENERATOR")
   void testDataWriterGenericWriteEngineRead(FileFormat fileFormat, 
DataGenerator dataGenerator)
       throws IOException {
-    Schema schema = supportedSchema(dataGenerator);
+    Schema schema = supportedSchema(fileFormat, dataGenerator);
 
     List<Record> genericRecords = project(dataGenerator.generateRecords(), 
schema);
     writeGenericRecords(fileFormat, schema, genericRecords);
 
     // Read back and verify
     InputFile inputFile = encryptedFile.encryptingOutputFile().toInputFile();
-    List<T> readRecords;
-    try (CloseableIterable<T> reader =
-        FormatModelRegistry.readBuilder(fileFormat, engineType(), inputFile)
-            .project(schema)
-            .build()) {
-      readRecords = ImmutableList.copyOf(reader);
-    }
 
-    assertEquals(schema, convertToEngineRecords(genericRecords, schema), 
readRecords);
+    try (CloseableIterable<T> reader = readBuilder(fileFormat, 
inputFile).project(schema).build()) {
+      assertRecordsEqual(schema, genericRecords, reader);
+    }
   }
 
   /** Write with Generic Record, read with engine type T */
@@ -277,7 +330,7 @@ public abstract class ReadFormatModelTests<T> {
   @FieldSource("FORMAT_AND_GENERATOR")
   void testEqualityDeleteWriterGenericWriteEngineRead(
       FileFormat fileFormat, DataGenerator dataGenerator) throws IOException {
-    Schema schema = supportedSchema(dataGenerator);
+    Schema schema = supportedSchema(fileFormat, dataGenerator);
     FileWriterBuilder<EqualityDeleteWriter<Record>, Object> writerBuilder =
         FormatModelRegistry.equalityDeleteWriteBuilder(fileFormat, 
Record.class, encryptedFile);
 
@@ -303,15 +356,10 @@ public abstract class ReadFormatModelTests<T> {
 
     // Read back and verify
     InputFile inputFile = encryptedFile.encryptingOutputFile().toInputFile();
-    List<T> readRecords;
-    try (CloseableIterable<T> reader =
-        FormatModelRegistry.readBuilder(fileFormat, engineType(), inputFile)
-            .project(schema)
-            .build()) {
-      readRecords = ImmutableList.copyOf(reader);
-    }
 
-    assertEquals(schema, convertToEngineRecords(genericRecords, schema), 
readRecords);
+    try (CloseableIterable<T> reader = readBuilder(fileFormat, 
inputFile).project(schema).build()) {
+      assertRecordsEqual(schema, genericRecords, reader);
+    }
   }
 
   /** Write position deletes, read with Generic Record */
@@ -370,19 +418,13 @@ public abstract class ReadFormatModelTests<T> {
     writeGenericRecords(fileFormat, fullSchema, genericRecords);
 
     List<Record> projectedGenericRecords = project(genericRecords, 
projectedSchema);
-    List<T> expectedEngineRecords =
-        convertToEngineRecords(projectedGenericRecords, projectedSchema);
 
     InputFile inputFile = encryptedFile.encryptingOutputFile().toInputFile();
-    List<T> readRecords;
+
     try (CloseableIterable<T> reader =
-        FormatModelRegistry.readBuilder(fileFormat, engineType(), inputFile)
-            .project(projectedSchema)
-            .build()) {
-      readRecords = ImmutableList.copyOf(reader);
+        readBuilder(fileFormat, inputFile).project(projectedSchema).build()) {
+      assertRecordsEqual(projectedSchema, projectedGenericRecords, reader);
     }
-
-    assertEquals(projectedSchema, expectedEngineRecords, readRecords);
   }
 
   @ParameterizedTest
@@ -408,27 +450,21 @@ public abstract class ReadFormatModelTests<T> {
     InputFile inputFile = encryptedFile.encryptingOutputFile().toInputFile();
     List<T> readRecords;
     try (CloseableIterable<T> reader =
-        FormatModelRegistry.readBuilder(fileFormat, engineType(), inputFile)
-            .project(schema)
-            .filter(lessThanFilter)
-            .build()) {
+        readBuilder(fileFormat, 
inputFile).project(schema).filter(lessThanFilter).build()) {
       readRecords = ImmutableList.copyOf(reader);
     }
 
-    assertThat(readRecords).isEmpty();
+    assertThat(countRows(readRecords)).isZero();
 
     // Filter: id >= count - 1, so only the last record matches across all row 
groups
     Expression greaterThanFilter = Expressions.greaterThanOrEqual("id", count 
- 1);
 
     try (CloseableIterable<T> reader =
-        FormatModelRegistry.readBuilder(fileFormat, engineType(), inputFile)
-            .project(schema)
-            .filter(greaterThanFilter)
-            .build()) {
+        readBuilder(fileFormat, 
inputFile).project(schema).filter(greaterThanFilter).build()) {
       readRecords = ImmutableList.copyOf(reader);
     }
 
-    
assertThat(readRecords).hasSizeGreaterThan(0).hasSizeLessThan(genericRecords.size());
+    
assertThat(countRows(readRecords)).isGreaterThan(0).isLessThan(genericRecords.size());
   }
 
   /**
@@ -458,7 +494,7 @@ public abstract class ReadFormatModelTests<T> {
     // caseSensitive=false: upper-cased column name must be resolved correctly.
     List<T> readRecords;
     try (CloseableIterable<T> reader =
-        FormatModelRegistry.readBuilder(fileFormat, engineType(), inputFile)
+        readBuilder(fileFormat, inputFile)
             .project(schema)
             .filter(upperCaseFilter)
             .caseSensitive(false)
@@ -466,13 +502,13 @@ public abstract class ReadFormatModelTests<T> {
       readRecords = ImmutableList.copyOf(reader);
     }
 
-    assertThat(readRecords).isNotEmpty();
+    assertThat(countRows(readRecords)).isGreaterThan(0);
 
     // caseSensitive=true: upper-cased column name cannot be resolved, so must 
throw.
     assertThatThrownBy(
             () -> {
               try (CloseableIterable<T> reader =
-                  FormatModelRegistry.readBuilder(fileFormat, engineType(), 
inputFile)
+                  readBuilder(fileFormat, inputFile)
                       .project(schema)
                       .filter(upperCaseFilter)
                       .caseSensitive(true)
@@ -495,7 +531,6 @@ public abstract class ReadFormatModelTests<T> {
   @ParameterizedTest
   @FieldSource("FILE_FORMATS")
   void testReaderBuilderSplit(FileFormat fileFormat) throws IOException {
-
     assumeSupports(fileFormat, FEATURE_SPLIT);
 
     DataGenerator dataGenerator = new DataGenerators.DefaultSchema();
@@ -514,44 +549,37 @@ public abstract class ReadFormatModelTests<T> {
 
     List<T> readRecords;
     try (CloseableIterable<T> reader =
-        FormatModelRegistry.readBuilder(fileFormat, engineType(), inputFile)
+        readBuilder(fileFormat, inputFile)
             .project(schema)
             .split(firstSplitStart, firstSplitLength)
             .build()) {
       readRecords = ImmutableList.copyOf(reader);
     }
 
-    
assertThat(readRecords).hasSizeGreaterThan(0).hasSizeLessThan(records.size());
+    
assertThat(countRows(readRecords)).isGreaterThan(0).isLessThan(records.size());
 
     // split(fileLength, 0): empty range at the end of the file, so no records 
should be returned
     List<T> emptyReadRecords;
     try (CloseableIterable<T> reader =
-        FormatModelRegistry.readBuilder(fileFormat, engineType(), inputFile)
-            .project(schema)
-            .split(fileLength, 0)
-            .build()) {
+        readBuilder(fileFormat, inputFile).project(schema).split(fileLength, 
0).build()) {
       emptyReadRecords = ImmutableList.copyOf(reader);
     }
 
-    assertThat(emptyReadRecords).isEmpty();
+    assertThat(countRows(emptyReadRecords)).isZero();
 
     // split(0, fileLength): full file range, so all records should be returned
     try (CloseableIterable<T> reader =
-        FormatModelRegistry.readBuilder(fileFormat, engineType(), inputFile)
-            .project(schema)
-            .split(0, fileLength)
-            .build()) {
+        readBuilder(fileFormat, inputFile).project(schema).split(0, 
fileLength).build()) {
       readRecords = ImmutableList.copyOf(reader);
     }
 
-    assertThat(readRecords).hasSize(records.size());
+    assertThat(countRows(readRecords)).isEqualTo(records.size());
   }
 
   /** Verifies the contract of reuseContainers */
   @ParameterizedTest
   @FieldSource("FILE_FORMATS")
   void testReaderBuilderReuseContainers(FileFormat fileFormat) throws 
IOException {
-
     assumeSupports(fileFormat, FEATURE_REUSE_CONTAINERS);
 
     DataGenerator dataGenerator = new DataGenerators.DefaultSchema();
@@ -564,31 +592,25 @@ public abstract class ReadFormatModelTests<T> {
     InputFile inputFile = encryptedFile.encryptingOutputFile().toInputFile();
 
     // Without reuseContainers: every record must be a distinct object instance
-    List<T> noReuseRecords;
-    try (CloseableIterable<T> reader =
-        FormatModelRegistry.readBuilder(fileFormat, engineType(), inputFile)
-            .project(schema)
-            .build()) {
-      noReuseRecords = ImmutableList.copyOf(reader);
-    }
+    try (CloseableIterable<T> reader = readBuilder(fileFormat, 
inputFile).project(schema).build()) {
+      List<T> noReuseRecords = ImmutableList.copyOf(reader);
 
-    for (int i = 0; i < noReuseRecords.size(); i++) {
-      for (int j = i + 1; j < noReuseRecords.size(); j++) {
-        assertThat(noReuseRecords.get(i)).isNotSameAs(noReuseRecords.get(j));
+      assertThat(noReuseRecords).hasSizeGreaterThan(1);
+      for (int i = 0; i < noReuseRecords.size(); i++) {
+        for (int j = i + 1; j < noReuseRecords.size(); j++) {
+          assertContainersNotReused(noReuseRecords.get(i), 
noReuseRecords.get(j));
+        }
       }
     }
 
-    // With reuseContainers: all collected elements must be the same object 
instance
-    List<T> reuseRecords;
+    // With reuseContainers: verify that the underlying containers are reused
     try (CloseableIterable<T> reader =
-        FormatModelRegistry.readBuilder(fileFormat, engineType(), inputFile)
-            .project(schema)
-            .reuseContainers()
-            .build()) {
-      reuseRecords = ImmutableList.copyOf(reader);
-    }
+        readBuilder(fileFormat, 
inputFile).project(schema).reuseContainers().build()) {
+      List<T> reuseRecords = ImmutableList.copyOf(reader);
 
-    reuseRecords.forEach(r -> assertThat(r).isSameAs(reuseRecords.get(0)));
+      assertThat(reuseRecords).hasSizeGreaterThan(1);
+      reuseRecords.forEach(r -> assertContainersReused(reuseRecords.get(0), 
r));
+    }
   }
 
   @ParameterizedTest
@@ -709,6 +731,7 @@ public abstract class ReadFormatModelTests<T> {
                 .withDoc("Used to test nested field defaults")
                 .build());
 
+    assumeSupportedTypes(fileFormat, writeSchema);
     List<Record> genericRecords = RandomGenericData.generate(writeSchema, 10, 
1L);
     writeGenericRecords(fileFormat, writeSchema, genericRecords);
 
@@ -779,6 +802,7 @@ public abstract class ReadFormatModelTests<T> {
                 .withDoc("Used to test nested map value field defaults")
                 .build());
 
+    assumeSupportedTypes(fileFormat, writeSchema);
     List<Record> genericRecords = RandomGenericData.generate(writeSchema, 10, 
1L);
     writeGenericRecords(fileFormat, writeSchema, genericRecords);
 
@@ -864,6 +888,7 @@ public abstract class ReadFormatModelTests<T> {
                 .withDoc("Used to test nested field defaults")
                 .build());
 
+    assumeSupportedTypes(fileFormat, writeSchema);
     List<Record> genericRecords = RandomGenericData.generate(writeSchema, 10, 
1L);
     writeGenericRecords(fileFormat, writeSchema, genericRecords);
 
@@ -938,6 +963,8 @@ public abstract class ReadFormatModelTests<T> {
         
Types.NestedField.optional("nested").withId(2).ofType(writeNested).build();
     Schema writeSchema = new Schema(idField, nestedField);
 
+    assumeSupportedTypes(fileFormat, writeSchema);
+
     Record presentNested = GenericRecord.create(writeNested);
     presentNested.setField("a", "x");
     presentNested.setField("b", "y");
@@ -1001,6 +1028,7 @@ public abstract class ReadFormatModelTests<T> {
                     Types.NestedField.required(5, "inner", 
Types.StringType.get())))
             .build();
     Schema writeSchema = new Schema(idField, nestedField);
+    assumeSupportedTypes(fileFormat, writeSchema);
     Types.StructType nestedType = nestedField.type().asStructType();
 
     // alternate present and null structs, with a different number of list 
elements per row so
@@ -1076,6 +1104,7 @@ public abstract class ReadFormatModelTests<T> {
                         3, "tags", Types.ListType.ofRequired(4, 
Types.StringType.get()))))
             .build();
     Schema writeSchema = new Schema(idField, nestedField);
+    assumeSupportedTypes(fileFormat, writeSchema);
     Types.StructType nestedType = nestedField.type().asStructType();
 
     List<Record> genericRecords = Lists.newArrayList();
@@ -1113,6 +1142,8 @@ public abstract class ReadFormatModelTests<T> {
             Types.StructType.of(Types.NestedField.required(3, "inner", 
Types.StringType.get())));
     Schema writeSchema = new Schema(idField, nestedField);
 
+    assumeSupportedTypes(fileFormat, writeSchema);
+
     Record present = GenericRecord.create(writeSchema);
     present.setField("id", 1L);
     Record presentNested = 
GenericRecord.create(nestedField.type().asStructType());
@@ -1170,6 +1201,7 @@ public abstract class ReadFormatModelTests<T> {
                 .ofType(Types.ListType.ofOptional(3, writeElement))
                 .build());
 
+    assumeSupportedTypes(fileFormat, writeSchema);
     Record present = GenericRecord.create(writeElement);
     present.setField("inner", "a");
     Record row = GenericRecord.create(writeSchema);
@@ -1225,6 +1257,7 @@ public abstract class ReadFormatModelTests<T> {
                 .ofType(Types.ListType.ofOptional(3, writeElement))
                 .build());
 
+    assumeSupportedTypes(fileFormat, writeSchema);
     Record nullList = GenericRecord.create(writeSchema);
     nullList.setField("id", 1L);
     nullList.setField("arr", null);
@@ -1279,6 +1312,8 @@ public abstract class ReadFormatModelTests<T> {
                 .ofType(Types.MapType.ofOptional(3, 4, Types.StringType.get(), 
writeValue))
                 .build());
 
+    assumeSupportedTypes(fileFormat, writeSchema);
+
     Record present = GenericRecord.create(writeValue);
     present.setField("inner", "a");
     Map<String, Record> writtenMap = Maps.newLinkedHashMap();
@@ -1340,6 +1375,8 @@ public abstract class ReadFormatModelTests<T> {
                 .ofType(Types.MapType.ofOptional(3, 4, Types.StringType.get(), 
writeValue))
                 .build());
 
+    assumeSupportedTypes(fileFormat, writeSchema);
+
     Record nullMap = GenericRecord.create(writeSchema);
     nullMap.setField("id", 1L);
     nullMap.setField("map", null);
@@ -1411,7 +1448,7 @@ public abstract class ReadFormatModelTests<T> {
     assumeSupports(fileFormat, FEATURE_READER_DEFAULT);
 
     Schema writeSchema = DataGenerators.PrimitiveDefaults.WRITE_SCHEMA;
-    Schema readSchema = 
filterUnsupported(DataGenerators.PrimitiveDefaults.READ_SCHEMA);
+    Schema readSchema = filterUnsupported(fileFormat, 
DataGenerators.PrimitiveDefaults.READ_SCHEMA);
 
     List<Record> sourceRecords = RandomGenericData.generate(writeSchema, 10, 
1L);
     writeGenericRecords(fileFormat, writeSchema, sourceRecords);
@@ -1440,7 +1477,7 @@ public abstract class ReadFormatModelTests<T> {
   void testPrimitiveDefaultValuesNotApplied(FileFormat fileFormat) throws 
IOException {
     assumeSupports(fileFormat, FEATURE_READER_DEFAULT);
 
-    Schema readSchema = 
filterUnsupported(DataGenerators.PrimitiveDefaults.READ_SCHEMA);
+    Schema readSchema = filterUnsupported(fileFormat, 
DataGenerators.PrimitiveDefaults.READ_SCHEMA);
 
     List<Record> sourceRecords = RandomGenericData.generate(readSchema, 10, 
1L);
     writeGenericRecords(fileFormat, readSchema, sourceRecords);
@@ -1451,7 +1488,9 @@ public abstract class ReadFormatModelTests<T> {
   @ParameterizedTest
   @FieldSource("FILE_FORMATS")
   void testReaderBuilderRecordsPerBatchNotSupported(FileFormat fileFormat) 
throws IOException {
-    assumeFalse(supportsBatchReads(), engineType().getSimpleName() + " 
supports batch reads");
+    assumeThat(supportsBatchReads())
+        .as(engineType().getSimpleName() + " supports batch reads")
+        .isFalse();
 
     DataGenerator dataGenerator = new DataGenerators.DefaultSchema();
     Schema schema = dataGenerator.schema();
@@ -1467,6 +1506,35 @@ public abstract class ReadFormatModelTests<T> {
         .isInstanceOf(UnsupportedOperationException.class);
   }
 
+  @ParameterizedTest
+  @FieldSource("FILE_FORMATS")
+  void testReaderBuilderRecordsPerBatch(FileFormat fileFormat) throws 
IOException {
+    assumeThat(supportsBatchReads())
+        .as(engineType().getSimpleName() + " does not support batch reads")
+        .isTrue();
+
+    DataGenerator dataGenerator = new DataGenerators.DefaultSchema();
+    Schema schema = dataGenerator.schema();
+    List<Record> genericRecords = dataGenerator.generateRecords(1000);
+    writeGenericRecords(fileFormat, schema, genericRecords);
+
+    InputFile inputFile = encryptedFile.encryptingOutputFile().toInputFile();
+    List<T> readRecords;
+    try (CloseableIterable<T> reader =
+        FormatModelRegistry.readBuilder(fileFormat, engineType(), inputFile)
+            .project(schema)
+            .recordsPerBatch(100)
+            .build()) {
+      readRecords = ImmutableList.copyOf(reader);
+    }
+
+    for (T batch : readRecords) {
+      assertThat(countRows(List.of(batch))).isLessThanOrEqualTo(100);
+    }
+
+    assertThat(countRows(readRecords)).isEqualTo(genericRecords.size());
+  }
+
   @ParameterizedTest
   @FieldSource("FILE_FORMATS")
   void testDataWriterMetricsCollection(FileFormat fileFormat) throws 
IOException {
@@ -1842,8 +1910,7 @@ public abstract class ReadFormatModelTests<T> {
 
   @ParameterizedTest
   @FieldSource("FILE_FORMATS")
-  void testReadMetadataColumnIsDeleted(FileFormat fileFormat) throws 
IOException {
-
+  protected void testReadMetadataColumnIsDeleted(FileFormat fileFormat) throws 
IOException {
     DataGenerator dataGenerator = new DataGenerators.DefaultSchema();
     Schema schema = dataGenerator.schema();
     List<Record> genericRecords = dataGenerator.generateRecords();
@@ -1862,8 +1929,7 @@ public abstract class ReadFormatModelTests<T> {
 
   @ParameterizedTest
   @FieldSource("FILE_FORMATS")
-  void testReadMetadataColumnRowLineage(FileFormat fileFormat) throws 
IOException {
-
+  protected void testReadMetadataColumnRowLineage(FileFormat fileFormat) 
throws IOException {
     DataGenerator dataGenerator = new DataGenerators.DefaultSchema();
     Schema schema = dataGenerator.schema();
     List<Record> genericRecords = dataGenerator.generateRecords();
@@ -1895,8 +1961,8 @@ public abstract class ReadFormatModelTests<T> {
 
   @ParameterizedTest
   @FieldSource("FILE_FORMATS")
-  void testReadMetadataColumnRowLineageExistingValues(FileFormat fileFormat) 
throws IOException {
-
+  protected void testReadMetadataColumnRowLineageExistingValues(FileFormat 
fileFormat)
+      throws IOException {
     DataGenerator dataGenerator = new DataGenerators.DefaultSchema();
     Schema dataSchema = dataGenerator.schema();
 
@@ -2367,13 +2433,11 @@ public abstract class ReadFormatModelTests<T> {
     InputFile inputFile = encryptedFile.encryptingOutputFile().toInputFile();
     List<T> readRecords;
     try (CloseableIterable<T> reader =
-        FormatModelRegistry.readBuilder(fileFormat, engineType(), inputFile)
-            .project(emptySchema)
-            .build()) {
+        readBuilder(fileFormat, inputFile).project(emptySchema).build()) {
       readRecords = ImmutableList.copyOf(reader);
     }
 
-    assertThat(readRecords).hasSameSizeAs(genericRecords);
+    assertThat(countRows(readRecords)).isEqualTo(genericRecords.size());
   }
 
   @ParameterizedTest
@@ -2392,16 +2456,14 @@ public abstract class ReadFormatModelTests<T> {
     NameMapping nameMapping = MappingUtil.create(icebergSchema);
 
     InputFile inputFile = encryptedFile.encryptingOutputFile().toInputFile();
-    List<T> readRecords;
+
     try (CloseableIterable<T> reader =
-        FormatModelRegistry.readBuilder(fileFormat, engineType(), inputFile)
+        readBuilder(fileFormat, inputFile)
             .project(icebergSchema)
             .withNameMapping(nameMapping)
             .build()) {
-      readRecords = ImmutableList.copyOf(reader);
+      assertRecordsEqual(icebergSchema, genericRecords, reader);
     }
-
-    assertEquals(icebergSchema, convertToEngineRecords(genericRecords, 
icebergSchema), readRecords);
   }
 
   protected void readAndAssertGenericRecords(
@@ -2456,7 +2518,7 @@ public abstract class ReadFormatModelTests<T> {
     assumeThat(MISSING_FEATURES.getOrDefault(fileFormat, new String[] 
{})).doesNotContain(feature);
   }
 
-  private static boolean supportsGenerator(FileFormat fileFormat, 
DataGenerator generator) {
+  protected static boolean supportsGenerator(FileFormat fileFormat, 
DataGenerator generator) {
     Schema schema = generator.schema();
     return supportsTypes(fileFormat, schema, VARIANT_TYPE_IDS, FEATURE_VARIANT)
         && supportsTypes(fileFormat, schema, GEOSPATIAL_TYPE_IDS, 
FEATURE_GEOSPATIAL);
@@ -2870,23 +2932,15 @@ public abstract class ReadFormatModelTests<T> {
       throws IOException {
 
     InputFile inputFile = encryptedFile.encryptingOutputFile().toInputFile();
-    List<T> readRecords;
-
-    ReadBuilder<T, ?> readerBuilder =
-        FormatModelRegistry.readBuilder(fileFormat, engineType(), inputFile)
-            .project(projectionSchema);
+    ReadBuilder<T, ?> readerBuilder = readBuilder(fileFormat, 
inputFile).project(projectionSchema);
 
     if (idToConstant != null) {
       readerBuilder.idToConstant(convertConstantsToEngine(projectionSchema, 
idToConstant));
     }
 
     try (CloseableIterable<T> reader = readerBuilder.build()) {
-      readRecords = ImmutableList.copyOf(reader);
+      assertRecordsEqual(projectionSchema, expectedRecords, reader);
     }
-
-    assertThat(readRecords).hasSize(expectedRecords.size());
-    assertEquals(
-        projectionSchema, convertToEngineRecords(expectedRecords, 
projectionSchema), readRecords);
   }
 
   private static Record copy(Record source, Schema sourceSchema, Schema 
targetSchema) {
@@ -2927,16 +2981,10 @@ public abstract class ReadFormatModelTests<T> {
       throws IOException {
     List<Record> expectedGenericRecords = 
sourceRecords.stream().map(converter).toList();
     InputFile inputFile = encryptedFile.encryptingOutputFile().toInputFile();
-    List<T> readRecords;
+
     try (CloseableIterable<T> reader =
-        FormatModelRegistry.readBuilder(fileFormat, engineType(), inputFile)
-            .project(readSchema)
-            .build()) {
-      readRecords = ImmutableList.copyOf(reader);
+        readBuilder(fileFormat, inputFile).project(readSchema).build()) {
+      assertRecordsEqual(readSchema, expectedGenericRecords, reader);
     }
-
-    assertThat(readRecords).hasSize(expectedGenericRecords.size());
-    assertEquals(
-        readSchema, convertToEngineRecords(expectedGenericRecords, 
readSchema), readRecords);
   }
 }
diff --git 
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/data/TestSparkVectorizedFormatModel.java
 
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/data/TestSparkVectorizedFormatModel.java
new file mode 100644
index 0000000000..88b4d7e214
--- /dev/null
+++ 
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/data/TestSparkVectorizedFormatModel.java
@@ -0,0 +1,207 @@
+/*
+ * 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.iceberg.spark.data;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assumptions.assumeThat;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import org.apache.arrow.vector.ValueVector;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.data.DataGenerator;
+import org.apache.iceberg.data.DataGenerators;
+import org.apache.iceberg.data.ReadFormatModelTests;
+import org.apache.iceberg.data.Record;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableSet;
+import org.apache.iceberg.spark.SparkSchemaUtil;
+import org.apache.iceberg.spark.SparkUtil;
+import org.apache.iceberg.spark.data.vectorized.IcebergArrowColumnVector;
+import org.apache.iceberg.types.Type;
+import org.apache.spark.sql.vectorized.ColumnarBatch;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.FieldSource;
+
+class TestSparkVectorizedFormatModel extends 
ReadFormatModelTests<ColumnarBatch> {
+
+  private static final FileFormat[] FILE_FORMATS = {FileFormat.ORC, 
FileFormat.PARQUET};
+
+  private static final Set<Type.TypeID> SPARK_UNSUPPORTED_TYPE_IDS =
+      Set.of(Type.TypeID.TIME, Type.TypeID.TIMESTAMP_NANO);
+
+  // The Arrow-based Parquet reader cannot read nested or geospatial columns;
+  // the vectorized ORC reader supports nested types.
+  private static final Set<Type.TypeID> ARROW_UNSUPPORTED_TYPE_IDS =
+      Set.of(
+          Type.TypeID.STRUCT,
+          Type.TypeID.LIST,
+          Type.TypeID.MAP,
+          Type.TypeID.GEOMETRY,
+          Type.TypeID.GEOGRAPHY);
+
+  private static final Set<Type.TypeID> PARQUET_UNSUPPORTED_TYPE_IDS =
+      ImmutableSet.<Type.TypeID>builder()
+          .addAll(SPARK_UNSUPPORTED_TYPE_IDS)
+          .addAll(ARROW_UNSUPPORTED_TYPE_IDS)
+          .build();
+
+  private static final List<Arguments> FORMAT_AND_GENERATOR =
+      Arrays.stream(FILE_FORMATS)
+          .flatMap(
+              format ->
+                  Arrays.stream(DataGenerators.ALL)
+                      .filter(generator -> supportsVectorized(format, 
generator))
+                      .map(generator -> Arguments.of(format, generator)))
+          .toList();
+
+  private static boolean supportsVectorized(FileFormat format, DataGenerator 
generator) {
+    return supportsGenerator(format, generator)
+        && (format != FileFormat.PARQUET
+            || !containsType(generator.schema(), ARROW_UNSUPPORTED_TYPE_IDS));
+  }
+
+  @Override
+  protected Collection<Type.TypeID> unsupportedTypeIds(FileFormat fileFormat) {
+    return fileFormat == FileFormat.PARQUET
+        ? PARQUET_UNSUPPORTED_TYPE_IDS
+        : SPARK_UNSUPPORTED_TYPE_IDS;
+  }
+
+  @Override
+  protected boolean supportsBatchReads() {
+    return true;
+  }
+
+  @Override
+  protected int countRows(List<ColumnarBatch> batches) {
+    return batches.stream().mapToInt(ColumnarBatch::numRows).sum();
+  }
+
+  @Override
+  protected void assertContainersReused(ColumnarBatch first, ColumnarBatch 
second) {
+    assertThat(second.numCols()).isEqualTo(first.numCols());
+
+    for (int i = 0; i < first.numCols(); i++) {
+      assertThat(arrowVector(second, i)).isSameAs(arrowVector(first, i));
+    }
+  }
+
+  @Override
+  @ParameterizedTest
+  @FieldSource("FILE_FORMATS")
+  protected void testReadMetadataColumnIsDeleted(FileFormat fileFormat) throws 
IOException {
+    assumeThat(fileFormat)
+        .as("Vectorized Parquet reader only populates _deleted when a delete 
filter is set")
+        .isNotEqualTo(FileFormat.PARQUET);
+    super.testReadMetadataColumnIsDeleted(fileFormat);
+  }
+
+  @Override
+  @ParameterizedTest
+  @FieldSource("FILE_FORMATS")
+  protected void testReadMetadataColumnRowLineage(FileFormat fileFormat) 
throws IOException {
+    assumeRowLineageSupported(fileFormat);
+    super.testReadMetadataColumnRowLineage(fileFormat);
+  }
+
+  @Override
+  @ParameterizedTest
+  @FieldSource("FILE_FORMATS")
+  protected void testReadMetadataColumnRowLineageExistingValues(FileFormat 
fileFormat)
+      throws IOException {
+    assumeRowLineageSupported(fileFormat);
+    super.testReadMetadataColumnRowLineageExistingValues(fileFormat);
+  }
+
+  // TODO: remove when https://github.com/apache/iceberg/issues/18543 is fixed
+  private static void assumeRowLineageSupported(FileFormat fileFormat) {
+    assumeThat(fileFormat)
+        .as("Vectorized ORC reader does not apply row lineage")
+        .isNotEqualTo(FileFormat.ORC);
+  }
+
+  private ValueVector arrowVector(ColumnarBatch batch, int col) {
+    assertThat(batch.column(col)).isInstanceOf(IcebergArrowColumnVector.class);
+
+    IcebergArrowColumnVector vector = (IcebergArrowColumnVector) 
batch.column(col);
+    return vector.vectorAccessor().getVector();
+  }
+
+  @Override
+  protected void assertContainersNotReused(ColumnarBatch first, ColumnarBatch 
second) {
+    assertThat(second.numCols()).isEqualTo(first.numCols());
+
+    for (int i = 0; i < first.numCols(); i++) {
+      assertThat(arrowVector(second, i)).isNotSameAs(arrowVector(first, i));
+    }
+  }
+
+  @Override
+  protected Object engineSchema(Schema schema) {
+    return SparkSchemaUtil.convert(schema);
+  }
+
+  @Override
+  protected Object convertConstantToEngine(Type type, Object value) {
+    return SparkUtil.internalToSpark(type, value);
+  }
+
+  @Override
+  protected Class<ColumnarBatch> engineType() {
+    return ColumnarBatch.class;
+  }
+
+  @Override
+  protected ColumnarBatch convertToEngine(Record record, Schema schema) {
+    throw new UnsupportedOperationException("Vectorized reads are read-only");
+  }
+
+  @Override
+  protected void assertEquals(
+      Schema schema, List<ColumnarBatch> expected, List<ColumnarBatch> actual) 
{
+    throw new UnsupportedOperationException("Use assertRecordsEqual for batch 
reads");
+  }
+
+  @Override
+  protected void assertRecordsEqual(
+      Schema schema, List<Record> expected, CloseableIterable<ColumnarBatch> 
actual) {
+    int rowOffset = 0;
+
+    for (ColumnarBatch batch : actual) {
+      assertThat(rowOffset + 
batch.numRows()).isLessThanOrEqualTo(expected.size());
+
+      for (int i = 0; i < batch.numRows(); i++) {
+        TestHelpers.assertEquals(
+            schema,
+            InternalRowConverter.convert(schema, expected.get(rowOffset + i)),
+            batch.getRow(i));
+      }
+
+      rowOffset += batch.numRows();
+    }
+
+    assertThat(rowOffset).isEqualTo(expected.size());
+  }
+}

Reply via email to