adriangb commented on code in PR #11159:
URL: https://github.com/apache/arrow-rs/pull/11159#discussion_r4078133601


##########
parquet/src/file/metadata/page_index.rs:
##########
@@ -363,6 +365,183 @@ impl RowGroupPageIndex {
     }
 }
 
+/// A memory-efficient sparse set representation storing sorted deduplicated 
indexes
+///
+/// Stores which positions are set in a sparse vector. For example:
+/// `[None, None, Some(...), Some(...), None, Some(...)]` becomes `[2, 3, 5]`
+///
+/// Position checking uses binary search for O(log n) lookup.

Review Comment:
   **Nit (D10).** `position` uses a linear scan for 32 indexes or fewer 
(L427-433).
   
   ```suggestion
   /// Position checking uses a linear scan for up to 32 indexes, and binary 
search above that.
   ```



##########
parquet/src/file/metadata/page_index.rs:
##########
@@ -363,6 +365,183 @@ impl RowGroupPageIndex {
     }
 }
 
+/// A memory-efficient sparse set representation storing sorted deduplicated 
indexes
+///
+/// Stores which positions are set in a sparse vector. For example:
+/// `[None, None, Some(...), Some(...), None, Some(...)]` becomes `[2, 3, 5]`
+///
+/// Position checking uses binary search for O(log n) lookup.
+///
+/// Implementation note: we can downsize to `i32` here because thrift encodes 
vector
+/// sizes with an `i32`.
+#[derive(Debug, Clone, PartialEq)]
+pub(crate) struct Keep {
+    /// Sorted, deduplicated indexes of set positions
+    /// None means all positions in the span are set
+    kept: Option<Arc<[i32]>>,
+    /// Total span of positions (0..span)
+    span: i32,
+}
+
+impl Keep {
+    // an empty set or a set whose size equals "span" will consider all 
positions in the
+    // span are set.
+    pub(crate) fn new(set: &BTreeSet<i32>, span: usize) -> Self {
+        assert!(
+            i32::try_from(span).is_ok(),
+            "Keep cannot have a span that exceeds the storage of an i32: got 
{span}"
+        );
+        // this should preserve the BTreeSet ordering
+        let kept = set
+            .iter()
+            .filter(|&&idx| idx < span as i32 && idx >= 0)
+            .copied()
+            .collect::<Vec<_>>();
+        let kept = if kept.is_empty() || kept.len() == span {
+            None
+        } else {
+            Some(Arc::from(kept))
+        };

Review Comment:
   **Footgun (D2) + pre-allocation (D7).** A mask whose indexes are all out of 
range keeps *every* position. The reader uses the opposite meaning 
(`includes_column` is `false`):
   
   ```text
   ColumnChunkMask::columns([100]) on 4 columns:
     mask.includes_column(0)                          = false
     new_with_mask(..) then put_column_index(_, 0, 0) -> stored
   ColumnChunkMask::columns([1]):
     new_with_mask(..) then put_column_index(_, 0, 0) -> dropped
   ```
   
   Through the reader (1000 columns × 100 row groups, CI `Optional`, OI 
`Required`), `with_column_index_mask(columns([5000]))` peaks at 30.4 MB, the 
same as `all()`. With `columns([0])` the peak is 7.2 MB. The parser loads 
nothing into the extra 23.2 MB grid.
   
   Cause: L400 treats "no index left after the filter" like "empty set" (= all).
   
   Fix: keep "empty set = all" (as in `ColumnChunkMask`), but let "no index in 
range" keep nothing. The same change sizes the `Vec` up front: `set.len()` is 
an upper bound, and `range` replaces the filter. Tested: `cargo test -p parquet 
--features arrow,async --lib file::metadata` passes (also 
`test_empty_keep_selects_all`), and the peak above drops to 7.2 MB.
   
   ```suggestion
       // An empty set, or a set that covers the whole span, keeps all 
positions.
       // Indexes outside `0..span` are ignored: if none is in range, no 
position is kept.
       pub(crate) fn new(set: &BTreeSet<i32>, span: usize) -> Self {
           assert!(
               i32::try_from(span).is_ok(),
               "Keep cannot have a span that exceeds the storage of an i32: got 
{span}"
           );
           // `range` yields the in-range indexes in sorted order; `set.len()` 
is an upper bound
           let mut kept = Vec::with_capacity(set.len());
           kept.extend(set.range(0..span as i32));
           let kept = (!set.is_empty() && kept.len() != span).then(|| 
Arc::from(kept));
   ```
   
   `Arc::from` still copies once. If `ColumnChunkMask` stored a sorted 
`Arc<[i32]>` instead of `Arc<BTreeSet<i32>>`, `Keep` could share it: 0 
allocations, and one slice for both grids.



##########
parquet/src/file/metadata/page_index.rs:
##########
@@ -363,6 +365,183 @@ impl RowGroupPageIndex {
     }
 }
 
+/// A memory-efficient sparse set representation storing sorted deduplicated 
indexes
+///
+/// Stores which positions are set in a sparse vector. For example:
+/// `[None, None, Some(...), Some(...), None, Some(...)]` becomes `[2, 3, 5]`
+///
+/// Position checking uses binary search for O(log n) lookup.
+///
+/// Implementation note: we can downsize to `i32` here because thrift encodes 
vector
+/// sizes with an `i32`.
+#[derive(Debug, Clone, PartialEq)]
+pub(crate) struct Keep {
+    /// Sorted, deduplicated indexes of set positions
+    /// None means all positions in the span are set
+    kept: Option<Arc<[i32]>>,
+    /// Total span of positions (0..span)
+    span: i32,
+}
+
+impl Keep {
+    // an empty set or a set whose size equals "span" will consider all 
positions in the
+    // span are set.
+    pub(crate) fn new(set: &BTreeSet<i32>, span: usize) -> Self {
+        assert!(
+            i32::try_from(span).is_ok(),
+            "Keep cannot have a span that exceeds the storage of an i32: got 
{span}"
+        );
+        // this should preserve the BTreeSet ordering
+        let kept = set
+            .iter()
+            .filter(|&&idx| idx < span as i32 && idx >= 0)
+            .copied()
+            .collect::<Vec<_>>();
+        let kept = if kept.is_empty() || kept.len() == span {
+            None
+        } else {
+            Some(Arc::from(kept))
+        };
+
+        Self {
+            kept,
+            span: span as i32,
+        }
+    }
+
+    // shortened version for a full keep set
+    pub(crate) fn new_full(span: usize) -> Self {
+        assert!(
+            i32::try_from(span).is_ok(),
+            "Keep cannot have a span that exceeds the storage of an i32: got 
{span}"
+        );
+        Self {
+            kept: None,
+            span: span as i32,
+        }
+    }
+
+    /// Retrieve a position if set
+    fn position(&self, idx: usize) -> Option<usize> {
+        let needle = i32::try_from(idx).ok()?;
+        // below CUTOFF elements, use linear search
+        const CUTOFF: usize = 32;
+        match self.kept.as_ref() {
+            None => (idx < self.span as usize).then_some(idx),
+            Some(k) if k.len() > CUTOFF => k.binary_search(&needle).ok(),
+            Some(k) => k.iter().position(|&i| i == needle),
+        }
+    }
+
+    /// Returns the number of set positions
+    fn len(&self) -> usize {
+        match &self.kept {
+            None => self.span as usize,
+            Some(indexes) => indexes.len(),
+        }
+    }
+}
+
+impl HeapSize for Arc<[i32]> {
+    fn heap_size(&self) -> usize {
+        // Arc stores weak and strong counts on the heap alongside an instance 
of T
+        // T = [i32], so that should be the size of a pointer + the size of 
the allocation
+        2 * std::mem::size_of::<usize>()
+            + std::mem::size_of::<*mut i32>()
+            + std::mem::size_of_val(self.as_ref())

Review Comment:
   **Nit (D8).** The fat pointer is stored inline in `Keep`, not in the heap 
block. The block is also padded to the alignment of `usize`. Measured with a 
counting allocator (`Arc::<[i32]>::from`, 64-bit):
   
   | len | allocated | this PR | suggestion |
   |---|---|---|---|
   | 1 | 24 | 28 | 24 |
   | 2 | 24 | 32 | 24 |
   | 3 | 32 | 36 | 32 |
   
   ```suggestion
           // The heap block holds the strong and weak counts, then the slice, 
padded to the
           // alignment of `usize`. The (fat) pointer itself is stored inline, 
not on the heap.
           (2 * std::mem::size_of::<usize>() + 
std::mem::size_of_val(self.as_ref()))
               .next_multiple_of(std::mem::align_of::<usize>())
   ```
   
   `test_page_index_sizes` then expects 10664, 9680, 8342 (was 10668, 9692, 
8362). With `encryption`: 11088, 10104, 8766 (was 11092, 10116, 8786). I ran 
both.



##########
parquet/src/file/metadata/page_index.rs:
##########
@@ -363,6 +365,183 @@ impl RowGroupPageIndex {
     }
 }
 
+/// A memory-efficient sparse set representation storing sorted deduplicated 
indexes
+///
+/// Stores which positions are set in a sparse vector. For example:
+/// `[None, None, Some(...), Some(...), None, Some(...)]` becomes `[2, 3, 5]`
+///
+/// Position checking uses binary search for O(log n) lookup.
+///
+/// Implementation note: we can downsize to `i32` here because thrift encodes 
vector
+/// sizes with an `i32`.
+#[derive(Debug, Clone, PartialEq)]
+pub(crate) struct Keep {
+    /// Sorted, deduplicated indexes of set positions
+    /// None means all positions in the span are set
+    kept: Option<Arc<[i32]>>,
+    /// Total span of positions (0..span)
+    span: i32,
+}
+
+impl Keep {
+    // an empty set or a set whose size equals "span" will consider all 
positions in the
+    // span are set.
+    pub(crate) fn new(set: &BTreeSet<i32>, span: usize) -> Self {
+        assert!(
+            i32::try_from(span).is_ok(),
+            "Keep cannot have a span that exceeds the storage of an i32: got 
{span}"
+        );
+        // this should preserve the BTreeSet ordering
+        let kept = set
+            .iter()
+            .filter(|&&idx| idx < span as i32 && idx >= 0)
+            .copied()
+            .collect::<Vec<_>>();
+        let kept = if kept.is_empty() || kept.len() == span {
+            None
+        } else {
+            Some(Arc::from(kept))
+        };
+
+        Self {
+            kept,
+            span: span as i32,
+        }
+    }
+
+    // shortened version for a full keep set
+    pub(crate) fn new_full(span: usize) -> Self {
+        assert!(
+            i32::try_from(span).is_ok(),
+            "Keep cannot have a span that exceeds the storage of an i32: got 
{span}"
+        );
+        Self {
+            kept: None,
+            span: span as i32,
+        }
+    }
+
+    /// Retrieve a position if set
+    fn position(&self, idx: usize) -> Option<usize> {
+        let needle = i32::try_from(idx).ok()?;
+        // below CUTOFF elements, use linear search
+        const CUTOFF: usize = 32;
+        match self.kept.as_ref() {
+            None => (idx < self.span as usize).then_some(idx),
+            Some(k) if k.len() > CUTOFF => k.binary_search(&needle).ok(),
+            Some(k) => k.iter().position(|&i| i == needle),
+        }
+    }
+
+    /// Returns the number of set positions
+    fn len(&self) -> usize {
+        match &self.kept {
+            None => self.span as usize,
+            Some(indexes) => indexes.len(),
+        }
+    }
+}
+
+impl HeapSize for Arc<[i32]> {
+    fn heap_size(&self) -> usize {
+        // Arc stores weak and strong counts on the heap alongside an instance 
of T
+        // T = [i32], so that should be the size of a pointer + the size of 
the allocation
+        2 * std::mem::size_of::<usize>()
+            + std::mem::size_of::<*mut i32>()
+            + std::mem::size_of_val(self.as_ref())
+    }
+}
+
+impl HeapSize for Keep {
+    fn heap_size(&self) -> usize {
+        self.kept.heap_size()
+    }
+}
+
+/// A memory-efficient 2D sparse grid using Keep structures for rows and 
columns
+///
+/// Maps (row_group_idx, column_idx) to values efficiently for sparse access 
patterns.
+/// This is particularly useful when only a few columns are accessed from wide 
schemas.
+#[derive(Debug, Clone, PartialEq)]
+pub(crate) struct Grid<T> {
+    /// Set of row group indexes that have any values
+    rows: Keep,
+    /// Set of column indexes that have any values
+    cols: Keep,

Review Comment:
   **Nit (D10).** These fields hold the positions that have storage (from the 
mask). A kept position can have no value.
   
   ```suggestion
       /// Row group indexes that have storage (from the mask); a cell can 
still be `None`
       rows: Keep,
       /// Column indexes that have storage (from the mask); a cell can still 
be `None`
       cols: Keep,
   ```



##########
parquet/src/file/metadata/page_index.rs:
##########
@@ -363,6 +365,183 @@ impl RowGroupPageIndex {
     }
 }
 
+/// A memory-efficient sparse set representation storing sorted deduplicated 
indexes
+///
+/// Stores which positions are set in a sparse vector. For example:
+/// `[None, None, Some(...), Some(...), None, Some(...)]` becomes `[2, 3, 5]`
+///
+/// Position checking uses binary search for O(log n) lookup.
+///
+/// Implementation note: we can downsize to `i32` here because thrift encodes 
vector
+/// sizes with an `i32`.
+#[derive(Debug, Clone, PartialEq)]
+pub(crate) struct Keep {
+    /// Sorted, deduplicated indexes of set positions
+    /// None means all positions in the span are set
+    kept: Option<Arc<[i32]>>,
+    /// Total span of positions (0..span)
+    span: i32,
+}
+
+impl Keep {
+    // an empty set or a set whose size equals "span" will consider all 
positions in the
+    // span are set.
+    pub(crate) fn new(set: &BTreeSet<i32>, span: usize) -> Self {
+        assert!(
+            i32::try_from(span).is_ok(),
+            "Keep cannot have a span that exceeds the storage of an i32: got 
{span}"
+        );
+        // this should preserve the BTreeSet ordering
+        let kept = set
+            .iter()
+            .filter(|&&idx| idx < span as i32 && idx >= 0)
+            .copied()
+            .collect::<Vec<_>>();
+        let kept = if kept.is_empty() || kept.len() == span {
+            None
+        } else {
+            Some(Arc::from(kept))
+        };
+
+        Self {
+            kept,
+            span: span as i32,
+        }
+    }
+
+    // shortened version for a full keep set
+    pub(crate) fn new_full(span: usize) -> Self {
+        assert!(
+            i32::try_from(span).is_ok(),
+            "Keep cannot have a span that exceeds the storage of an i32: got 
{span}"
+        );
+        Self {
+            kept: None,
+            span: span as i32,
+        }
+    }
+
+    /// Retrieve a position if set
+    fn position(&self, idx: usize) -> Option<usize> {
+        let needle = i32::try_from(idx).ok()?;
+        // below CUTOFF elements, use linear search
+        const CUTOFF: usize = 32;
+        match self.kept.as_ref() {
+            None => (idx < self.span as usize).then_some(idx),
+            Some(k) if k.len() > CUTOFF => k.binary_search(&needle).ok(),
+            Some(k) => k.iter().position(|&i| i == needle),
+        }
+    }
+
+    /// Returns the number of set positions
+    fn len(&self) -> usize {
+        match &self.kept {
+            None => self.span as usize,
+            Some(indexes) => indexes.len(),
+        }
+    }
+}
+
+impl HeapSize for Arc<[i32]> {
+    fn heap_size(&self) -> usize {
+        // Arc stores weak and strong counts on the heap alongside an instance 
of T
+        // T = [i32], so that should be the size of a pointer + the size of 
the allocation
+        2 * std::mem::size_of::<usize>()
+            + std::mem::size_of::<*mut i32>()
+            + std::mem::size_of_val(self.as_ref())
+    }
+}
+
+impl HeapSize for Keep {
+    fn heap_size(&self) -> usize {
+        self.kept.heap_size()
+    }
+}
+
+/// A memory-efficient 2D sparse grid using Keep structures for rows and 
columns
+///
+/// Maps (row_group_idx, column_idx) to values efficiently for sparse access 
patterns.
+/// This is particularly useful when only a few columns are accessed from wide 
schemas.
+#[derive(Debug, Clone, PartialEq)]
+pub(crate) struct Grid<T> {
+    /// Set of row group indexes that have any values
+    rows: Keep,
+    /// Set of column indexes that have any values
+    cols: Keep,
+    /// Flattened cells stored in row-major order
+    /// cells[row_offset * cols.len() + col_offset] = value at (row, col)
+    /// where row_offset = position of row in rows.kept
+    /// and col_offset = position of col in cols.kept
+    cells: Vec<Option<T>>,
+}
+
+impl<T: Clone> Grid<T> {
+    /// Creates a new empty Grid with the specified dimensions
+    fn new(rows: Keep, cols: Keep) -> Self {
+        let size = rows.len() * cols.len();
+        let cells = vec![None; size];
+        Self { rows, cols, cells }
+    }
+
+    pub(crate) fn new_dense(num_row_groups: usize, num_columns: usize) -> Self 
{
+        let rows = Keep::new_full(num_row_groups);
+        let cols = Keep::new_full(num_columns);
+        Self::new(rows, cols)
+    }
+
+    pub(crate) fn from_vec(index: Vec<Vec<Option<T>>>) -> Self {
+        let num_row_groups = index.len();
+        let num_columns = if index.is_empty() { 0 } else { index[0].len() };
+        let mut result = Self::new_dense(num_row_groups, num_columns);
+        for (rg_idx, row_group) in index.into_iter().enumerate() {
+            for (col_idx, idx) in row_group.into_iter().enumerate() {
+                if let Some(idx) = idx {
+                    result.insert(rg_idx, col_idx, idx);
+                }
+            }
+        }
+
+        result
+    }

Review Comment:
   **Pre-allocation + footgun (D6).** The size is known (n × m). This code 
fills n × m `None` values, then calls `insert` for each cell (2 
`Keep::position` lookups each). It also takes the width from row 0, and it 
drops cells of longer rows without an error (observed on this head):
   
   ```text
   Grid::from_vec(vec![vec![None], vec![None, Some(1)]]).get(1, 1)  -> None
   Grid::from_vec(vec![vec![], vec![Some(1)]]).is_empty()           -> true
   ```
   
   The only caller (`SerializedFileWriter::write_metadata`) passes rectangular 
input today, so the data loss is latent. This version moves each row into one 
exact allocation and rejects ragged input. `--lib file::metadata` and `--test 
arrow_writer` pass.
   
   ```suggestion
       pub(crate) fn from_vec(index: Vec<Vec<Option<T>>>) -> Self {
           let num_row_groups = index.len();
           let num_columns = index.first().map_or(0, Vec::len);
           let mut cells = Vec::with_capacity(num_row_groups * num_columns);
           for row_group in index {
               assert_eq!(row_group.len(), num_columns, "ragged page index");
               cells.extend(row_group);
           }
           Self {
               rows: Keep::new_full(num_row_groups),
               cols: Keep::new_full(num_columns),
               cells,
           }
       }
   ```
   
   Keep `assert_eq!`, not `debug_assert_eq!`: in a release build, ragged input 
would move cells to the wrong position.



##########
parquet/src/file/metadata/page_index.rs:
##########
@@ -554,9 +712,34 @@ impl PageIndexBuilder {
     /// All index entries are initialized to `None` and can be populated using
     /// [`put_column_index`](Self::put_column_index) and 
[`put_offset_index`](Self::put_offset_index).

Review Comment:
   **Docs (D9).** `Keep::new_full` asserts that the span fits in an `i32`. So 
this method now panics for a dimension above `i32::MAX`, even when the other 
dimension is 0:
   
   ```text
   PageIndexBuilder::new(0, i32::MAX as usize + 1)
   panicked at parquet/src/file/metadata/page_index.rs:414:9:
   Keep cannot have a span that exceeds the storage of an i32: got 2147483648
   ```
   
   On the [#11157](https://github.com/apache/arrow-rs/pull/11157) head it 
returns an empty builder. Real files cannot have that many row groups or 
columns, so a `# Panics` section is enough. The same applies to `new_with_mask` 
and `allocate_*`.
   
   ```suggestion
       /// [`put_column_index`](Self::put_column_index) and 
[`put_offset_index`](Self::put_offset_index).
       ///
       /// # Panics
       ///
       /// Panics if `num_row_groups` or `num_columns` is greater than 
`i32::MAX`.
   ```



##########
parquet/src/file/metadata/page_index.rs:
##########
@@ -554,9 +712,34 @@ impl PageIndexBuilder {
     /// All index entries are initialized to `None` and can be populated using
     /// [`put_column_index`](Self::put_column_index) and 
[`put_offset_index`](Self::put_offset_index).
     pub fn new(num_row_groups: usize, num_columns: usize) -> Self {
+        let keep_cols = Keep::new_full(num_columns);
+        let keep_rows = Keep::new_full(num_row_groups);
         Self {
-            column_indexes: Self::empty_index(num_row_groups, num_columns),
-            offset_indexes: Self::empty_index(num_row_groups, num_columns),
+            column_indexes: Some(Grid::new(keep_rows.clone(), 
keep_cols.clone())),
+            offset_indexes: Some(Grid::new(keep_rows, keep_cols)),
+        }
+    }
+
+    /// Creates a new [`PageIndexBuilder`] where storage is defined by the 
policy
+    ///
+    /// For sparse indexes, this can save a great deal of memory
+    pub fn new_with_mask(

Review Comment:
   **API (D3).** This is new public API, but the PR description says "No, only 
internal storage structures are changed". After a release, the signature and 
the "drop outside the mask" behavior (D4) are hard to change. The parser is the 
only caller. I suggest `pub(crate)` until there is a public use case. The doc 
also says "policy", but the storage comes from the masks.
   
   ```suggestion
       /// Creates a new [`PageIndexBuilder`] where storage is defined by the 
masks
       ///
       /// For sparse indexes, this can save a great deal of memory. The 
`put_*` methods
       /// ignore positions outside the masks.
       pub(crate) fn new_with_mask(
   ```
   
   The D1 patch (review body) also changes this signature to 
`Option<&ColumnChunkMask>`. With `pub(crate)`, that change costs nothing.



##########
parquet/src/file/metadata/page_index.rs:
##########
@@ -363,6 +365,183 @@ impl RowGroupPageIndex {
     }
 }
 
+/// A memory-efficient sparse set representation storing sorted deduplicated 
indexes
+///
+/// Stores which positions are set in a sparse vector. For example:
+/// `[None, None, Some(...), Some(...), None, Some(...)]` becomes `[2, 3, 5]`
+///
+/// Position checking uses binary search for O(log n) lookup.
+///
+/// Implementation note: we can downsize to `i32` here because thrift encodes 
vector
+/// sizes with an `i32`.
+#[derive(Debug, Clone, PartialEq)]
+pub(crate) struct Keep {
+    /// Sorted, deduplicated indexes of set positions
+    /// None means all positions in the span are set
+    kept: Option<Arc<[i32]>>,
+    /// Total span of positions (0..span)
+    span: i32,
+}
+
+impl Keep {
+    // an empty set or a set whose size equals "span" will consider all 
positions in the
+    // span are set.
+    pub(crate) fn new(set: &BTreeSet<i32>, span: usize) -> Self {
+        assert!(
+            i32::try_from(span).is_ok(),
+            "Keep cannot have a span that exceeds the storage of an i32: got 
{span}"
+        );
+        // this should preserve the BTreeSet ordering
+        let kept = set
+            .iter()
+            .filter(|&&idx| idx < span as i32 && idx >= 0)
+            .copied()
+            .collect::<Vec<_>>();
+        let kept = if kept.is_empty() || kept.len() == span {
+            None
+        } else {
+            Some(Arc::from(kept))
+        };
+
+        Self {
+            kept,
+            span: span as i32,
+        }
+    }
+
+    // shortened version for a full keep set
+    pub(crate) fn new_full(span: usize) -> Self {
+        assert!(
+            i32::try_from(span).is_ok(),
+            "Keep cannot have a span that exceeds the storage of an i32: got 
{span}"
+        );
+        Self {
+            kept: None,
+            span: span as i32,
+        }
+    }
+
+    /// Retrieve a position if set
+    fn position(&self, idx: usize) -> Option<usize> {
+        let needle = i32::try_from(idx).ok()?;
+        // below CUTOFF elements, use linear search
+        const CUTOFF: usize = 32;
+        match self.kept.as_ref() {
+            None => (idx < self.span as usize).then_some(idx),
+            Some(k) if k.len() > CUTOFF => k.binary_search(&needle).ok(),
+            Some(k) => k.iter().position(|&i| i == needle),
+        }
+    }
+
+    /// Returns the number of set positions
+    fn len(&self) -> usize {
+        match &self.kept {
+            None => self.span as usize,
+            Some(indexes) => indexes.len(),
+        }
+    }
+}
+
+impl HeapSize for Arc<[i32]> {
+    fn heap_size(&self) -> usize {
+        // Arc stores weak and strong counts on the heap alongside an instance 
of T
+        // T = [i32], so that should be the size of a pointer + the size of 
the allocation
+        2 * std::mem::size_of::<usize>()
+            + std::mem::size_of::<*mut i32>()
+            + std::mem::size_of_val(self.as_ref())
+    }
+}
+
+impl HeapSize for Keep {
+    fn heap_size(&self) -> usize {
+        self.kept.heap_size()
+    }
+}
+
+/// A memory-efficient 2D sparse grid using Keep structures for rows and 
columns
+///
+/// Maps (row_group_idx, column_idx) to values efficiently for sparse access 
patterns.
+/// This is particularly useful when only a few columns are accessed from wide 
schemas.
+#[derive(Debug, Clone, PartialEq)]

Review Comment:
   **API (D5).** The derived `PartialEq` compares the storage shape. 
`PageIndex` and `ParquetMetaData` use it, so indexes with equal content can 
compare unequal. With the dense layout of 60.0.0, this did not happen.
   
   ```text
   a = PageIndexBuilder::new(1, 2)                                 + put CI/OI 
at (0, 0)
   b = PageIndexBuilder::new_with_mask(1, 2, columns([0]), columns([0])) + put 
CI/OI at (0, 0)
   same answer for every (rg, col): true
   a == b: false
   ```
   
   The same happens for `ParquetMetaData` from a full read and a 
`with_column_index_mask(columns([0]))` read of a file where column 1 has no 
column index.
   
   Fix: compare the content (tested; the check above then gives `a == b: true`, 
and the lib tests pass). Remove `PartialEq` from this derive and add:
   
   ```rust
   /// Two grids are equal if they return the same value for every position,
   /// whatever their storage shape
   impl<T: PartialEq> PartialEq for Grid<T> {
       fn eq(&self, other: &Self) -> bool {
           if self.rows == other.rows && self.cols == other.cols {
               return self.cells == other.cells;
           }
           let rows = self.rows.span.max(other.rows.span) as usize;
           let cols = self.cols.span.max(other.cols.span) as usize;
           (0..rows).all(|r| (0..cols).all(|c| self.get(r, c) == other.get(r, 
c)))
       }
   }
   ```
   
   The fast path keeps today's cost when the shapes are equal.



##########
parquet/src/file/metadata/page_index.rs:
##########
@@ -363,6 +365,183 @@ impl RowGroupPageIndex {
     }
 }
 
+/// A memory-efficient sparse set representation storing sorted deduplicated 
indexes
+///
+/// Stores which positions are set in a sparse vector. For example:
+/// `[None, None, Some(...), Some(...), None, Some(...)]` becomes `[2, 3, 5]`
+///
+/// Position checking uses binary search for O(log n) lookup.
+///
+/// Implementation note: we can downsize to `i32` here because thrift encodes 
vector
+/// sizes with an `i32`.
+#[derive(Debug, Clone, PartialEq)]
+pub(crate) struct Keep {
+    /// Sorted, deduplicated indexes of set positions
+    /// None means all positions in the span are set
+    kept: Option<Arc<[i32]>>,
+    /// Total span of positions (0..span)
+    span: i32,
+}
+
+impl Keep {
+    // an empty set or a set whose size equals "span" will consider all 
positions in the
+    // span are set.
+    pub(crate) fn new(set: &BTreeSet<i32>, span: usize) -> Self {
+        assert!(
+            i32::try_from(span).is_ok(),
+            "Keep cannot have a span that exceeds the storage of an i32: got 
{span}"
+        );
+        // this should preserve the BTreeSet ordering
+        let kept = set
+            .iter()
+            .filter(|&&idx| idx < span as i32 && idx >= 0)
+            .copied()
+            .collect::<Vec<_>>();
+        let kept = if kept.is_empty() || kept.len() == span {
+            None
+        } else {
+            Some(Arc::from(kept))
+        };
+
+        Self {
+            kept,
+            span: span as i32,
+        }
+    }
+
+    // shortened version for a full keep set
+    pub(crate) fn new_full(span: usize) -> Self {
+        assert!(
+            i32::try_from(span).is_ok(),
+            "Keep cannot have a span that exceeds the storage of an i32: got 
{span}"
+        );
+        Self {
+            kept: None,
+            span: span as i32,
+        }
+    }
+
+    /// Retrieve a position if set
+    fn position(&self, idx: usize) -> Option<usize> {
+        let needle = i32::try_from(idx).ok()?;
+        // below CUTOFF elements, use linear search
+        const CUTOFF: usize = 32;
+        match self.kept.as_ref() {
+            None => (idx < self.span as usize).then_some(idx),
+            Some(k) if k.len() > CUTOFF => k.binary_search(&needle).ok(),
+            Some(k) => k.iter().position(|&i| i == needle),
+        }
+    }
+
+    /// Returns the number of set positions
+    fn len(&self) -> usize {
+        match &self.kept {
+            None => self.span as usize,
+            Some(indexes) => indexes.len(),
+        }
+    }
+}
+
+impl HeapSize for Arc<[i32]> {
+    fn heap_size(&self) -> usize {
+        // Arc stores weak and strong counts on the heap alongside an instance 
of T
+        // T = [i32], so that should be the size of a pointer + the size of 
the allocation
+        2 * std::mem::size_of::<usize>()
+            + std::mem::size_of::<*mut i32>()
+            + std::mem::size_of_val(self.as_ref())
+    }
+}
+
+impl HeapSize for Keep {
+    fn heap_size(&self) -> usize {
+        self.kept.heap_size()
+    }
+}
+
+/// A memory-efficient 2D sparse grid using Keep structures for rows and 
columns
+///
+/// Maps (row_group_idx, column_idx) to values efficiently for sparse access 
patterns.
+/// This is particularly useful when only a few columns are accessed from wide 
schemas.
+#[derive(Debug, Clone, PartialEq)]
+pub(crate) struct Grid<T> {
+    /// Set of row group indexes that have any values
+    rows: Keep,
+    /// Set of column indexes that have any values
+    cols: Keep,
+    /// Flattened cells stored in row-major order
+    /// cells[row_offset * cols.len() + col_offset] = value at (row, col)
+    /// where row_offset = position of row in rows.kept
+    /// and col_offset = position of col in cols.kept
+    cells: Vec<Option<T>>,
+}
+
+impl<T: Clone> Grid<T> {
+    /// Creates a new empty Grid with the specified dimensions
+    fn new(rows: Keep, cols: Keep) -> Self {
+        let size = rows.len() * cols.len();
+        let cells = vec![None; size];
+        Self { rows, cols, cells }
+    }

Review Comment:
   **Nit (D10).** `Clone` is necessary only for `vec![None; size]`. 
`resize_with` keeps the allocation exact and removes the bound. Then also 
remove `T: Clone` from `storage_for_selection` (L693) and `is_empty_index` 
(L816). Tested with `cargo clippy -p parquet --features arrow,async --lib 
--tests -- -D warnings`.
   
   ```suggestion
   impl<T> Grid<T> {
       /// Creates a new empty Grid with the specified dimensions
       fn new(rows: Keep, cols: Keep) -> Self {
           let size = rows.len() * cols.len();
           let mut cells = Vec::with_capacity(size);
           cells.resize_with(size, || None);
           Self { rows, cols, cells }
       }
   ```



##########
parquet/src/file/metadata/page_index.rs:
##########
@@ -363,6 +365,183 @@ impl RowGroupPageIndex {
     }
 }
 
+/// A memory-efficient sparse set representation storing sorted deduplicated 
indexes
+///
+/// Stores which positions are set in a sparse vector. For example:
+/// `[None, None, Some(...), Some(...), None, Some(...)]` becomes `[2, 3, 5]`
+///
+/// Position checking uses binary search for O(log n) lookup.
+///
+/// Implementation note: we can downsize to `i32` here because thrift encodes 
vector
+/// sizes with an `i32`.
+#[derive(Debug, Clone, PartialEq)]
+pub(crate) struct Keep {
+    /// Sorted, deduplicated indexes of set positions
+    /// None means all positions in the span are set
+    kept: Option<Arc<[i32]>>,
+    /// Total span of positions (0..span)
+    span: i32,
+}
+
+impl Keep {
+    // an empty set or a set whose size equals "span" will consider all 
positions in the
+    // span are set.
+    pub(crate) fn new(set: &BTreeSet<i32>, span: usize) -> Self {
+        assert!(
+            i32::try_from(span).is_ok(),
+            "Keep cannot have a span that exceeds the storage of an i32: got 
{span}"
+        );
+        // this should preserve the BTreeSet ordering
+        let kept = set
+            .iter()
+            .filter(|&&idx| idx < span as i32 && idx >= 0)
+            .copied()
+            .collect::<Vec<_>>();
+        let kept = if kept.is_empty() || kept.len() == span {
+            None
+        } else {
+            Some(Arc::from(kept))
+        };
+
+        Self {
+            kept,
+            span: span as i32,
+        }
+    }
+
+    // shortened version for a full keep set
+    pub(crate) fn new_full(span: usize) -> Self {
+        assert!(
+            i32::try_from(span).is_ok(),
+            "Keep cannot have a span that exceeds the storage of an i32: got 
{span}"
+        );
+        Self {
+            kept: None,
+            span: span as i32,
+        }
+    }
+
+    /// Retrieve a position if set
+    fn position(&self, idx: usize) -> Option<usize> {
+        let needle = i32::try_from(idx).ok()?;
+        // below CUTOFF elements, use linear search
+        const CUTOFF: usize = 32;
+        match self.kept.as_ref() {
+            None => (idx < self.span as usize).then_some(idx),
+            Some(k) if k.len() > CUTOFF => k.binary_search(&needle).ok(),
+            Some(k) => k.iter().position(|&i| i == needle),
+        }
+    }
+
+    /// Returns the number of set positions
+    fn len(&self) -> usize {
+        match &self.kept {
+            None => self.span as usize,
+            Some(indexes) => indexes.len(),
+        }
+    }
+}
+
+impl HeapSize for Arc<[i32]> {
+    fn heap_size(&self) -> usize {
+        // Arc stores weak and strong counts on the heap alongside an instance 
of T
+        // T = [i32], so that should be the size of a pointer + the size of 
the allocation
+        2 * std::mem::size_of::<usize>()
+            + std::mem::size_of::<*mut i32>()
+            + std::mem::size_of_val(self.as_ref())
+    }
+}
+
+impl HeapSize for Keep {
+    fn heap_size(&self) -> usize {
+        self.kept.heap_size()
+    }
+}
+
+/// A memory-efficient 2D sparse grid using Keep structures for rows and 
columns
+///
+/// Maps (row_group_idx, column_idx) to values efficiently for sparse access 
patterns.
+/// This is particularly useful when only a few columns are accessed from wide 
schemas.
+#[derive(Debug, Clone, PartialEq)]
+pub(crate) struct Grid<T> {
+    /// Set of row group indexes that have any values
+    rows: Keep,
+    /// Set of column indexes that have any values
+    cols: Keep,
+    /// Flattened cells stored in row-major order
+    /// cells[row_offset * cols.len() + col_offset] = value at (row, col)
+    /// where row_offset = position of row in rows.kept
+    /// and col_offset = position of col in cols.kept
+    cells: Vec<Option<T>>,
+}
+
+impl<T: Clone> Grid<T> {
+    /// Creates a new empty Grid with the specified dimensions
+    fn new(rows: Keep, cols: Keep) -> Self {
+        let size = rows.len() * cols.len();
+        let cells = vec![None; size];
+        Self { rows, cols, cells }
+    }
+
+    pub(crate) fn new_dense(num_row_groups: usize, num_columns: usize) -> Self 
{
+        let rows = Keep::new_full(num_row_groups);
+        let cols = Keep::new_full(num_columns);
+        Self::new(rows, cols)
+    }
+
+    pub(crate) fn from_vec(index: Vec<Vec<Option<T>>>) -> Self {
+        let num_row_groups = index.len();
+        let num_columns = if index.is_empty() { 0 } else { index[0].len() };
+        let mut result = Self::new_dense(num_row_groups, num_columns);
+        for (rg_idx, row_group) in index.into_iter().enumerate() {
+            for (col_idx, idx) in row_group.into_iter().enumerate() {
+                if let Some(idx) = idx {
+                    result.insert(rg_idx, col_idx, idx);
+                }
+            }
+        }
+
+        result
+    }
+
+    /// Gets a value at the specified row and column
+    pub(crate) fn get(&self, row: usize, col: usize) -> Option<&T> {
+        // Find the offset of this row in the kept rows
+        let row_offset = self.rows.position(row)?;
+        let col_offset = self.cols.position(col)?;
+
+        let index = row_offset * self.cols.len() + col_offset;
+        self.cells.get(index)?.as_ref()
+    }
+
+    /// Sets a value at the specified row and column
+    pub(crate) fn insert(&mut self, row: usize, col: usize, value: T) {
+        let row_offset = self.rows.position(row);
+        let col_offset = self.cols.position(col);
+        if let Some(row_offset) = row_offset
+            && let Some(col_offset) = col_offset
+        {
+            // update the existing cell
+            let index = row_offset * self.cols.len() + col_offset;
+
+            if index < self.cells.len() {
+                self.cells[index] = Some(value);
+            }
+        }
+    }
+
+    /// Returns true if the grid has no values
+    pub(crate) fn is_empty(&self) -> bool {
+        self.cells.iter().all(|cell| cell.is_none())
+    }
+}
+
+impl<T: HeapSize> HeapSize for Grid<T> {
+    fn heap_size(&self) -> usize {
+        self.rows.heap_size() + self.cols.heap_size() + self.cells.heap_size()
+    }
+}
+
 /// Struct to encapsulate the Parquet [Page Index]
 ///
 /// This struct provides a dense representation of the Page Index. It is

Review Comment:
   **Nit (D10).** The storage is no longer dense.
   
   ```suggestion
   /// This struct provides a sparse representation of the Page Index: it has 
storage only for
   /// the column chunks selected when it was built (all chunks by default). It 
is
   ```



##########
parquet/src/file/metadata/page_index.rs:
##########
@@ -605,11 +792,8 @@ impl PageIndexBuilder {
         row_group_idx: usize,
         column_idx: usize,
     ) {
-        if let Some(ref mut indexes) = self.column_indexes
-            && let Some(row_group) = indexes.get_mut(row_group_idx)
-            && let Some(column_slot) = row_group.get_mut(column_idx)
-        {
-            *column_slot = Some(column_index);
+        if let Some(ref mut indexes) = self.column_indexes {
+            indexes.insert(row_group_idx, column_idx, column_index);
         }

Review Comment:
   **Footgun (D4).** `put_*` drops a value outside the mask without a signal. 
This also happens after `into_builder()` on a masked index, because the builder 
keeps the fixed shape:
   
   ```text
   new_with_mask(2, 3, columns([0]), columns([0])) + put_offset_index(oi, 0, 1) 
-> offset_index(0, 1) = None
   masked_index.into_builder()                    + put_column_index(ci, 1, 2) 
-> column_index(1, 2) = None
   ```
   
   The doc at L787-788 lists only "not allocated" and "out of bounds". Also, 
`allocate_*` (L765-783) replaces a masked grid with a dense one and drops its 
cells. The doc does not say this.
   
   Options: document the three cases, or return `bool` so that a caller can see 
the drop. The `bool` version (tested, clippy clean) also removes the `index < 
self.cells.len()` check at L527, which cargo-mutants shows is dead code:
   
   ```rust
   // Grid
       /// Returns `false`, and drops `value`, if the grid has no storage for 
the position
       pub(crate) fn insert(&mut self, row: usize, col: usize, value: T) -> 
bool {
           let (Some(row_offset), Some(col_offset)) =
               (self.rows.position(row), self.cols.position(col))
           else {
               return false;
           };
           self.cells[row_offset * self.cols.len() + col_offset] = Some(value);
           true
       }
   
   // PageIndexBuilder::put_column_index (put_offset_index is the same)
       ) -> bool {
           self.column_indexes
               .as_mut()
               .is_some_and(|indexes| indexes.insert(row_group_idx, column_idx, 
column_index))
       }
   ```
   
   `()` → `bool` on these 60.0.0 methods is a small API change.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to