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}