Replace Apache Thrift by cudf APIs for Parquet footer reading/writing - #4851
Replace Apache Thrift by cudf APIs for Parquet footer reading/writing#4851ttnghia wants to merge 2 commits into
Conversation
cudf-spark-jni linked `libparquet` and `libthrift` solely so `NativeParquetJni` could deserialize, prune, and re-serialize a Parquet footer via the Arrow `parquet::format` types and the Thrift `TCompactProtocol`; the bundled Thrift is a standing build fragility through its archive-mirror source fetch. Rewrite `NativeParquetJni` on the new public cudf facade `cudf::io::read_parquet_footer_bytes` and `write_parquet_footer_bytes` (a host-only Thrift-compact codec, no libthrift), calling the reader in `throw_if_type_mismatch::NO` mode so a field whose wire type cudf does not model is skipped rather than rejected, matching the forward compatibility of the Arrow Thrift reader it replaces. Port the `__isset` presence checks to `std::optional` and the `meta_data`-absent rows to a `data_page_offset == 0` heuristic; drop the `PARQUET_LIB` and `THRIFT_LIB` `find_library` and link entries from the JNI `CMakeLists.txt` while keeping `ARROW_LIB` for the whole-archived Arrow-IPC symbols. The cudfjni build needs no Arrow-Parquet flag: the facade commit makes cudf-java default `CUDF_ENABLE_ARROW_PARQUET` OFF, so `libparquet` and the bundled Apache Thrift fall away on their own. Expand `ParquetFooterTest` with coverage for the rewritten footer path: nested-schema column pruning, the `parquet-mr` re-serialization round-trip, the `meta_data`-absent (PARQUET-2078) fallback, the `std::optional`-ported fields (absent `converted_type`, `file_offset`, `total_compressed_size`, and `column_orders`), and the trailing-length frame the spark-rapids caller appends after the footer. --------- Signed-off-by: Nghia Truong <nghiat@nvidia.com>
There was a problem hiding this comment.
Pull request overview
This PR rewrites the Parquet footer JNI path to use cuDF’s public Parquet metadata read/write APIs instead of Apache Thrift/parquet-mr C++ plumbing, removing the libthrift/libparquet static-link dependencies from the native build while preserving footer filtering and column-pruning behavior.
Changes:
- Switch
NativeParquetJnifooter parsing/serialization tocudf::io::{read,write}_parquet_footer_bytesand update pruning logic to cuDF’s Parquet metadata structs. - Remove
libparquet/libthriftdiscovery, include paths, and linkage from the native CMake build. - Expand
ParquetFooterTestcoverage to exercise nested-schema pruning, PARQUET-2078 metadata absence handling, column_orders reindexing, corrupt footer exceptions, and tolerance to trailing footer-length bytes.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/test/java/com/nvidia/spark/rapids/jni/ParquetFooterTest.java | Adds targeted regression tests for nested pruning, footer round-trip via parquet-mr, and defensive parsing behaviors needed by the new cuDF-based implementation. |
| src/main/cpp/src/NativeParquetJni.cpp | Replaces Thrift-based footer decode/encode with cuDF APIs and adapts row-group filtering + column pruning to cuDF metadata types. |
| src/main/cpp/CMakeLists.txt | Drops parquet/thrift static library dependencies and related include paths to match the new implementation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| #include <cwctype> | ||
| #include <limits> | ||
| #include <sstream> | ||
| #include <stdexcept> |
| JNI_TRY | ||
| { | ||
| auto meta = std::make_unique<parquet::format::FileMetaData>(); | ||
| uint32_t len = static_cast<uint32_t>(buffer_length); |
Greptile SummaryThis PR rewrites
Confidence Score: 5/5Safe to merge — the rewrite is a clean mechanical substitution of Thrift internals with cudf public APIs; integration tests passed against Spark 3.5.5 and the new unit-test suite covers all the edge-case paths introduced by the migration. Every __isset guard has a clear value-based equivalent with an explanatory comment, the build change is purely subtractive, and the ten new tests exercise the specific paths (absent metadata, fallback offsets, schema pruning) that are most sensitive to the API swap. No logic regressions were found. Files Needing Attention: No files require special attention. The one suggestion (widening buf_size to std::size_t) is cosmetic hardening on a path that cannot realistically be triggered. Important Files Changed
Sequence DiagramsequenceDiagram
participant Java as Java ParquetFooter
participant JNI as NativeParquetJni C++
participant cudf as cudf::io
Java->>JNI: readAndFilter(buffer, partOffset, partLength, schema)
JNI->>cudf: read_parquet_footer_bytes(span, throw_if_type_mismatch::NO)
cudf-->>JNI: FileMetaData
JNI->>JNI: column_pruner::filter_schema builds schema_map and chunk_map
JNI->>JNI: filter_groups applies midpoint test per row group
Note right of JNI: Absent meta_data falls back to RowGroup.file_offset
JNI->>JNI: filter_columns reindexes column chunks
JNI-->>Java: handle parquet_footer_with_row_group_offsets
Java->>JNI: serializeThriftFile(handle)
JNI->>cudf: write_parquet_footer_bytes(FileMetaData)
cudf-->>JNI: vector of uint8_t
JNI->>JNI: Wrap with PAR1 magic + LE footer length + PAR1
JNI-->>Java: HostMemoryBuffer
Reviews (2): Last reviewed commit: "[Fix] Guard footer buffer length and doc..." | Re-trigger Greptile |
| List<SchemaElement> schema = new ArrayList<>(); | ||
| schema.add(root); | ||
| schema.add(leaf("id", FieldRepetitionType.OPTIONAL)); | ||
| schema.add(group("names", 1, FieldRepetitionType.OPTIONAL, ConvertedType.LIST)); | ||
| schema.add(group("list", 1, FieldRepetitionType.REPEATED, null)); | ||
| schema.add(leaf("element", FieldRepetitionType.OPTIONAL)); | ||
| schema.add(group("props", 1, FieldRepetitionType.OPTIONAL, ConvertedType.MAP)); | ||
| schema.add(group("key_value", 2, FieldRepetitionType.REPEATED, null)); | ||
| schema.add(leaf("key", FieldRepetitionType.REQUIRED)); | ||
| schema.add(leaf("value", FieldRepetitionType.OPTIONAL)); | ||
| schema.add(group("nested", 2, FieldRepetitionType.OPTIONAL, null)); | ||
| schema.add(leaf("x", FieldRepetitionType.OPTIONAL)); | ||
| schema.add(leaf("y", FieldRepetitionType.OPTIONAL)); |
There was a problem hiding this comment.
| List<SchemaElement> schema = new ArrayList<>(); | |
| schema.add(root); | |
| schema.add(leaf("id", FieldRepetitionType.OPTIONAL)); | |
| schema.add(group("names", 1, FieldRepetitionType.OPTIONAL, ConvertedType.LIST)); | |
| schema.add(group("list", 1, FieldRepetitionType.REPEATED, null)); | |
| schema.add(leaf("element", FieldRepetitionType.OPTIONAL)); | |
| schema.add(group("props", 1, FieldRepetitionType.OPTIONAL, ConvertedType.MAP)); | |
| schema.add(group("key_value", 2, FieldRepetitionType.REPEATED, null)); | |
| schema.add(leaf("key", FieldRepetitionType.REQUIRED)); | |
| schema.add(leaf("value", FieldRepetitionType.OPTIONAL)); | |
| schema.add(group("nested", 2, FieldRepetitionType.OPTIONAL, null)); | |
| schema.add(leaf("x", FieldRepetitionType.OPTIONAL)); | |
| schema.add(leaf("y", FieldRepetitionType.OPTIONAL)); | |
| List<SchemaElement> schema = Arrays.asList( | |
| root, | |
| leaf("id", FieldRepetitionType.OPTIONAL), | |
| group("names", 1, FieldRepetitionType.OPTIONAL, ConvertedType.LIST), | |
| group("list", 1, FieldRepetitionType.REPEATED, null), | |
| leaf("element", FieldRepetitionType.OPTIONAL), | |
| group("props", 1, FieldRepetitionType.OPTIONAL, ConvertedType.MAP), | |
| group("key_value", 2, FieldRepetitionType.REPEATED, null), | |
| leaf("key", FieldRepetitionType.REQUIRED), | |
| leaf("value", FieldRepetitionType.OPTIONAL), | |
| group("nested", 2, FieldRepetitionType.OPTIONAL, null), | |
| leaf("x", FieldRepetitionType.OPTIONAL), | |
| leaf("y", FieldRepetitionType.OPTIONAL)); |
| // Parse leniently (throw_if_type_mismatch::NO): skip a known field whose wire type does not | ||
| // match cudf's schema (Thrift forward-compat) rather than rejecting real files that use it. |
There was a problem hiding this comment.
Is this tested? Can it be?
There was a problem hiding this comment.
Yes it can be tested, but in cudf PR instead.
| rapids::jni::deserialize_parquet_footer(reinterpret_cast<uint8_t*>(buffer), len, meta.get()); | ||
| // Parse leniently (throw_if_type_mismatch::NO): skip a known field whose wire type does not | ||
| // match cudf's schema (Thrift forward-compat) rather than rejecting real files that use it. | ||
| auto meta = std::make_unique<rapids::jni::pq::FileMetaData>(cudf::io::read_parquet_footer_bytes( |
There was a problem hiding this comment.
I assume this depends on rapidsai/cudf#23345 and rapidsai/cudf#23346 — don't forget to pick those up into the submodule once they're merged.
There was a problem hiding this comment.
Sure, this PR explicitly depends on them.
Reject a negative `buffer_length` and stop truncating it to `uint32_t`: cast the `jlong` to `std::size_t` (lossless for the `host_span` the footer facade consumes) and throw `std::invalid_argument` on a negative length, caught by the surrounding `JNI_TRY`/`JNI_CATCH` and surfaced to Java. Document the zero-default assumptions the Thrift-to-cudf port relies on: an absent `repetition_type` defaults to `REQUIRED` (so the bare `!= REPEATED` check matches the old `__isset` guard), an absent `total_compressed_size` reads back as 0 via `value_or(0)` (matching the legacy Thrift zero-init), and `dictionary_page_offset == 0` means unset because offset 0 holds the `PAR1` magic. Include `<cstring>` directly for `std::memcpy`, and accumulate `num_to_skip` with `+=`. --------- Signed-off-by: Nghia Truong <nghiat@nvidia.com>
igorpeshansky
left a comment
There was a problem hiding this comment.
LGTM
modulo a minor test cleanup.
Rewrite
NativeParquetJnito use cudf public APIs for Parquet footer reading/writing instead of using Apache Thrift. This effectively dropslibthriftandlibparquetbuild dependency completely.Depends on:
varintdecoding rapidsai/cudf#23346This PR and its dependencies have been tested and validated against cudf-spark plugin integration tests (Spark 3.5.5):