Skip to main content

parquet/file/metadata/
page_index.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: See the NOTICE file distributed with this work for additional information regarding copyright ownership
3// SPDX-FileContributor: Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements
4
5//! Page Index structures for efficient page-level skipping
6
7use crate::file::metadata::memory::HeapSize;
8use crate::file::page_index::{
9    column_index::ColumnIndexMetaData,
10    offset_index::{OffsetIndexMetaData, PageLocation},
11};
12use std::sync::Arc;
13
14/// Trait for accessing Parquet [Page Index] data for efficient page-level skipping
15///
16/// The [Page Index] enables query engines to skip irrelevant data pages during scans,
17/// significantly improving I/O efficiency. It provides access to two complementary
18/// structures:
19///
20/// * **[`ColumnIndex`]**: Per-page min/max value boundaries that enable predicate-based
21///   page filtering. Allows determining which pages might contain rows matching a query
22///   predicate without reading the actual data pages.
23///
24/// * **[`OffsetIndex`]**: Physical locations and sizes of data pages, plus the first row
25///   index of each page. Used to locate and read only the pages identified as relevant
26///   by the ColumnIndex.
27///
28/// Together, these indexes enable:
29/// - Single-row lookups reading only one data page per column (on sorted columns)
30/// - Range scans reading only pages containing values in the query range
31/// - Efficient cross-column filtering by skipping corresponding row ranges
32///
33/// # Structure
34///
35/// Within a Parquet file, both indexes are organized as a two-level structure, with
36/// indexes arranged first by row group, and then column. The [`ColumnChunkMetaData`]
37/// contains pointers to the indexes for a given column chunk, so they may be
38/// populated piecemeal. This trait allows access by row group index and column number
39/// ([Self::column_index], [Self::offset_index]). Access by row group is provided by
40/// [`RowGroupPageIndex`].
41///
42/// Each entry is `Option<T>` because:
43/// - The entire page index might be absent (old files, disabled during write)
44/// - Individual columns might lack indexes (unsupported types, statistics disabled)
45///
46/// # Example: Checking if Page Index is Available
47///
48/// ```
49/// use parquet::file::metadata::ParquetMetaData;
50/// # use parquet::errors::Result;
51///
52/// fn check_page_index_availability(metadata: &ParquetMetaData) -> Result<()> {
53///     if let Some(page_index) = metadata.page_index() {
54///         println!("Page index present:");
55///         println!("  Has offset indexes: {}", page_index.has_offset_indexes());
56///         println!("  Has column indexes: {}", page_index.has_column_indexes());
57///
58///         // Check availability for first row group, first column
59///         if let Some(col_idx) = page_index.column_index(0, 0) {
60///             println!("  Column index found for row group 0, column 0");
61///             println!("    Number of pages: {}", col_idx.num_pages());
62///         }
63///
64///         if let Some(offset_idx) = page_index.offset_index(0, 0) {
65///             println!("  Offset index found for row group 0, column 0");
66///             println!("    Number of pages: {}", offset_idx.page_locations().len());
67///         }
68///     } else {
69///         println!("No page index available");
70///     }
71///     Ok(())
72/// }
73/// ```
74///
75/// # Example: Using Page Index for Predicate Pushdown
76///
77/// ```
78/// use parquet::file::metadata::ParquetMetaData;
79/// use parquet::file::page_index::column_index::ColumnIndexMetaData;
80/// # use parquet::errors::Result;
81///
82/// /// Identifies which pages in a column might contain values >= min_value
83/// fn find_relevant_pages(
84///     metadata: &ParquetMetaData,
85///     row_group_idx: usize,
86///     column_idx: usize,
87///     min_value: i32,
88/// ) -> Vec<usize> {
89///     let mut relevant_pages = Vec::new();
90///
91///     let Some(page_index) = metadata.page_index() else {
92///         // No page index - must read all pages
93///         return relevant_pages;
94///     };
95///
96///     let Some(column_index) = page_index.column_index(row_group_idx, column_idx) else {
97///         // No column index - must read all pages
98///         return relevant_pages;
99///     };
100///
101///     // Check each page's statistics
102///     match column_index {
103///         ColumnIndexMetaData::INT32(index) => {
104///             for (page_num, max_value) in index.max_values_iter().enumerate() {
105///                 // Page might contain matching rows if its max >= our min
106///                 if let Some(max) = max_value {
107///                     if *max >= min_value {
108///                         relevant_pages.push(page_num);
109///                     }
110///                 }
111///             }
112///         }
113///         _ => {
114///             // Wrong column type - read all pages
115///         }
116///     }
117///
118///     relevant_pages
119/// }
120/// ```
121///
122/// [Page Index]: https://parquet.apache.org/docs/file-format/pageindex/
123/// [`ColumnIndex`]: crate::file::page_index::column_index::ColumnIndexMetaData
124/// [`OffsetIndex`]: crate::file::page_index::offset_index::OffsetIndexMetaData
125/// [`ColumnChunkMetaData`]: crate::file::metadata::ColumnChunkMetaData
126pub trait PageIndexProvider: Send + Sync + std::fmt::Debug {
127    /// Returns `true` if offset index structures are available via this provider
128    ///
129    /// This indicates whether [`OffsetIndexMetaData`] structures were loaded or created.
130    /// Returns `true` even if some individual columns lack offset indexes.
131    /// This should return `false` if all calls to [`Self::offset_index`] will return `None`.
132    ///
133    /// This does *not* indicate if the underlying Parquet file contains offset indexes.
134    ///
135    /// To check if a specific column has an offset index, use [`Self::offset_index`].
136    fn has_offset_indexes(&self) -> bool;
137
138    /// Returns `true` if column index structures are available via this provider
139    ///
140    /// This indicates whether [`ColumnIndexMetaData`] structures were loaded or created.
141    /// Returns `true` even if some individual columns lack column indexes.
142    /// This should return `false` if all calls to [`Self::column_index`] will return `None`.
143    ///
144    /// This does *not* indicate if the underlying Parquet file contains column indexes.
145    ///
146    /// To check if a specific column has a column index, use [`Self::column_index`].
147    fn has_column_indexes(&self) -> bool;
148
149    /// Returns `true` if both the offset and column index structures are present
150    ///
151    /// This is equivalent to both [`Self::has_offset_indexes`] and [`Self::has_column_indexes`]
152    /// returning `true`.
153    fn is_complete(&self) -> bool {
154        self.has_column_indexes() && self.has_offset_indexes()
155    }
156
157    /// Returns the column index for a specific row group and column
158    ///
159    /// This is the primary method for accessing page-level min/max statistics
160    /// used in predicate pushdown and page skipping optimizations.
161    ///
162    /// Returns:
163    /// * `Some(&ColumnIndexMetaData)` - Column index is available with statistics
164    /// * `None` - Index unavailable (not loaded, row group/column out of bounds, or no statistics)
165    ///
166    /// For access to the indexes for a specific row group, use [`RowGroupPageIndex`].
167    fn column_index(&self, row_group_idx: usize, column_idx: usize)
168    -> Option<&ColumnIndexMetaData>;
169
170    /// Returns the offset index for a specific row group and column
171    ///
172    /// This provides physical locations and sizes of data pages, enabling:
173    /// - Direct seeking to specific pages identified by column index filtering
174    /// - Reading only relevant pages without scanning entire column chunks
175    /// - Efficient cross-column row-based filtering
176    ///
177    /// Returns:
178    /// * `Some(&OffsetIndexMetaData)` - Offset index is available
179    /// * `None` - Index unavailable (not loaded, row group/column out of bounds)
180    ///
181    /// For access to the indexes for a specific row group, use [`RowGroupPageIndex`].
182    fn offset_index(&self, row_group_idx: usize, column_idx: usize)
183    -> Option<&OffsetIndexMetaData>;
184
185    /// Returns the expected number of data pages for a specific column chunk
186    ///
187    /// This count includes only data pages, not dictionary pages or other metadata pages.
188    ///
189    /// Returns:
190    /// * `Some(usize)` - Number of data pages if any index is available
191    /// * `None` - No index information available for this column
192    fn num_data_pages(&self, row_group_idx: usize, column_idx: usize) -> Option<usize> {
193        match self.offset_index(row_group_idx, column_idx) {
194            Some(offset_index) => Some(offset_index.page_locations.len()),
195            None => Some(self.column_index(row_group_idx, column_idx)?.num_pages() as usize),
196        }
197    }
198
199    /// Returns the physical locations of all data pages in a column chunk
200    ///
201    /// Each [`PageLocation`] contains:
202    /// - File offset where the page begins
203    /// - Compressed size of the page
204    /// - First row index within the row group
205    ///
206    /// This enables direct I/O to specific pages without reading the entire column chunk.
207    ///
208    /// Returns:
209    /// * `Some(&Vec<PageLocation>)` - Vector of page locations if offset index exists
210    /// * `None` - Offset index not available
211    fn page_locations(
212        &self,
213        row_group_idx: usize,
214        column_idx: usize,
215    ) -> Option<&Vec<PageLocation>> {
216        Some(
217            self.offset_index(row_group_idx, column_idx)?
218                .page_locations(),
219        )
220    }
221
222    /// Returns a reference to the trait object as `&dyn Any` for downcasting
223    ///
224    /// This allows downcasting to concrete types when needed (e.g., for serialization)
225    fn as_any(&self) -> &dyn std::any::Any;
226}
227
228/// Provides convenient access to page index data for a specific row group
229///
230/// This struct wraps a [`PageIndexProvider`] and automatically applies the row group
231/// index, simplifying access to column and offset indexes for a single row group.
232/// It is primarily used by readers to avoid repeatedly passing the row group index
233/// when accessing page-level metadata.
234///
235/// # Example
236///
237/// ```
238/// use parquet::file::metadata::ParquetMetaData;
239/// # use parquet::errors::Result;
240///
241/// fn process_row_group_pages(metadata: &ParquetMetaData, row_group_idx: usize) -> Result<()> {
242///     // Create a row-group-specific view of the page index
243///     let rg_page_index = metadata.page_index_for_row_group(row_group_idx);
244///
245///     // Now access column indexes without specifying row_group_idx each time
246///     for col_idx in 0..metadata.file_metadata().schema_descr().num_columns() {
247///         if let Some(col_idx_data) = rg_page_index.column_index(col_idx) {
248///             println!("Column {} has {} pages", col_idx, col_idx_data.num_pages());
249///         }
250///     }
251///     Ok(())
252/// }
253/// ```
254#[derive(Debug)]
255pub struct RowGroupPageIndex {
256    row_group_idx: usize,
257    page_index: Option<Arc<dyn PageIndexProvider>>,
258}
259
260impl RowGroupPageIndex {
261    /// Creates a new [`RowGroupPageIndex`] for the specified row group
262    ///
263    /// # Arguments
264    ///
265    /// * `row_group_idx` - The index of the row group within the file
266    /// * `page_index` - Optional page index provider containing the index data
267    pub fn new(row_group_idx: usize, page_index: Option<Arc<dyn PageIndexProvider>>) -> Self {
268        Self {
269            row_group_idx,
270            page_index,
271        }
272    }
273
274    /// Returns the column index for a specific column in this row group
275    ///
276    /// This is a convenience method that wraps [`PageIndexProvider::column_index`],
277    /// automatically applying the row group index stored in this struct.
278    ///
279    /// # Returns
280    ///
281    /// * `Some(&ColumnIndexMetaData)` - Column index is available with page-level statistics
282    /// * `None` - Index unavailable (no page index, column out of bounds, or no statistics)
283    ///
284    /// # See Also
285    ///
286    /// * [`PageIndexProvider::column_index`] for more details on column indexes
287    pub fn column_index(&self, column_idx: usize) -> Option<&ColumnIndexMetaData> {
288        self.page_index
289            .as_ref()?
290            .column_index(self.row_group_idx, column_idx)
291    }
292
293    /// Returns the offset index for a specific column in this row group
294    ///
295    /// This is a convenience method that wraps [`PageIndexProvider::offset_index`],
296    /// automatically applying the row group index stored in this struct.
297    ///
298    /// # Returns
299    ///
300    /// * `Some(&OffsetIndexMetaData)` - Offset index is available with page locations
301    /// * `None` - Index unavailable (no page index, column out of bounds)
302    ///
303    /// # See Also
304    ///
305    /// * [`PageIndexProvider::offset_index`] for more details on offset indexes
306    pub fn offset_index(&self, column_idx: usize) -> Option<&OffsetIndexMetaData> {
307        self.page_index
308            .as_ref()?
309            .offset_index(self.row_group_idx, column_idx)
310    }
311
312    /// Returns the physical locations of all data pages for a specific column in this row group
313    ///
314    /// This is a convenience method that wraps [`PageIndexProvider::page_locations`],
315    /// automatically applying the row group index stored in this struct.
316    ///
317    /// This enables direct I/O to specific pages without reading the entire column chunk.
318    ///
319    /// # Returns
320    ///
321    /// * `Some(&Vec<PageLocation>)` - Vector of page locations if offset index exists
322    /// * `None` - Offset index not available for this column
323    ///
324    /// # See Also
325    ///
326    /// * [`PageIndexProvider::page_locations`] for more details on page locations
327    pub fn page_locations(&self, column_idx: usize) -> Option<&Vec<PageLocation>> {
328        Some(self.offset_index(column_idx)?.page_locations())
329    }
330
331    /// Returns the expected number of data pages for a specific column in this row group
332    ///
333    /// This count includes only data pages, not dictionary pages or other metadata pages.
334    ///
335    /// This is a convenience method that wraps [`PageIndexProvider::num_data_pages`],
336    /// automatically applying the row group index stored in this struct.
337    ///
338    /// # Returns
339    ///
340    /// * `Some(usize)` - Number of data pages if any index is available
341    /// * `None` - No index information available for this column
342    ///
343    /// # See Also
344    ///
345    /// * [`PageIndexProvider::num_data_pages`] for more details
346    pub fn num_data_pages(&self, column_idx: usize) -> Option<usize> {
347        self.page_index
348            .as_ref()?
349            .num_data_pages(self.row_group_idx, column_idx)
350    }
351}
352
353/// Struct to encapsulate the Parquet [Page Index]
354///
355/// This struct provides a dense representation of the Page Index. It is
356/// used internally by this crate when assembling and writing the Page
357/// Index. It is also the default implementation of the [`PageIndexProvider`]
358/// contained in the [`ParquetMetaData`].
359///
360/// # Example: Constructing a synthetic `PageIndex`
361///
362/// This example builds a [`ParquetMetaData`] for a file with a single row
363/// group containing a single `BYTE_ARRAY` column with one data page, and
364/// attaches a matching `PageIndex`, as might be done in tests that
365/// exercise page-level statistics handling.
366///
367/// ```
368/// # use std::sync::Arc;
369/// # use parquet::basic::{BoundaryOrder, Type as PhysicalType};
370/// # use parquet::file::metadata::{
371/// #     ColumnChunkMetaData, ColumnIndexBuilder, FileMetaData, OffsetIndexBuilder,
372/// #     ParquetMetaData, RowGroupMetaData,
373/// # };
374/// # use parquet::file::metadata::page_index::PageIndexBuilder;
375/// # use parquet::schema::types::{SchemaDescriptor, Type};
376/// // Create metadata for a file with a single row group containing a
377/// // single BYTE_ARRAY column "s" with three values
378/// # let schema = Arc::new(SchemaDescriptor::new(Arc::new(
379/// #     Type::group_type_builder("schema")
380/// #         .with_fields(vec![Arc::new(
381/// #             Type::primitive_type_builder("s", PhysicalType::BYTE_ARRAY)
382/// #                 .build()
383/// #                 .unwrap(),
384/// #         )])
385/// #         .build()
386/// #         .unwrap(),
387/// # )));
388/// # let column = ColumnChunkMetaData::builder(schema.column(0))
389/// #     .set_num_values(3)
390/// #     .build()
391/// #     .unwrap();
392/// # let row_group = RowGroupMetaData::builder(Arc::clone(&schema))
393/// #     .set_num_rows(3)
394/// #     .set_column_metadata(vec![column])
395/// #     .build()
396/// #     .unwrap();
397/// let file_metadata = FileMetaData::new(1, 3, None, None, schema, None);
398/// let metadata = ParquetMetaData::new(file_metadata, vec![row_group]);
399///
400/// // Build a column index with min/max statistics for the single page
401/// let mut column_index = ColumnIndexBuilder::new(PhysicalType::BYTE_ARRAY);
402/// column_index.append(false, b"az".to_vec(), b"b".to_vec(), 0, None);
403/// column_index.set_boundary_order(BoundaryOrder::ASCENDING);
404/// let column_index = column_index.build().unwrap();
405///
406/// // Build an offset index recording the location of the single page
407/// let mut offset_index = OffsetIndexBuilder::new();
408/// offset_index.append_row_count(3);
409/// offset_index.append_offset_and_size(4, 100);
410/// let offset_index = offset_index.build();
411///
412/// // Assemble the PageIndex (one entry per row group, each with one
413/// // entry per column) and attach it to the metadata
414/// let mut page_index = PageIndexBuilder::new(1, 1);
415/// page_index.put_column_index(column_index, 0, 0);
416/// page_index.put_offset_index(offset_index, 0, 0);
417/// let page_index = page_index.build();
418/// let metadata = metadata
419///     .into_builder()
420///     .set_page_index(Some(Arc::new(page_index)))
421///     .build();
422/// assert!(metadata.page_index().unwrap().is_complete());
423/// ```
424///
425/// [Page Index]: https://parquet.apache.org/docs/file-format/pageindex/
426/// [`ColumnIndex`]: crate::file::page_index::column_index::ColumnIndexMetaData
427/// [`OffsetIndex`]: crate::file::page_index::offset_index::OffsetIndexMetaData
428/// [`ParquetMetaData`]: crate::file::metadata::ParquetMetaData
429#[derive(Debug, Clone, PartialEq)]
430pub struct PageIndex {
431    column_indexes: Option<Vec<Vec<Option<ColumnIndexMetaData>>>>,
432    offset_indexes: Option<Vec<Vec<Option<OffsetIndexMetaData>>>>,
433}
434
435impl PageIndex {
436    pub(crate) fn new(
437        column_indexes: Option<Vec<Vec<Option<ColumnIndexMetaData>>>>,
438        offset_indexes: Option<Vec<Vec<Option<OffsetIndexMetaData>>>>,
439    ) -> Self {
440        Self {
441            column_indexes,
442            offset_indexes,
443        }
444    }
445
446    /// Convert this `PageIndex` into a [`PageIndexBuilder`]
447    pub fn into_builder(self) -> PageIndexBuilder {
448        self.into()
449    }
450}
451
452impl PageIndexProvider for PageIndex {
453    fn has_offset_indexes(&self) -> bool {
454        self.offset_indexes.is_some()
455    }
456
457    fn has_column_indexes(&self) -> bool {
458        self.column_indexes.is_some()
459    }
460
461    fn column_index(
462        &self,
463        row_group_idx: usize,
464        column_idx: usize,
465    ) -> Option<&ColumnIndexMetaData> {
466        let rg = self.column_indexes.as_ref()?.get(row_group_idx)?;
467        rg.get(column_idx)?.as_ref()
468    }
469
470    fn offset_index(
471        &self,
472        row_group_idx: usize,
473        column_idx: usize,
474    ) -> Option<&OffsetIndexMetaData> {
475        let rg = self.offset_indexes.as_ref()?.get(row_group_idx)?;
476        rg.get(column_idx)?.as_ref()
477    }
478
479    fn as_any(&self) -> &dyn std::any::Any {
480        self
481    }
482}
483
484impl HeapSize for PageIndex {
485    fn heap_size(&self) -> usize {
486        self.column_indexes.heap_size() + self.offset_indexes.heap_size()
487    }
488}
489
490/// Builder for constructing [`PageIndex`] structures
491///
492/// It supports:
493/// - Populating column indexes for predicate columns (for page filtering)
494/// - Populating offset indexes for projected columns (for direct I/O)
495/// - Automatic conversion of empty structures to `None` to save memory
496#[derive(Default)]
497pub struct PageIndexBuilder {
498    column_indexes: Option<Vec<Vec<Option<ColumnIndexMetaData>>>>,
499    offset_indexes: Option<Vec<Vec<Option<OffsetIndexMetaData>>>>,
500}
501
502impl PageIndexBuilder {
503    /// Creates an empty index structure with space for the specified number of row groups and columns
504    ///
505    /// Returns `Some` containing a nested vector structure where all entries are initialized to `None`.
506    /// The outer vector has one entry per row group, and each inner vector has one entry per column.
507    ///
508    /// # Type Parameters
509    /// * `T` - The type of index this is to be, either `ColumnIndexMetaData` or `OffsetIndexMetaData`
510    fn empty_index<T>(num_row_groups: usize, num_columns: usize) -> Option<Vec<Vec<Option<T>>>> {
511        Some(
512            (0..num_row_groups)
513                .map(|_| {
514                    let mut idx = Vec::with_capacity(num_columns);
515                    idx.resize_with(num_columns, || None);
516                    idx
517                })
518                .collect(),
519        )
520    }
521
522    /// Creates a new [`PageIndexBuilder`] with space allocated for both column and offset indexes
523    ///
524    /// This allocates empty index structures for the specified number of row groups and columns.
525    /// All index entries are initialized to `None` and can be populated using
526    /// [`put_column_index`](Self::put_column_index) and [`put_offset_index`](Self::put_offset_index).
527    pub fn new(num_row_groups: usize, num_columns: usize) -> Self {
528        Self {
529            column_indexes: Self::empty_index(num_row_groups, num_columns),
530            offset_indexes: Self::empty_index(num_row_groups, num_columns),
531        }
532    }
533
534    /// Creates a new [`PageIndexBuilder`] from an existing [`PageIndex`]
535    ///
536    /// This takes ownership of the index structures from the provided [`PageIndex`],
537    /// allowing them to be modified and rebuilt. Useful for updating existing page indexes.
538    pub(crate) fn new_from(page_index: PageIndex) -> Self {
539        Self {
540            column_indexes: page_index.column_indexes,
541            offset_indexes: page_index.offset_indexes,
542        }
543    }
544
545    /// Allocates space for column indexes
546    ///
547    /// This allocates an empty index structure for the specified number of row groups and columns.
548    /// All index entries are initialized to `None` and can be populated using
549    /// [`put_column_index`](Self::put_column_index).
550    ///
551    /// This can be used to add column index storage to a builder that lacks one
552    /// (either a `Default` builder, or one created from a [`PageIndex`] without column indexes).
553    pub fn allocate_column_indexes(&mut self, num_row_groups: usize, num_columns: usize) {
554        self.column_indexes = Self::empty_index(num_row_groups, num_columns);
555    }
556
557    /// Allocates space for offset indexes
558    ///
559    /// This allocates an empty index structure for the specified number of row groups and columns.
560    /// All index entries are initialized to `None` and can be populated using
561    /// [`put_offset_index`](Self::put_offset_index).
562    ///
563    /// This can be used to add offset index storage to a builder that lacks one
564    /// (either a `Default` builder, or one created from a [`PageIndex`] without offset indexes).
565    pub fn allocate_offset_indexes(&mut self, num_row_groups: usize, num_columns: usize) {
566        self.offset_indexes = Self::empty_index(num_row_groups, num_columns);
567    }
568
569    /// Sets the column index for a specific row group and column
570    ///
571    /// If column indexes were not allocated (see [`Self::allocate_column_indexes`]),
572    /// or the row group or column index is out of bounds, this method does nothing.
573    pub fn put_column_index(
574        &mut self,
575        column_index: ColumnIndexMetaData,
576        row_group_idx: usize,
577        column_idx: usize,
578    ) {
579        if let Some(ref mut indexes) = self.column_indexes
580            && let Some(row_group) = indexes.get_mut(row_group_idx)
581            && let Some(column_slot) = row_group.get_mut(column_idx)
582        {
583            *column_slot = Some(column_index);
584        }
585    }
586
587    /// Sets the offset index for a specific row group and column
588    ///
589    /// If offset indexes were not allocated (see [`Self::allocate_offset_indexes`]),
590    /// or the row group or column index is out of bounds, this method does nothing.
591    pub fn put_offset_index(
592        &mut self,
593        offset_index: OffsetIndexMetaData,
594        row_group_idx: usize,
595        column_idx: usize,
596    ) {
597        if let Some(ref mut indexes) = self.offset_indexes
598            && let Some(row_group) = indexes.get_mut(row_group_idx)
599            && let Some(column_slot) = row_group.get_mut(column_idx)
600        {
601            *column_slot = Some(offset_index);
602        }
603    }
604
605    /// Checks if an index structure is entirely empty (all entries are None)
606    fn is_empty_index<T>(index: Option<&Vec<Vec<Option<T>>>>) -> bool {
607        match index {
608            None => true,
609            Some(row_groups) => row_groups
610                .iter()
611                .all(|columns| columns.iter().all(|entry| entry.is_none())),
612        }
613    }
614
615    /// Consumes the builder and returns a [`PageIndex`]
616    ///
617    /// If an index structure was allocated but remains entirely empty (all entries are `None`),
618    /// it will be converted to `None` in the final [`PageIndex`]. This ensures that:
619    /// - Empty structures don't consume memory unnecessarily
620    /// - [`PageIndex::has_column_indexes()`] and [`PageIndex::has_offset_indexes()`]
621    ///   correctly return `false` for unpopulated indexes
622    pub fn build(self) -> PageIndex {
623        let column_indexes = if Self::is_empty_index(self.column_indexes.as_ref()) {
624            None
625        } else {
626            self.column_indexes
627        };
628
629        let offset_indexes = if Self::is_empty_index(self.offset_indexes.as_ref()) {
630            None
631        } else {
632            self.offset_indexes
633        };
634
635        PageIndex::new(column_indexes, offset_indexes)
636    }
637}
638
639impl From<PageIndex> for PageIndexBuilder {
640    fn from(page_index: PageIndex) -> Self {
641        Self::new_from(page_index)
642    }
643}