Skip to main content

parquet/file/metadata/
page_index.rs

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