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}