This is an automated email from the ASF dual-hosted git repository.

alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 44141e61bb fix(parquet): skip miniblocks wider than 64 values instead 
of erroring (#11021)
44141e61bb is described below

commit 44141e61bbc3cf1d470c0437ee3bbbbe92379904
Author: Bharadwaj Pendyala <[email protected]>
AuthorDate: Thu Sep 10 03:32:41 2026 -0500

    fix(parquet): skip miniblocks wider than 64 values instead of erroring 
(#11021)
    
    # Which issue does this PR close?
    
    - Closes #11018.
    
    # Rationale for this change
    
    `DeltaBitPackDecoder::skip` sized its scratch buffer with a `match` on
    `values_per_mini_block` that only accepted 32 and 64, and returned
    `cannot skip miniblock of size N` for anything else. The spec only
    requires a multiple of 32, and Databricks Photon 0.2 writes 256, so
    valid files from that engine fail to read as soon as a
    `RowSelector::skip` is involved. `get` never had the restriction because
    it chunks against the caller's buffer.
    
    The scratch buffer only exists to walk `last_value` forward through the
    deltas. `BitReader::get_batch` carries its own `bit_offset` and
    `byte_offset`, so reading a 256 value miniblock as four calls of 64
    leaves the reader exactly where one call of 256 would. That means the
    buffer can stay small and the miniblock can be consumed a chunk at a
    time, which was the concern that closed #9793 without a fix.
    
    # What changes are included in this PR?
    
    - Size `skip_buffer` at `values_per_mini_block.min(64)` and loop over
    the miniblock in chunks of it, instead of rejecting sizes other than 32
    and 64.
    - Flip `non_standard_delta_blocks` in `bad_data.rs` from asserting the
    error to asserting the skip lands on the right rows. That test already
    carried a commented-out `skip should succeed` and a TODO for this
    change.
    
    For 32 and 64 the buffer is the same length it was before and the new
    loop runs exactly once, so the path this crate's own writer produces
    allocates and reads what it did before.
    
    # Are these changes tested?
    
    - New `test_delta_bit_packed_skip_wide_miniblocks` builds a hand-written
    page with `block_size = 1024` over 4 miniblocks, so 256 values each. It
    fails on `main` with `cannot skip miniblock of size 256` and passes
    here. It checks the values after a skip that stops mid-miniblock, and
    after one that crosses into a `bit_width = 0` miniblock, since both
    depend on `last_value` surviving the chunk boundary.
    - `non_standard_delta_blocks` now reads `bigdelta.parquet` twice, once
    with the row selection and once without, and compares the 5 selected
    rows against `all.slice(1000, 5)`. Asserting only `num_rows() == 5`
    would pass on wrong values.
    - `cargo test -p parquet --all-features`: 1426 lib tests and 456
    integration tests pass, 0 failures.
    - `cargo clippy -p parquet --all-features --all-targets`: clean.
    - No benchmark numbers here because the change doesn't claim a speedup.
    The argument that the common path is untouched is the buffer length and
    loop count above, not a measurement.
    
    # Are there any user-facing changes?
    
    Reading a page with miniblocks wider than 64 values under a row
    selection now returns data instead of an error. No public API changes.
    
    There's also an existing overflow bug in `skip`. The `bit_width == 0`
    branch of `skip` computes `min_delta * n` in `i64` and then converts to
    `T::T`, so a valid INT32 progression can fail the conversion even though
    every decoded value fits. A page with `first_value = -2000000000`,
    `min_delta = 10000000` and zero-width miniblocks reads fine through
    `get`, giving 560000000 at index 256, but `skip(257)` returns `delta*n
    overflow in skip`. That's independent of miniblock width and predates
    this change, so I've left it for a separate issue and fix.
    
    # AI usage
    
    Claude wrote the change and the tests, and Codex reviewed the diff
    adversarially before it was pushed. I checked every finding myself: the
    split-read equivalence against `BitReader::get_batch`, the
    truncated-page error path, and the overflow above, which I reproduced
    before writing it down. The PR description is AI-assisted.
    
    ---------
    
    Co-authored-by: Ed Seidl <[email protected]>
---
 parquet/src/encodings/decoding.rs      | 129 +++++++++++++++++++++++----------
 parquet/tests/arrow_reader/bad_data.rs |  29 +++++---
 2 files changed, 107 insertions(+), 51 deletions(-)

diff --git a/parquet/src/encodings/decoding.rs 
b/parquet/src/encodings/decoding.rs
index c884f3c2e0..9830cd9d5e 100644
--- a/parquet/src/encodings/decoding.rs
+++ b/parquet/src/encodings/decoding.rs
@@ -515,6 +515,11 @@ where
 // ----------------------------------------------------------------------
 // DELTA_BINARY_PACKED Decoding
 
+/// Upper bound on the scratch buffer [`DeltaBitPackDecoder::skip`] decodes 
into.
+/// Matches the widest miniblock this crate writes, and the widest unpack
+/// [`BitReader::get_batch`] performs in one step for a 64 bit type.
+const MAX_SKIP_BUFFER_VALUES: usize = 64;
+
 /// Delta binary packed decoder.
 /// Supports INT32 and INT64 types.
 /// See [`DeltaBitPackEncoder`](crate::encoding::DeltaBitPackEncoder) for more
@@ -852,23 +857,12 @@ where
         }
 
         // See https://github.com/apache/arrow-rs/pull/9794.
-        // The parquet spec actually allows for miniblock sizes other than 32 
or 64, but
-        // no current writers use anything else. Using values_per_mini_block 
directly
-        // for the skip_buffer doesn't allow stack allocation and leads to a 
significant
-        // drop in performance. We'll settle for erroring out here and come up 
with a
-        // better fix if writers ever start getting creative with block sizes.
-        let mini_block_batch_size = match self.values_per_mini_block {
-            32 => 32,
-            64 => 64,
-            _ => {
-                return Err(general_err!(
-                    "cannot skip miniblock of size {}",
-                    self.values_per_mini_block
-                ));
-            }
-        };
-
-        let mut skip_buffer = vec![T::T::default(); mini_block_batch_size];
+        // The parquet spec allows miniblock sizes other than the 32 and 64 
this crate
+        // writes, and sizing skip_buffer off values_per_mini_block costs us 
the stack
+        // allocation that keeps skip fast. The buffer only exists to walk 
last_value
+        // forward, so keep it fixed at the widest size the common cases need 
and
+        // consume wider miniblocks a chunk at a time.
+        let mut skip_buffer = [T::T::default(); MAX_SKIP_BUFFER_VALUES];
         while skip < to_skip {
             if self.mini_block_remaining == 0 {
                 self.next_mini_block()?;
@@ -894,30 +888,37 @@ where
                 // bit_width=0 payloads occupy zero bytes; no bit_reader 
advancement needed.
             } else {
                 // bw>0: must decode to track last_value for subsequent get() 
calls.
-                let skip_count = self
-                    .bit_reader
-                    .get_batch(&mut skip_buffer[0..mini_block_to_skip], 
bit_width);
-
-                if skip_count != mini_block_to_skip {
-                    return Err(general_err!(
-                        "Expected to skip {} values from mini block got {}.",
-                        mini_block_to_skip,
-                        skip_count
-                    ));
-                }
-
-                if min_delta == 0 {
-                    for v in &mut skip_buffer[0..skip_count] {
-                        *v = v.wrapping_add(&self.last_value);
-                        self.last_value = *v;
+                let mut skipped_in_mini_block = 0;
+                while skipped_in_mini_block < mini_block_to_skip {
+                    let batch_to_skip =
+                        (mini_block_to_skip - 
skipped_in_mini_block).min(skip_buffer.len());
+                    let skip_count = self
+                        .bit_reader
+                        .get_batch(&mut skip_buffer[0..batch_to_skip], 
bit_width);
+
+                    if skip_count != batch_to_skip {
+                        return Err(general_err!(
+                            "Expected to skip {} values from mini block got 
{}.",
+                            batch_to_skip,
+                            skip_count
+                        ));
                     }
-                } else {
-                    for v in &mut skip_buffer[0..skip_count] {
-                        *v = v
-                            .wrapping_add(&self.min_delta)
-                            .wrapping_add(&self.last_value);
-                        self.last_value = *v;
+
+                    if min_delta == 0 {
+                        for v in &mut skip_buffer[0..skip_count] {
+                            *v = v.wrapping_add(&self.last_value);
+                            self.last_value = *v;
+                        }
+                    } else {
+                        for v in &mut skip_buffer[0..skip_count] {
+                            *v = v
+                                .wrapping_add(&self.min_delta)
+                                .wrapping_add(&self.last_value);
+                            self.last_value = *v;
+                        }
                     }
+
+                    skipped_in_mini_block += batch_to_skip;
                 }
             }
 
@@ -1912,6 +1913,56 @@ mod tests {
         assert_eq!(result, vec![29, 43, 89]);
     }
 
+    #[test]
+    fn test_delta_bit_packed_skip_wide_miniblocks() {
+        // 1024 values per block over 4 miniblocks is 256 values per 
miniblock, which is
+        // spec legal but wider than anything this crate writes. Databricks 
Photon 0.2
+        // emits pages shaped like this. See
+        // https://github.com/apache/arrow-rs/issues/11018.
+        let header = vec![
+            128, 8, // block_size = 1024
+            4, // mini_blocks_per_block = 4
+            172, 2, // total_values = 300
+            0, // first_value = 0
+        ];
+
+        let block_header = vec![
+            0, // min_delta = 0
+            1, 0, 0, 0, // bit widths
+        ];
+
+        // Miniblock 1 - bit width 1, all deltas 1 => 256 bits
+        // Miniblocks 2 to 4 - bit width 0 => no bytes
+        let block = vec![0xFF; 32];
+
+        let data: Vec<u8> = header
+            .into_iter()
+            .chain(block_header)
+            .chain(block)
+            .collect();
+        let data = Bytes::from(data);
+
+        // Values are 0..=256 followed by 43 more copies of 256.
+        let expected: Vec<i32> = (200..=256).chain(std::iter::repeat_n(256, 
43)).collect();
+
+        let mut decoder = DeltaBitPackDecoder::<Int32Type>::new();
+        decoder.set_data(data.clone(), 0).unwrap();
+        assert_eq!(decoder.skip(200).unwrap(), 200);
+
+        let mut output = vec![0_i32; 100];
+        assert_eq!(decoder.get(&mut output).unwrap(), 100);
+        assert_eq!(output, expected);
+
+        // Skipping across the miniblock boundary must leave last_value on the 
plateau.
+        let mut decoder = DeltaBitPackDecoder::<Int32Type>::new();
+        decoder.set_data(data, 0).unwrap();
+        assert_eq!(decoder.skip(299).unwrap(), 299);
+
+        let mut output = vec![0_i32; 1];
+        assert_eq!(decoder.get(&mut output).unwrap(), 1);
+        assert_eq!(output, vec![256]);
+    }
+
     #[test]
     fn test_delta_bit_packed_padding() {
         // Page header
diff --git a/parquet/tests/arrow_reader/bad_data.rs 
b/parquet/tests/arrow_reader/bad_data.rs
index 8587d15230..b22e78a32b 100644
--- a/parquet/tests/arrow_reader/bad_data.rs
+++ b/parquet/tests/arrow_reader/bad_data.rs
@@ -17,7 +17,9 @@
 
 //! Tests that reading invalid parquet files returns an error
 
+use arrow::compute::concat_batches;
 use arrow::util::test_util::parquet_test_data;
+use arrow_array::RecordBatchReader;
 use bytes::Bytes;
 use parquet::arrow::arrow_reader::ArrowReaderBuilder;
 use parquet::errors::ParquetError;
@@ -171,23 +173,26 @@ fn non_standard_delta_blocks() {
     let selectors = vec![RowSelector::skip(1000), RowSelector::select(5)];
 
     let selection: RowSelection = selectors.into();
-    let reader = ArrowReaderBuilder::try_new(file)
+    let reader = ArrowReaderBuilder::try_new(file.clone())
         .unwrap()
         .with_row_selection(selection)
         .build()
         .unwrap();
 
-    if let Some(maybe_batch) = reader.into_iter().next() {
-        // TODO: uncomment if we ever allow skipping miniblocks > 64 elements
-        //let batch = maybe_batch.expect("skip should succeed");
-        //assert_eq!(batch.num_rows(), 5);
-        assert!(
-            maybe_batch
-                .unwrap_err()
-                .to_string()
-                .contains("cannot skip miniblock of size 128")
-        );
-    }
+    let selected = concat_batches(
+        &reader.schema(),
+        &reader.collect::<Result<Vec<_>, _>>().unwrap(),
+    )
+    .unwrap();
+    assert_eq!(selected.num_rows(), 5);
+
+    let reader = ArrowReaderBuilder::try_new(file).unwrap().build().unwrap();
+    let all = concat_batches(
+        &reader.schema(),
+        &reader.collect::<Result<Vec<_>, _>>().unwrap(),
+    )
+    .unwrap();
+    assert_eq!(selected, all.slice(1000, 5));
 }
 
 #[test]

Reply via email to