Skip to content

Additional Unit Tests for Parquet VARIANT Field Extraction - #23036

Merged
rapids-bot[bot] merged 56 commits into
NVIDIA:mainfrom
abigalekim:ak/field-extraction-tests
Jul 28, 2026
Merged

Additional Unit Tests for Parquet VARIANT Field Extraction#23036
rapids-bot[bot] merged 56 commits into
NVIDIA:mainfrom
abigalekim:ak/field-extraction-tests

Conversation

@abigalekim

@abigalekim abigalekim commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Description

Adds edge-case and error-path tests for the experimental Parquet variant extraction API (cast_variant and get_variant_field). New tests cover unsupported cast types, zero-length and max-length short strings, truncated long-string payloads, and invalid input shapes (non-struct/non-list inputs, wrong child count, and mistyped child columns).

Checklist

  • I am familiar with the Contributing Guidelines.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

@abigalekim
abigalekim requested a review from a team as a code owner June 29, 2026 21:09
@abigalekim
abigalekim requested review from bdice and mattgara June 29, 2026 21:09
@copy-pr-bot

copy-pr-bot Bot commented Jun 29, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@abigalekim abigalekim self-assigned this Jun 29, 2026
@abigalekim
abigalekim marked this pull request as draft June 29, 2026 21:09
@github-actions github-actions Bot added the libcudf Affects libcudf (C++/CUDA) code. label Jun 29, 2026
@abigalekim abigalekim added tests Unit testing for project non-breaking Non-breaking change improvement Improvement / enhancement to an existing function and removed libcudf Affects libcudf (C++/CUDA) code. labels Jun 29, 2026
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds shared VARIANT metadata enums, updates extraction decoding to use them, and expands tests for malformed data, casting matrices, string boundaries, and invalid input shapes.

Changes

VARIANT contract and extraction validation

Layer / File(s) Summary
VARIANT type contract
cpp/include/cudf/io/experimental/variant_spec.hpp
Adds public enums for VARIANT basic categories and primitive physical type identifiers.
Shared decoding type integration
cpp/src/io/parquet/experimental/variant_extract.cu
Uses the shared VARIANT enums and metadata decoder throughout object, integer, string, and value-length decoding.
VARIANT encoding and extraction validation
cpp/tests/io/experimental/variant_extract_test.cpp
Adds structured encoding helpers, refactors object builders, and covers malformed metadata and value buffers.
Casting and input-shape validation
cpp/tests/io/experimental/variant_extract_test.cpp
Covers unsupported targets, source-target matrices, string boundaries, malformed lengths, and invalid VARIANT column shapes.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • rapidsai/cudf#23069: Adds Java/JNI wrappers that call the same experimental VARIANT extraction and casting APIs.

Suggested reviewers: gregorykimball, vuule

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: added unit tests for Parquet VARIANT field extraction.
Description check ✅ Passed The description is directly related to the added edge-case and error-path tests in the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@abigalekim
abigalekim marked this pull request as ready for review June 30, 2026 00:25
@abigalekim
abigalekim requested a review from vuule June 30, 2026 00:25
@github-actions github-actions Bot added the libcudf Affects libcudf (C++/CUDA) code. label Jun 30, 2026
@mhaseeb123

Copy link
Copy Markdown
Contributor

/ok to test 693c014

@abigalekim
abigalekim requested a review from mhaseeb123 June 30, 2026 21:52

@bdice bdice left a comment

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 know very little about Parquet variants. Can you help explain these a bit more?

TEST_F(CastVariantTest, UnsupportedCastTypeThrows)
{
auto stream = cudf::test::get_default_stream();
std::vector<uint8_t> const val{0x00}; // null primitive — valid list<uint8> input

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 might be missing context. I don't really know what this means. Is this 0 a value in the Parquet specification that represents null? Is this representing a type id, or a value?

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.

Is uint8_t a type declared in the Parquet variant spec?

@mhaseeb123 mhaseeb123 Jul 1, 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.

Hi @bdice these tests are targeting casting from sample raw variant bytes (like the val vector here and other tests). We extract these bytes out from variant rows as col<list<uint8_t>> (one list per extracted field per variant row)

Variant looks like this:

First byte -> header

bits 0-1: basic_type   (0=primitive, 1=short_string, 2=object, 3=array)
bits 2-7: value_header (6 bits, meaning depends on basic_type)

For basic_type::primitive (0), the 6-bit value_header is a physical type id (0=null, 1/2=bool true/false, 3-6=int8/16/32/64, 7=float64, 16=long_string, etc. from the enum). 
For basic_type::short_string (1), the 6-bit value_header is the string length (0-63 bytes) and the string bytes follow immediately.

In this test, there's just one byte 0x0 saying primitive type but no other info on what type and what data (so it throws). Please correct me if I am wrong @abigalekim

Similarly in the next test, we have 0x1 (indicating short string) but nothing really after so it should decode to a valid but empty ("") value.

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.

Here's a summary of what's going on in each test that I got from AI:

UnsupportedCastTypeThrows — uses val = {0x00}. Byte 0x00 = basic_type 00 (primitive) + value_header 000000 (primitive_type::null), i.e. a Variant "null" value. This is fine as input, but it's actually a bit of a red herring: cast_variant's type-check happens entirely at compile-time dispatch (cast_variant_fn::operator() with requires(not is_variant_castable<T>) → CUDF_FAIL), based purely on the desired output type (FLOAT64/BOOL8), not on the byte content. Any well-formed list<uint8> row would trigger the same throw — this matches exactly what bdice asked about in review ("Is this 0 a value...type id, or a value?").

ShortStringLengthZero — val = {0x01}. 0x01 = basic_type=01 (short_string), value_header = 0 (length 0). Per decode_string, with btype == short_string, str_len = value_header = 0, so it slices 0 bytes after the header → empty string. This exercises the lower boundary of the 6-bit length field.

ShortStringLengthSixtyThree — header 0xFD = binary 11111101: bits 0-1 = 01 (short_string), bits 2-7 = 111111 = 63 (the max value a 6-bit field can hold, i.e. the largest string that can use the short-string encoding before Parquet must switch to long_string). Followed by 63 'z' payload bytes. This is the correctness boundary check corresponding to 0x01 | (63 << 2) = 0xFD in the test comment.

LongStringLengthZero — val = {0x40, 0x00, 0x00, 0x00, 0x00}. 0x40 = binary 01000000: bits 0-1 = 00 (primitive), bits 2-7 = 010000 = 16 = primitive_type::long_string. Per the spec, a long_string primitive is followed by a 4-byte little-endian length, then that many payload bytes — here the length is 0x00000000 = 0, so no payload follows.

LongStringTruncatedPayloadYieldsNull — val = {0x40, 0x0A, 0x00, 0x00, 0x00, 'a','b','c'}: same long_string header, but the LE length field claims 0x0000000A = 10 bytes of payload while only 3 ("abc") are actually present. This targets the truncation guard in decode_string:

@abigalekim can you please double check this and perhaps add some info from here to the tests so it's easy to review 😄. Perhaps we can add a comment/link to the schema so we can refer to it while reviewing.

@bdice bdice Jul 1, 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.

Should we have something like a factory that builds a byte from named enums?

Like replace 0x06 with make_variant_primitive(variant_type_id::INT64)?

Then it’s self-explanatory at the call site.

@bdice bdice Jul 1, 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.

Also having a factory would help resolve any ambiguity between big-endian and little-endian encodings. (Are bits 0-1 low or high?)

@mhaseeb123 mhaseeb123 Jul 2, 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.

I think this is a great idea to have a reusable factory + provides context for (current and future) variant tests.

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.

Hi, I added small comments describing what each test is, and implemented a reusable factory for writing tests! I think the AI descriptions seem correct.


TEST_F(CastVariantTest, ShortStringLengthZero)
{
// Short string with length 0: header = 0x01 | (0 << 2) = 0x01, no payload.

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.

Where do the values 0x01 | (0 << 2) come from?

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.

This test now uses the factory, but it was the manual construction of the VARIANT short-string value-header byte.

Comment thread cpp/tests/io/experimental/variant_extract_test.cpp Outdated
Comment thread cpp/tests/io/experimental/variant_extract_test.cpp Outdated
Comment thread cpp/tests/io/experimental/variant_extract_test.cpp Outdated
Comment thread cpp/tests/io/experimental/variant_extract_test.cpp Outdated
Comment thread cpp/tests/io/experimental/variant_extract_test.cpp Outdated
Comment thread cpp/tests/io/experimental/variant_extract_test.cpp Outdated
}
}

TEST_F(CastVariantTest, UnsupportedCastTypeThrows)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

There appears to be a real gap in testing (and likely requiring an implementation change) where cast_variant bypasses the validation for empty inputs.

The implementation returns make_empty_column(desired_type) before it reaches the unsupported-type dispatcher, so cast_variant(empty_list_uint8, FLOAT64) can succeed even though unsupported desired_type values are documented to throw.

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 think this should be its own PR. @vuule thoughts?

@mhaseeb123 mhaseeb123 Jul 7, 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.

@vuule is OOTO so let's not ping him here 🙂. @abigalekim if you would like to handle this in a separate PR, please open an issue (perhaps with a proposed test/repro) against this comment so it could be tracked

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.

missed this until now. Yeah, separate PR sounds good. Keep them focused so they move faster.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@abigalekim Please create an issue for this so we don't loose track.

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 made an issue for this problem #23361!

@abigalekim
abigalekim requested a review from bdice July 7, 2026 01:58
@vuule vuule added the 5 - Ready to Merge Testing and reviews complete, ready to merge label Jul 24, 2026
@mhaseeb123 mhaseeb123 moved this from Burndown to Slip in libcudf Jul 24, 2026
@mhaseeb123

Copy link
Copy Markdown
Contributor

/merge

@abigalekim

Copy link
Copy Markdown
Contributor Author

/ok to test 01e5942

The merge of main brought in array-element and floating-point extraction
code that still used the pre-rename lowercase enumerators and the
variant_basic_type() helper, which this branch had replaced with the
shared uppercase enums in variant_spec.hpp and decode_basic_type().
@vuule

vuule commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

/ok to test 8ed7f3b

@abigalekim

Copy link
Copy Markdown
Contributor Author

/ok to test e41f893

@abigalekim

Copy link
Copy Markdown
Contributor Author

/ok to test 03194b0

@rapids-bot
rapids-bot Bot merged commit 9708dd0 into NVIDIA:main Jul 28, 2026
136 checks passed
@abigalekim
abigalekim deleted the ak/field-extraction-tests branch July 28, 2026 22:05
@GregoryKimball GregoryKimball moved this from Slip to Landed in libcudf Aug 20, 2026
@GregoryKimball GregoryKimball removed this from libcudf Aug 21, 2026
@GregoryKimball GregoryKimball moved this from Slip to Landed in libcudf Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

5 - Ready to Merge Testing and reviews complete, ready to merge improvement Improvement / enhancement to an existing function libcudf Affects libcudf (C++/CUDA) code. non-breaking Non-breaking change tests Unit testing for project

Projects

Status: Landed

Development

Successfully merging this pull request may close these issues.

8 participants