Skip to content

feat(parquet): Consolidate bloom filter reads with a push decoder - #11975

Open
etseidl wants to merge 7 commits into
apache:mainfrom
etseidl:consolidate_bloom_filter_reads
Open

etseidl wants to merge 7 commits into
apache:mainfrom
etseidl:consolidate_bloom_filter_reads

Conversation

@etseidl

@etseidl etseidl commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

Reduce code duplication and centralize Bloom filter parsing and validation using a push decoder.

What changes are included in this PR?

  • Add SbbfPushDecoder to decode Bloom filters from caller-provided byte ranges without I/O.
  • Share decoding between synchronous and asynchronous reads, including Arrow builder APIs.
  • Apply the same header and bitset validation to Sbbf::from_bytes.
  • Consolidate parsing tests around the push decoder, retaining sync/async I/O tests..

Are these changes tested?

Yes, new tests are added

Are there any significant user-facing changes?

Adds the public SbbfPushDecoder API. Existing read APIs are unchanged, but synchronous reads and Sbbf::from_bytes now reject invalid bitset sizes and inconsistent declared lengths, matching the asynchronous behavior introduced in #11953.

AI disclosure

Implementation and regression tests were prepared with coding-agent assistance, but were reviewed by me.

@etseidl

etseidl commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Note this is stacked on #11953.

@etseidl
etseidl force-pushed the consolidate_bloom_filter_reads branch from e86ec10 to 8a73701 Compare November 2, 2026 07:05
@etseidl etseidl changed the title feat(parquet): Consolidate bloom filter reads feat(parquet): Consolidate bloom filter reads with a push decoder Nov 2, 2026
@etseidl
etseidl marked this pull request as ready for review November 2, 2026 15:30
@etseidl

etseidl commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

@alamb I think this is ready...added the push API you requested.

cc @adriangb who also has Sbbf changes in flight.

@alamb

alamb commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

👀

@adriangb adriangb 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.

This looks really nice @etseidl !

Comment on lines +839 to +852
#[derive(Debug)]
enum DecodeState {
/// Finding the first Bloom filter range
FindingRange,
// Reading the SBBF header
ReadingSbbfHeader(Range<u64>),
// Reading the SBBF bitset bytes
ReadingSbbfBitset(Range<u64>),
// Decoding is complete
Finished,
/// State left during the `try_decode` method so something valid is present.
/// This state should never be observed.
Intermediate,
}

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.

🥳

Comment on lines +972 to +980
/// Attempts to decode the filter, returning the file ranges needed if data
/// is missing. Repeated calls before supplying data return the same ranges.
///
/// Without prefetched data, requests one range when `bloom_filter_length` is
/// known, or two otherwise (a 20-byte header estimate, then the bitset).
/// Headers larger than the estimate require an advertised length.
/// After returning [`DecodeResult::Data`], subsequent calls return
/// [`DecodeResult::Finished`].
pub fn try_decode(&mut self) -> Result<DecodeResult<Sbbf>> {

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.

Would it be helpful to have an agent generate a state transition diagram so readers can easily visualize the code?

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'll see what it comes up with, although I think @alamb enjoys making ascii art 😂

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.

Ok, it's there, @adriangb. That's the best we could come up with 😅

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 do enjoy making ascii art. Though recently I had a 🤯 experience of having my agent reverse engineer the monodraw file format so I can have it draft ascii art diagrams and then tweak them visually ❤️

Comment on lines +977 to +979
/// Headers larger than the estimate require an advertised length.
/// After returning [`DecodeResult::Data`], subsequent calls return
/// [`DecodeResult::Finished`].

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 know under what circumstances this happens? Does this error if no length is provided or does it request more data?

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 is a guard for any changes that may be made to the thrift header, either via a new version of the spec, or proprietary changes that would otherwise be ignored by a streaming thrift decoder. One would hope that the bloom filter length field would be populated in such a scenario, which is what the comment is driving at. Were an expanded header not be accompanied by a valid length, then read_bloom_filter_header_and_length would hit an EoF and return an error, and the state machine would be left in an unusable state.

Comment thread parquet/src/bloom_filter/mod.rs Resolved
etseidl and others added 3 commits November 2, 2026 18:30
Co-authored-by: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>
Replace hand-written `match ... other => panic!` blocks and
`assert!(matches!(...))` checks with the `expect_data`,
`expect_needs_data` and `expect_finished` helpers used by the other
push decoder tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@alamb alamb 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.

Thanks @etseidl and @adriangb -- this looks great to me

I did take the liberty of tweaking some tests to use the same pattern as the other push decoders in 4d4935e, just for consistency (e.g.

fn expect_data<T: Debug>(result: Result<DecodeResult<T>>) -> T {
match result.expect("Expected Ok(DecodeResult::Data(T))") {
DecodeResult::Data(data) => data,
result => panic!("Expected DecodeResult::Data, got {result:?}"),
}
}
/// Expect that the [`DecodeResult`] is a [`DecodeResult::NeedsData`] and return the corresponding ranges
fn expect_needs_data<T: Debug>(result: Result<DecodeResult<T>>) -> Vec<Range<u64>> {
match result.expect("Expected Ok(DecodeResult::NeedsData(ranges))") {
DecodeResult::NeedsData(ranges) => ranges,
result => panic!("Expected DecodeResult::NeedsData, got {result:?}"),
}
}
fn expect_finished<T: Debug>(result: Result<DecodeResult<T>>) {
)

);
}

#[derive(Default)]

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.

❤️

"Bloom filter length does not match its header"
));
loop {
let result = decoder.try_decode()?;

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 is so nice

Comment on lines +972 to +980
/// Attempts to decode the filter, returning the file ranges needed if data
/// is missing. Repeated calls before supplying data return the same ranges.
///
/// Without prefetched data, requests one range when `bloom_filter_length` is
/// known, or two otherwise (a 20-byte header estimate, then the bitset).
/// Headers larger than the estimate require an advertised length.
/// After returning [`DecodeResult::Data`], subsequent calls return
/// [`DecodeResult::Finished`].
pub fn try_decode(&mut self) -> Result<DecodeResult<Sbbf>> {

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 do enjoy making ascii art. Though recently I had a 🤯 experience of having my agent reverse engineer the monodraw file format so I can have it draft ascii art diagrams and then tweak them visually ❤️

.contains("Bloom filter read range overflow")
);
assert_eq!(reader.position(), 0);
#[test]

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.

it isn't clear to me why these tests are not with the others in parquet/tests/arrow_reader/bloom_filter/push.rs -- as they all seem to be testing the public APIs

I recommend consolidating to parquet/tests/arrow_reader/bloom_filter/push.rs

#[inline]
fn hash_as_bytes<A: AsBytes + ?Sized>(value: &A) -> u64 {
XxHash64::oneshot(SEED, value.as_bytes())
/// Decoding state machine

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.

as a minor nit I might move this into its own mod like bloom_filter/push.rs or something, but this is totally fine too

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

Labels

enhancement New feature/addition parquet Changes to the parquet crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consolidate Bloom filter parsing

3 participants