feat(parquet): provide direct access to dictionary pages - #10420
Conversation
7c65729 to
8145bb8
Compare
|
Thanks @DarkWanderer -- I think it would help to have some sort of example here that shows the "usecase" of these APIs -- so either a test or something in parquet-examples that shows how these new APIs are meant to be used i see that apache/datafusion#23851 is sort of an example but I think we should distill the use down to something just arrow-rs specific so the API is clear Does that makes sense? |
09c54af to
4371070
Compare
Add sync and async APIs for decoding a BYTE_ARRAY column chunk dictionary page without materializing the full column, enabling exact row-group membership pruning for fully dictionary-encoded chunks. Reuse the regular page reader header, size validation, and decryption path so standalone dictionary decoding handles encrypted, truncated, and malformed pages safely. See apache#9010. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y1dzYmhkPzEC6H48UVneAJ
4371070 to
90eb024
Compare
Co-authored-by: Ed Seidl <etseidl@users.noreply.github.com>
|
Thank you for spotting @etseidl , applied the suggestion |
|
Thanks @DarkWanderer, and sorry this is taking so long. I hope to find time to review this Friday. |
# Conflicts: # parquet/src/arrow/async_reader/mod.rs # parquet/src/arrow/mod.rs
|
@etseidl let me know if I can provide additional info to help review |
etseidl
left a comment
There was a problem hiding this comment.
Thanks @DarkWanderer, sorry for the delay reviewing this. I think this looks good, mod a few last nits. It would be nice to eventually extend this to all data types (or at least physical types), but I suspect strings are the major use case for this.
ill just chip in and say i probably wont have time to review this 😅 so long as it looks good to you and the new public APIs are reasonable 👍 |
|
I do not have access to press "merge" here, in case this is implicitly expected. Let me know if I can clarify anything else or make further tweaks |
etseidl
left a comment
There was a problem hiding this comment.
Sorry for further delay @DarkWanderer, and thank you for your patience! Because I'm less familiar with the arrow bits and there isn't enough reviewer bandwidth, I had Codex look for some issues. Here's my translation of what Codex found, which I think are reasonable nits. There were other nits around correctness of the metadata, but that applies all over this crate so those can be deferred. I think if we address these, then I'll feel more confident merging this.
| parquet_meta_data: &ParquetMetaData, | ||
| row_group_idx: usize, | ||
| column_idx: usize, | ||
| ) -> Result<ArrayRef> { |
There was a problem hiding this comment.
This function will conditionally return binary or string arrays depending on the parquet logical type. If we want to later extend this to other physical types, we're now in the business of casting all of those to the appropriate arrow type, like we do in the array readers (e.g.
). This also doesn't handle decimals with a BYTE_ARRAY physical type.Maybe for now we can simply return a binary array, and leave it to the user to either cast to an appropriate arrow type, or instead cast probes to the physical type. Perhaps a later effort could add helpers for either approach (potentially reusing the current parquet->arrow conversion logic).
| let crypto_context = | ||
| CryptoContext::for_column(file_decryptor, crypto_metadata, row_group_idx, column_idx)? | ||
| .for_dictionary_page(); |
There was a problem hiding this comment.
This is using the passed in row_group_idx, which is an index into the current parquet_meta_data::row_groups. But the row groups may have been filtered at this point, so the 0th row group may have been the 2nd in the original file. I think it would be better to use parquet_meta_data.row_group(row_group_idx).ordinal() here, and error if there is no ordinal in the metadata (it should be present when modular encryption is used, for exactly this purpose).
There was a problem hiding this comment.
Done incl test
| let is_utf8 = is_utf8(column_descriptor); | ||
| let mut decoder = ByteArrayDecoderPlain::new(buf, num_values, Some(num_values), is_utf8); | ||
| let mut offsets = OffsetBuffer::<i32>::with_capacity(num_values); | ||
| decoder.read(&mut offsets, usize::MAX)?; |
There was a problem hiding this comment.
Apparently read here can return when the input buffer is exhausted, but it still returns to_read, rather than the actual number of values read. You might want to do something like
| decoder.read(&mut offsets, usize::MAX)?; | |
| decoder.read(&mut offsets, usize::MAX)?; | |
| if offsets.len() != num_values { | |
| return Err(general_err!( | |
| "did not read entire dictionary expected {num_values}, got {}", | |
| offsets.len() | |
| )); | |
| } |
| if header.r#type != PageType::DICTIONARY_PAGE { | ||
| return Err(ParquetError::General(format!( | ||
| "Expected a dictionary page, found {:?}", | ||
| header.r#type | ||
| ))); | ||
| } |
There was a problem hiding this comment.
We should also check header.dictionary_page_header.encoding to ensure it's PLAIN. I think there's been talk of using other encodings (although maybe just replacing RLE) so it would be nice to future-proof this.
etseidl
left a comment
There was a problem hiding this comment.
Thanks @DarkWanderer. I think this is ready now.
|
|
||
| #[cfg(feature = "encryption")] | ||
| #[test] | ||
| fn read_column_dictionary_uses_file_ordinal_after_filtering() { |
There was a problem hiding this comment.
❤️
Validated this fails when row_group_idx is used.
|
I'll add this makes #10963 more useful now. |
|
Thanks again @DarkWanderer. cc @ranflarion who might find this useful. |
|
EPIC |
apache/arrow-rs#10420 is merged to arrow-rs main as b9b1d5005; the [patch.crates-io] block (repointed during the upstream/main merge) now tracks that branch instead of the old DarkWanderer/arrow-rs fork. Cargo.lock is re-pinned to the merged commit for every patched arrow-*/parquet crate. Review changed the final API before merging, so adapt to it: - ParquetRecordBatchStreamBuilder::get_row_group_column_dictionary was renamed to get_column_chunk_dictionary. - Dictionary pages now always decode as Binary, never Utf8, so DictionaryStatistics::insert drops its Utf8/StringArray arm. Also: - Fix a tag collision in ParquetOptions' proto message: our dictionary_filter_on_read (38) collided with upstream's newly added max_in_list_size (also 38). Move ours to the next free tag, 39, and regenerate proto-common/proto-models. - Adapt dictionary_filter.rs's test helper to reader.rs's ParquetFileReader, which upstream refactored to build its ParquetObjectReader internally rather than taking one as a field. - Migrate two more PruningPredicate::try_new call sites (deprecated since 55.0.0) to PruningPredicateBuilder, needed for a clean `-D warnings` clippy run. - Fix a FixedSizeListArray::value_offset deprecation (arrow-rs 60.0.0) in unnest.rs by switching to value_offset_at. - Add the row_groups_pruned_dictionary / bytes_processed metrics to the .slt golden files upstream's merge didn't touch (DataSourceExec lines outside the merge's conflict hunks). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Which issue does this PR close?
Rationale for this change
This change provides low-level API necessary for enabling dictionary-based pruning in DataFusion: apache/datafusion#23851
What changes are included in this PR?
parquet::file::metadata::dictionary::decode_dictionary_page: decodesa BYTE_ARRAY dictionary page (Thrift header parse, decompress, PLAIN
decode) into a
Utf8/BinaryArrow array.ParquetMetaDataReader::read_column_dictionary(sync) andread_column_dictionary_async(async) to fetch and decode a given row group/column's dictionary page from aParquetMetaData, returningOk(None)if the chunk has no dictionary page.ParquetRecordBatchStreamBuilder::get_row_group_column_dictionaryconvenience method mirroringget_row_group_column_bloom_filter.Are these changes tested?
Yes: a round-trip unit test for a dictionary-encoded string column, a
non-BYTE_ARRAY rejection test, and sync + async reader tests that
decode a real dictionary page written through
ArrowWriter.Are there any user-facing changes?
Yes, three new public APIs (see above). No changes to
existing API.