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
465impl PageIndexProvider for PageIndex {
466 fn has_offset_indexes(&self) -> bool {
467 self.offset_indexes.is_some()
468 }
469
470 fn has_column_indexes(&self) -> bool {
471 self.column_indexes.is_some()
472 }
473
474 fn column_index(
475 &self,
476 row_group_idx: usize,
477 column_idx: usize,
478 ) -> Option<&ColumnIndexMetaData> {
479 let rg = self.column_indexes.as_ref()?.get(row_group_idx)?;
480 rg.get(column_idx)?.as_ref()
481 }
482
483 fn offset_index(
484 &self,
485 row_group_idx: usize,
486 column_idx: usize,
487 ) -> Option<&OffsetIndexMetaData> {
488 let rg = self.offset_indexes.as_ref()?.get(row_group_idx)?;
489 rg.get(column_idx)?.as_ref()
490 }
491
492 fn as_any(&self) -> &dyn std::any::Any {
493 self
494 }
495}
496
497impl HeapSize for PageIndex {
498 fn heap_size(&self) -> usize {
499 self.column_indexes.heap_size() + self.offset_indexes.heap_size()
500 }
501}
502
503/// Builder for constructing [`PageIndex`] structures
504///
505/// It supports:
506/// - Populating column indexes for predicate columns (for page filtering)
507/// - Populating offset indexes for projected columns (for direct I/O)
508/// - Automatic conversion of empty structures to `None` to save memory
509#[derive(Default)]
510pub struct PageIndexBuilder {
511 column_indexes: Option<Vec<Vec<Option<ColumnIndexMetaData>>>>,
512 offset_indexes: Option<Vec<Vec<Option<OffsetIndexMetaData>>>>,
513}
514
515impl PageIndexBuilder {
516 /// Creates an empty index structure with space for the specified number of row groups and columns
517 ///
518 /// Returns `Some` containing a nested vector structure where all entries are initialized to `None`.
519 /// The outer vector has one entry per row group, and each inner vector has one entry per column.
520 ///
521 /// # Type Parameters
522 /// * `T` - The type of index this is to be, either `ColumnIndexMetaData` or `OffsetIndexMetaData`
523 fn empty_index<T>(num_row_groups: usize, num_columns: usize) -> Option<Vec<Vec<Option<T>>>> {
524 Some(
525 (0..num_row_groups)
526 .map(|_| {
527 let mut idx = Vec::with_capacity(num_columns);
528 idx.resize_with(num_columns, || None);
529 idx
530 })
531 .collect(),
532 )
533 }
534
535 /// Creates a new [`PageIndexBuilder`] with space allocated for both column and offset indexes
536 ///
537 /// This allocates empty index structures for the specified number of row groups and columns.
538 /// All index entries are initialized to `None` and can be populated using
539 /// [`put_column_index`](Self::put_column_index) and [`put_offset_index`](Self::put_offset_index).
540 pub fn new(num_row_groups: usize, num_columns: usize) -> Self {
541 Self {
542 column_indexes: Self::empty_index(num_row_groups, num_columns),
543 offset_indexes: Self::empty_index(num_row_groups, num_columns),
544 }
545 }
546
547 /// Creates a new [`PageIndexBuilder`] from an existing [`PageIndex`]
548 ///
549 /// This takes ownership of the index structures from the provided [`PageIndex`],
550 /// allowing them to be modified and rebuilt. Useful for updating existing page indexes.
551 pub(crate) fn new_from(page_index: PageIndex) -> Self {
552 Self {
553 column_indexes: page_index.column_indexes,
554 offset_indexes: page_index.offset_indexes,
555 }
556 }
557
558 /// Allocates space for column indexes
559 ///
560 /// This allocates an empty index structure for the specified number of row groups and columns.
561 /// All index entries are initialized to `None` and can be populated using
562 /// [`put_column_index`](Self::put_column_index).
563 ///
564 /// This can be used to add column index storage to a builder that lacks one
565 /// (either a `Default` builder, or one created from a [`PageIndex`] without column indexes).
566 pub fn allocate_column_indexes(&mut self, num_row_groups: usize, num_columns: usize) {
567 self.column_indexes = Self::empty_index(num_row_groups, num_columns);
568 }
569
570 /// Allocates space for offset indexes
571 ///
572 /// This allocates an empty index structure for the specified number of row groups and columns.
573 /// All index entries are initialized to `None` and can be populated using
574 /// [`put_offset_index`](Self::put_offset_index).
575 ///
576 /// This can be used to add offset index storage to a builder that lacks one
577 /// (either a `Default` builder, or one created from a [`PageIndex`] without offset indexes).
578 pub fn allocate_offset_indexes(&mut self, num_row_groups: usize, num_columns: usize) {
579 self.offset_indexes = Self::empty_index(num_row_groups, num_columns);
580 }
581
582 /// Sets the column index for a specific row group and column
583 ///
584 /// If column indexes were not allocated (see [`Self::allocate_column_indexes`]),
585 /// or the row group or column index is out of bounds, this method does nothing.
586 pub fn put_column_index(
587 &mut self,
588 column_index: ColumnIndexMetaData,
589 row_group_idx: usize,
590 column_idx: usize,
591 ) {
592 if let Some(ref mut indexes) = self.column_indexes
593 && let Some(row_group) = indexes.get_mut(row_group_idx)
594 && let Some(column_slot) = row_group.get_mut(column_idx)
595 {
596 *column_slot = Some(column_index);
597 }
598 }
599
600 /// Sets the offset index for a specific row group and column
601 ///
602 /// If offset indexes were not allocated (see [`Self::allocate_offset_indexes`]),
603 /// or the row group or column index is out of bounds, this method does nothing.
604 pub fn put_offset_index(
605 &mut self,
606 offset_index: OffsetIndexMetaData,
607 row_group_idx: usize,
608 column_idx: usize,
609 ) {
610 if let Some(ref mut indexes) = self.offset_indexes
611 && let Some(row_group) = indexes.get_mut(row_group_idx)
612 && let Some(column_slot) = row_group.get_mut(column_idx)
613 {
614 *column_slot = Some(offset_index);
615 }
616 }
617
618 /// Checks if an index structure is entirely empty (all entries are None)
619 fn is_empty_index<T>(index: Option<&Vec<Vec<Option<T>>>>) -> bool {
620 match index {
621 None => true,
622 Some(row_groups) => row_groups
623 .iter()
624 .all(|columns| columns.iter().all(|entry| entry.is_none())),
625 }
626 }
627
628 /// Consumes the builder and returns a [`PageIndex`]
629 ///
630 /// If an index structure was allocated but remains entirely empty (all entries are `None`),
631 /// it will be converted to `None` in the final [`PageIndex`]. This ensures that:
632 /// - Empty structures don't consume memory unnecessarily
633 /// - [`PageIndex::has_column_indexes()`] and [`PageIndex::has_offset_indexes()`]
634 /// correctly return `false` for unpopulated indexes
635 pub fn build(self) -> PageIndex {
636 let column_indexes = if Self::is_empty_index(self.column_indexes.as_ref()) {
637 None
638 } else {
639 self.column_indexes
640 };
641
642 let offset_indexes = if Self::is_empty_index(self.offset_indexes.as_ref()) {
643 None
644 } else {
645 self.offset_indexes
646 };
647
648 PageIndex::new(column_indexes, offset_indexes)
649 }
650}
651
652impl From<PageIndex> for PageIndexBuilder {
653 fn from(page_index: PageIndex) -> Self {
654 Self::new_from(page_index)
655 }
656}