Skip to content

Data, Spark 4.2: Add format model TCK coverage for vectorized reads - #17610

Open
joyhaldar wants to merge 9 commits into
apache:mainfrom
joyhaldar:spark-vectorized-formatmodel-tck
Open

joyhaldar wants to merge 9 commits into
apache:mainfrom
joyhaldar:spark-vectorized-formatmodel-tck

Conversation

@joyhaldar

@joyhaldar joyhaldar commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Adds a TCK subclass for Spark's vectorized ColumnarBatch read path.

TestSparkVectorizedFormatModel extends ReadFormatModelTests and redefines FILE_FORMATS and FORMAT_AND_GENERATOR to skip Avro, which has no ColumnarBatch model registered. This required making DataGenerator, DataGenerators and DataGenerators.ALL public in the test package.

Two base class changes were needed:

  • assertRecordsEqual() - read tests built expectations via convertToEngineRecords, which can't work for a batch type
  • Read assertions moved inside try-with-resources, columnar batches don't survive the reader closing

@joyhaldar joyhaldar changed the title [WIP] Data, Spark: Add format model TCK coverage for vectorized reads Data, Spark: Add format model TCK coverage for vectorized reads (WIP) Aug 11, 2026
return false;
}

protected Set<FileFormat> supportedFormats() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like this one.

Could we just re-define FILE_FORMATS, and FORMAT_AND_GENERATOR in TestSparkVectorizedFormatModel?

  private static final FileFormat[] FILE_FORMATS = {FileFormat.ORC, FileFormat.PARQUET};
  private static final List<Arguments> FORMAT_AND_GENERATOR

This relies on the fact, how junit works internally, but no change is required in all of the tests.

I'm open for other ideas as well, this is just the first one occured to me

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the suggestion @pvary I tried it out and here's what I found:

Redefining FILE_FORMATS in the subclass works as you described, those tests drop AVRO with no base class changes.

Redefining FORMAT_AND_GENERATOR doesn't work as is, because it references DataGenerators, which is package-private in org.apache.iceberg.data while TestSparkVectorizedFormatModel is in org.apache.iceberg.spark.data.

I tried making DataGenerators, DataGenerators.ALL and the DataGenerator interface public, and with that both fields can be redefined in the subclass. All AVRO cases are gone and I could remove supportedFormats() and the per test assumptions entirely. TestSparkFormatModel and TestFlinkFormatModel still pass.

Would you like me to go with that, or would you rather avoid widening the visibility of those test classes?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with making the DataGenerator and related stuff public in the test packages

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @pvary.

  1. Made DataGenerator, DataGenerators and DataGenerators.ALL public in the test package.
  2. TestSparkVectorizedFormatModel redefines both FILE_FORMATS and FORMAT_AND_GENERATOR.
  3. Dropped supportedFormats() and all the per test assumeSupportedFormat() calls from the base class.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I hit while doing this. When the subclass redefines FORMAT_AND_GENERATOR, it also has to repeat every filter the base applies. #17617 added supportsGenerator yesterday, my subclass didn't have it, and ORC + Variant came back and failed CI.

To fix it I'd have to either make supportsGenerator public, or copy the check into the subclass. Neither seems clean, and any new filter will have the same problem IIUC.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

protected is enough

return false;
}

protected boolean readOnly() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we split the base test class to read-tests, and read+write-tests?
Using this method in all test seem awkward a bit.

If we decide so, we might want to do it in a different PR

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you, would love your advice.

Two ways we could do it:

  1. Keep the read tests and shared helpers in BaseFormatModelTests, move the write tests into a BaseReadWriteFormatModelTests that extends it. Existing tests move to the new class.
  2. Insert a BaseReadFormatModelTests above the current BaseFormatModelTests and put the read tests there. No existing subclass changes its extends.

And TestSparkVectorizedFormatModel would only extend the read-only class.

Would you like to do it in a follow-up PR or this PR?

@pvary pvary Aug 17, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please separate out the test to:

  • ReadFormatModelTests as a base, and move the read tests there
  • BaseFormatModelTests inheriting from ReadFormatModelTests

I would do this way

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be in a PR before this change

@joyhaldar
joyhaldar force-pushed the spark-vectorized-formatmodel-tck branch 3 times, most recently from 159b391 to 6b66972 Compare August 17, 2026 10:12
import org.apache.spark.sql.vectorized.ColumnarBatch;
import org.junit.jupiter.params.provider.Arguments;

public class TestSparkVectorizedFormatModel extends BaseFormatModelTests<ColumnarBatch> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

package private

@joyhaldar
joyhaldar force-pushed the spark-vectorized-formatmodel-tck branch from 6b66972 to 50d5209 Compare August 17, 2026 10:20
@joyhaldar
joyhaldar force-pushed the spark-vectorized-formatmodel-tck branch from 50d5209 to 625fe88 Compare August 27, 2026 18:28
@joyhaldar joyhaldar changed the title Data, Spark: Add format model TCK coverage for vectorized reads (WIP) Data, Spark: Add format model TCK coverage for vectorized reads Aug 28, 2026
@joyhaldar
joyhaldar marked this pull request as ready for review August 28, 2026 05:47
@joyhaldar joyhaldar changed the title Data, Spark: Add format model TCK coverage for vectorized reads Data, Spark 4.1: Add format model TCK coverage for vectorized reads Aug 28, 2026
@ParameterizedTest
@FieldSource("FILE_FORMATS")
void testNestedDefaultValue(FileFormat fileFormat) throws IOException {
assumeFalse(supportsBatchReads(), "Vectorized reads do not support nested types");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we handle this with unsupportedTypeIds?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added assumeSupportedTypes() to skip tests when the schema contains a type listed in unsupportedTypeIds().

@FieldSource("FILE_FORMATS")
void testReaderBuilderSplit(FileFormat fileFormat) throws IOException {

assumeFalse(supportsBatchReads(), "Batch reads return batches, not rows");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to override this test, or can we test differently?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a countRows() hook that sums the row counts across ColumnarBatch instances and uses .size() for row readers. This allows the shared tests to work for both.

@FieldSource("FILE_FORMATS")
void testReaderBuilderReuseContainers(FileFormat fileFormat) throws IOException {

assumeFalse(supportsBatchReads(), "Batch reads reuse differently");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Override the test?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither vectorized reader returns the same object twice. ColumnarBatchReader.read always builds a new ColumnarBatch and uses reuse only to decide whether to close the previous vectors IIUC:

if (reuse == null) {
  closeVectors();
}
return new ColumnBatchLoader(numRowsToRead).loadDataToColumnBatch();

VectorizedSparkOrcReaders.read doesn't take a reuse argument at all.

I checked with and without reuseContainers() on both formats and each call returns a new object either way. Is that expected for batch reads? Please correct me if I am wrong.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the underlying data structures? I expect that the Arrow Vlvectors should be reused.

@joyhaldar joyhaldar Oct 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reuse logic is in VectorizedArrowReader.java.

https://github.com/apache/iceberg/blob/main/arrow/src/main/java/org/apache/iceberg/arrow/vectorized/VectorizedArrowReader.java#L140-L156

In ColumnarBatchReader.java, the previous VectorHolder is passed back to the reader, allowing the underlying Arrow vector to be reused.

vectorHolders[i] = readers[i].read(vectorHolders[i], batchSize);

I am getting each vector in a batch like this,

private ValueVector arrowVector(ColumnarBatch batch, int col) {
    IcebergArrowColumnVector vector = (IcebergArrowColumnVector) batch.column(col);
    return vector.vectorAccessor().getVector();
  }

I have updated the shared test to compare the underlying Arrow vectors for every column, both with and without reuseContainers().

@FieldSource("FILE_FORMATS")
void testReadMetadataColumnIsDeleted(FileFormat fileFormat) throws IOException {

assumeFalse(supportsBatchReads(), "DeletedColumnVector requires a delete filter");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please provide a better message. DeletedColumnVector is Spark specific.
What is the exact issue here? The batch readers doesn't read the DVs either, so checking false seems ok.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't return false, it throws:

TestSparkVectorizedFormatModel > testReadMetadataColumnIsDeleted(FileFormat) > [2] PARQUET FAILED
    java.lang.NullPointerException: Cannot load from byte/boolean array because "this.isDeleted" is null
        at org.apache.iceberg.spark.data.vectorized.DeletedColumnVector.getBoolean(DeletedColumnVector.java:61)
        at org.apache.spark.sql.vectorized.ColumnarBatchRow.getBoolean(ColumnarBatchRow.java:108)
        at org.apache.spark.sql.vectorized.ColumnarBatchRow.get(ColumnarBatchRow.java:181)
        at org.apache.iceberg.spark.data.TestHelpers.assertEquals(TestHelpers.java:772)
        at org.apache.iceberg.spark.data.TestHelpers.assertEquals(TestHelpers.java:734)
        at org.apache.iceberg.spark.data.TestHelpers.assertEquals(TestHelpers.java:719)
        at org.apache.iceberg.spark.data.TestSparkVectorizedFormatModel.assertRecordsEqual(TestSparkVectorizedFormatModel.java:102)

The array is only filled when a delete filter is set IIUC. ORC passes, so this is Parquet only if I am not wrong. I will fix the message. Should I file an issue for this, or send a fix PR? Please correct me if I am wrong.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the array is only created by the delete filter.
Let me think what could we do here.

@pvary pvary Aug 28, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking a look at supportsBatchReads - it always return false.

I think we should override this method.

Also we should update testReaderBuilderRecordsPerBatchNotSupported to have a batch and a non-bach version. Maybe with override and rename to testReaderBuilderRecordsPerBatch

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the skip message to explain the issue and limited the skip to vectorized Parquet. ORC still runs the test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated testReaderBuilderRecordsPerBatch to test both row and batch readers. Row readers check that recordsPerBatch() throws an exception, while batch readers check the batch size and total row count.

@FieldSource("FILE_FORMATS")
void testReadMetadataColumnRowLineage(FileFormat fileFormat) throws IOException {

// TODO: ORC's vectorized reader treats _row_id as a plain constant

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shorten and leave an issue id in the text

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have left issue ID #18543

@ParameterizedTest
@FieldSource("FILE_FORMATS")
void testSchemaEvolutionEmptyProjection(FileFormat fileFormat) throws IOException {
assumeFalse(supportsBatchReads(), "Row count assertion does not apply to batches");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a countRows() hook that sums the row counts across ColumnarBatch instances and uses .size() for row readers. This allows the shared tests to work for both.

@github-actions

Copy link
Copy Markdown

This pull request has been marked as stale due to 30 days of inactivity. It will be closed in 1 week if no further activity occurs. If you think that’s incorrect or this pull request requires a review, please simply write any comment. If closed, you can revive the PR at any time and @mention a reviewer or discuss it on the dev@iceberg.apache.org list. Thank you for your contributions.

@github-actions github-actions Bot added the stale label Sep 29, 2026
@pvary

pvary commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

@joyhaldar: Are you planning to work on this? Shall we take over? Maybe @Guosmilesmile could help, if you want.

@joyhaldar

Copy link
Copy Markdown
Contributor Author

@joyhaldar: Are you planning to work on this? Shall we take over? Maybe @Guosmilesmile could help, if you want.

Sorry for the delay, I was still planning to work on this, but I've been consumed by other priorities. I would still require some time to get back to this. I would not mind if you'd like to take it over, but I am planning to make time in the coming weeks.

@github-actions github-actions Bot removed the stale label Sep 30, 2026
@rshdhere

Copy link
Copy Markdown
Contributor

@joyhaldar: Are you planning to work on this? Shall we take over? Maybe @Guosmilesmile could help, if you want.

Sorry for the delay, I was still planning to work on this, but I've been consumed by other priorities. I would still require some time to get back to this. I would not mind if you'd like to take it over, but I am planning to make time in the coming weeks.

Hi @joyhaldar, if you're still short on time I'd be happy to pick this up. I'd keep your commits and address @pvary's comments (splitting testReaderBuilderRecordsPerBatchNotSupported into batch and non-batch versions, and checking reuse of the underlying Arrow vectors). If you'd rather finish it yourself, no problem at all, I'll leave it with you.

Co-authored-by: Joy Haldar <joy.haldar@target.com>
@joyhaldar

Copy link
Copy Markdown
Contributor Author

@joyhaldar: Are you planning to work on this? Shall we take over? Maybe @Guosmilesmile could help, if you want.

Sorry for the delay, I was still planning to work on this, but I've been consumed by other priorities. I would still require some time to get back to this. I would not mind if you'd like to take it over, but I am planning to make time in the coming weeks.

Hi @joyhaldar, if you're still short on time I'd be happy to pick this up. I'd keep your commits and address @pvary's comments (splitting testReaderBuilderRecordsPerBatchNotSupported into batch and non-batch versions, and checking reuse of the underlying Arrow vectors). If you'd rather finish it yourself, no problem at all, I'll leave it with you.

Hello, sorry for the delay, I am working this over the weekend.

…o 4.2

Co-authored-by: Joy Haldar <joy.haldar@target.com>
joyhaldar and others added 2 commits October 16, 2026 22:43
… for batch readers

Co-authored-by: Joy Haldar <joy.haldar@target.com>
Co-authored-by: Joy Haldar <joy.haldar@target.com>
joyhaldar and others added 2 commits October 16, 2026 23:30
Co-authored-by: Joy Haldar <joy.haldar@target.com>
Co-authored-by: Joy Haldar <joy.haldar@target.com>
@joyhaldar
joyhaldar force-pushed the spark-vectorized-formatmodel-tck branch from 625fe88 to 556ce35 Compare October 16, 2026 18:16
@joyhaldar joyhaldar changed the title Data, Spark 4.1: Add format model TCK coverage for vectorized reads Data, Spark 4.2: Add format model TCK coverage for vectorized reads Oct 16, 2026
joyhaldar and others added 3 commits October 18, 2026 09:21
Co-authored-by: Joy Haldar <joy.haldar@target.com>
Co-authored-by: Joy Haldar <joy.haldar@target.com>
Co-authored-by: Joy Haldar <joy.haldar@target.com>
readRecords = ImmutableList.copyOf(reader);
}

assertThat(readRecords).hasSizeGreaterThan(0).hasSizeLessThan(genericRecords.size());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check this size checks. Are they still valid for the batches?

}

private ValueVector arrowVector(ColumnarBatch batch, int col) {
IcebergArrowColumnVector vector = (IcebergArrowColumnVector) batch.column(col);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure about this? At least an instanceof please

}

protected void assumeSupportedTypes(Schema schema) {
Collection<Type.TypeID> unsupportedTypeIds = unsupportedTypeIds();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAybe update the javadoc for unsupportedTypeIds?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants