This is an automated email from the ASF dual-hosted git repository.
etseidl pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
The following commit(s) were added to refs/heads/main by this push:
new 34060c4120 Allow for serialization of Parquet Page Index via custom
`PageIndexProvider` (#11031)
34060c4120 is described below
commit 34060c412044f891998e7288fb6d7b4e3fc7867d
Author: Ed Seidl <[email protected]>
AuthorDate: Thu Sep 24 13:26:07 2026 -0700
Allow for serialization of Parquet Page Index via custom
`PageIndexProvider` (#11031)
# Which issue does this PR close?
- Closes #11030.
# Rationale for this change
See issue, but allow for serialization of the page indexes through the
`PageIndexProvider` API.
# What changes are included in this PR?
Changes `ThriftMetadataWriter` to operate through the
`PageIndexProvider` API rather than using individual column and offset
index vectors. Removes some now unnecessary `pub(crate)` functions.
# Are these changes tested?
Yes
# Are there any user-facing changes?
All page indexes will now serialize, but no changes to the public API
---
parquet/src/arrow/mod.rs | 116 ++++++++++++++++++++
parquet/src/file/metadata/page_index.rs | 16 ---
parquet/src/file/metadata/writer.rs | 182 ++++++++++++++------------------
parquet/src/file/writer.rs | 74 ++++++++++++-
4 files changed, 266 insertions(+), 122 deletions(-)
diff --git a/parquet/src/arrow/mod.rs b/parquet/src/arrow/mod.rs
index e2ea1b4ca0..e5cbafea5e 100644
--- a/parquet/src/arrow/mod.rs
+++ b/parquet/src/arrow/mod.rs
@@ -595,6 +595,112 @@ mod test {
assert_eq!(original_metadata, roundtrip_metadata);
}
+ #[test]
+ fn test_metadata_read_write_roundtrip_missing_page_index() {
+ let parquet_bytes = create_parquet_file();
+
+ // read the metadata from the file but skip the page indexes
+ let options =
ParquetMetaDataOptions::new().with_encoding_stats_as_mask(false);
+ let original_metadata = ParquetMetaDataReader::new()
+ .with_metadata_options(Some(options))
+ .with_page_index_policy(PageIndexPolicy::Skip)
+ .parse_and_finish(&parquet_bytes)
+ .unwrap();
+
+ // metadata_to_bytes_no_page_idx should zero out the page index
locations. if they aren't
+ // then reading metadata_bytes will fail with EOF
+ let metadata_bytes = metadata_to_bytes_no_page_idx(&original_metadata);
+ let options =
ParquetMetaDataOptions::new().with_encoding_stats_as_mask(false);
+ let roundtrip_metadata = ParquetMetaDataReader::new()
+ .with_metadata_options(Some(options))
+ .with_page_index_policy(PageIndexPolicy::Optional)
+ .parse_and_finish(&metadata_bytes)
+ .expect("page index locations should have been cleared");
+
+ assert!(roundtrip_metadata.page_index().is_none());
+ }
+
+ #[test]
+ fn test_metadata_read_write_roundtrip_offset_index_only() {
+ // `Chunk` statistics: the file has an offset index, but no column
index
+ let array: ArrayRef = Arc::new(Int32Array::from(vec![1, 2, 3]));
+ let batch = RecordBatch::try_from_iter(vec![("id", array)]).unwrap();
+ let props = WriterProperties::builder()
+ .set_statistics_enabled(EnabledStatistics::Chunk)
+ .build();
+ let mut buf = vec![];
+ let mut writer = ArrowWriter::try_new(&mut buf, batch.schema(),
Some(props)).unwrap();
+ writer.write(&batch).unwrap();
+ writer.close().unwrap();
+
+ let read = |bytes: &Bytes| {
+ let options =
ParquetMetaDataOptions::new().with_encoding_stats_as_mask(false);
+ ParquetMetaDataReader::new()
+ .with_metadata_options(Some(options))
+ .with_page_index_policy(PageIndexPolicy::Optional)
+ .parse_and_finish(bytes)
+ .unwrap()
+ };
+ let original = read(&Bytes::from(buf));
+ let page_index = original.page_index().unwrap();
+ assert!(!page_index.has_column_indexes() &&
page_index.has_offset_indexes());
+ let roundtrip = read(&metadata_to_bytes(&original));
+ assert_eq!(
+ normalize_locations(original),
+ normalize_locations(roundtrip)
+ );
+ }
+
+ #[test]
+ fn test_metadata_read_write_roundtrip_custom_page_index() {
+ use crate::file::metadata::page_index::PageIndexProvider;
+ use crate::file::page_index::{
+ column_index::ColumnIndexMetaData,
offset_index::OffsetIndexMetaData,
+ };
+
+ /// A custom provider that forwards to another provider
+ #[derive(Debug)]
+ struct Forward(Arc<dyn PageIndexProvider>);
+ impl PageIndexProvider for Forward {
+ fn has_offset_indexes(&self) -> bool {
+ self.0.has_offset_indexes()
+ }
+ fn has_column_indexes(&self) -> bool {
+ self.0.has_column_indexes()
+ }
+ fn column_index(&self, rg: usize, col: usize) ->
Option<&ColumnIndexMetaData> {
+ self.0.column_index(rg, col)
+ }
+ fn offset_index(&self, rg: usize, col: usize) ->
Option<&OffsetIndexMetaData> {
+ self.0.offset_index(rg, col)
+ }
+ fn as_any(&self) -> &dyn std::any::Any {
+ self
+ }
+ }
+
+ let read = |bytes: &Bytes| {
+ let options =
ParquetMetaDataOptions::new().with_encoding_stats_as_mask(false);
+ ParquetMetaDataReader::new()
+ .with_metadata_options(Some(options))
+ .with_page_index_policy(PageIndexPolicy::Required)
+ .parse_and_finish(bytes)
+ .unwrap()
+ };
+ let original = read(&create_parquet_file());
+ let provider = Forward(original.page_index().unwrap().clone());
+ let custom = original
+ .clone()
+ .into_builder()
+ .set_page_index(Some(Arc::new(provider)))
+ .build();
+ let roundtrip = read(&metadata_to_bytes(&custom));
+ assert_eq!(
+ normalize_locations(original),
+ normalize_locations(roundtrip)
+ );
+ }
+
/// Sets the page index offset locations in the metadata to `None`
///
/// This is because the offsets are used to find the relative location of
the index
@@ -647,6 +753,16 @@ mod test {
Bytes::from(buf)
}
+ // like metadata_to_bytes, but do not preserve page index location info
+ fn metadata_to_bytes_no_page_idx(metadata: &ParquetMetaData) -> Bytes {
+ let mut buf = vec![];
+ ParquetMetaDataWriter::new(&mut buf, metadata)
+ .with_preserve_page_index_locations(false)
+ .finish()
+ .unwrap();
+ Bytes::from(buf)
+ }
+
#[test]
fn test_mask_from_column_names() {
let schema = parse_schema(
diff --git a/parquet/src/file/metadata/page_index.rs
b/parquet/src/file/metadata/page_index.rs
index 9ded57d0a2..78c3dc2792 100644
--- a/parquet/src/file/metadata/page_index.rs
+++ b/parquet/src/file/metadata/page_index.rs
@@ -460,22 +460,6 @@ impl PageIndex {
pub fn into_builder(self) -> PageIndexBuilder {
self.into()
}
-
- /// Returns a reference to the raw column indexes structure
- ///
- /// This method provides access to the underlying column index data for
serialization
- /// and other low-level operations.
- pub(crate) fn column_indexes_raw(&self) ->
Option<&Vec<Vec<Option<ColumnIndexMetaData>>>> {
- self.column_indexes.as_ref()
- }
-
- /// Returns a reference to the raw offset indexes structure
- ///
- /// This method provides access to the underlying offset index data for
serialization
- /// and other low-level operations.
- pub(crate) fn offset_indexes_raw(&self) ->
Option<&Vec<Vec<Option<OffsetIndexMetaData>>>> {
- self.offset_indexes.as_ref()
- }
}
impl PageIndexProvider for PageIndex {
diff --git a/parquet/src/file/metadata/writer.rs
b/parquet/src/file/metadata/writer.rs
index a1708ccf2c..3a00f855bc 100644
--- a/parquet/src/file/metadata/writer.rs
+++ b/parquet/src/file/metadata/writer.rs
@@ -15,8 +15,9 @@
// specific language governing permissions and limitations
// under the License.
+use crate::file::metadata::page_index::PageIndexProvider;
use crate::file::metadata::thrift::FileMeta;
-use crate::file::metadata::{ColumnChunkMetaData, PageIndex, RowGroupMetaData};
+use crate::file::metadata::{ColumnChunkMetaData, RowGroupMetaData};
use crate::schema::types::{SchemaDescPtr, SchemaDescriptor};
use crate::{
basic::ColumnOrder,
@@ -54,13 +55,13 @@ pub(crate) struct ThriftMetadataWriter<'a, W: Write> {
buf: &'a mut TrackedWrite<W>,
schema_descr: &'a SchemaDescPtr,
row_groups: Vec<RowGroupMetaData>,
- column_indexes: Option<Vec<Vec<Option<ColumnIndexMetaData>>>>,
- offset_indexes: Option<Vec<Vec<Option<OffsetIndexMetaData>>>>,
+ page_index: Option<Arc<dyn PageIndexProvider>>,
key_value_metadata: Option<Vec<KeyValue>>,
created_by: Option<String>,
object_writer: MetadataObjectWriter,
writer_version: i32,
write_path_in_schema: bool,
+ preserve_page_index_locations: bool,
}
impl<'a, W: Write> ThriftMetadataWriter<'a, W> {
@@ -69,16 +70,13 @@ impl<'a, W: Write> ThriftMetadataWriter<'a, W> {
/// Note: also updates the `ColumnChunk::offset_index_offset` and
/// `ColumnChunk::offset_index_length` to reflect the position and length
/// of the serialized offset indexes.
- fn write_offset_indexes(
- &mut self,
- offset_indexes: &[Vec<Option<OffsetIndexMetaData>>],
- ) -> Result<()> {
+ fn write_offset_indexes(&mut self, page_index: &dyn PageIndexProvider) ->
Result<()> {
// iter row group
// iter each column
// write offset index to the file
for (row_group_idx, row_group) in
self.row_groups.iter_mut().enumerate() {
for (column_idx, column_metadata) in
row_group.columns.iter_mut().enumerate() {
- if let Some(offset_index) =
&offset_indexes[row_group_idx][column_idx] {
+ if let Some(offset_index) =
page_index.offset_index(row_group_idx, column_idx) {
let start_offset = self.buf.bytes_written();
self.object_writer.write_offset_index(
offset_index,
@@ -91,6 +89,9 @@ impl<'a, W: Write> ThriftMetadataWriter<'a, W> {
// set offset and index for offset index
column_metadata.offset_index_offset = Some(start_offset as
i64);
column_metadata.offset_index_length = Some((end_offset -
start_offset) as i32);
+ } else if !self.preserve_page_index_locations {
+ column_metadata.offset_index_offset = None;
+ column_metadata.offset_index_length = None;
}
}
}
@@ -102,16 +103,13 @@ impl<'a, W: Write> ThriftMetadataWriter<'a, W> {
/// Note: also updates the `ColumnChunk::column_index_offset` and
/// `ColumnChunk::column_index_length` to reflect the position and length
/// of the serialized column indexes.
- fn write_column_indexes(
- &mut self,
- column_indexes: &[Vec<Option<ColumnIndexMetaData>>],
- ) -> Result<()> {
+ fn write_column_indexes(&mut self, page_index: &dyn PageIndexProvider) ->
Result<()> {
// iter row group
// iter each column
// write column index to the file
for (row_group_idx, row_group) in
self.row_groups.iter_mut().enumerate() {
for (column_idx, column_metadata) in
row_group.columns.iter_mut().enumerate() {
- if let Some(column_index) =
&column_indexes[row_group_idx][column_idx] {
+ if let Some(column_index) =
page_index.column_index(row_group_idx, column_idx) {
let start_offset = self.buf.bytes_written();
// only update column_metadata if the write succeeds
if self.object_writer.write_column_index(
@@ -127,61 +125,39 @@ impl<'a, W: Write> ThriftMetadataWriter<'a, W> {
column_metadata.column_index_length =
Some((end_offset - start_offset) as i32);
}
+ } else if !self.preserve_page_index_locations {
+ column_metadata.column_index_offset = None;
+ column_metadata.column_index_length = None;
}
}
}
Ok(())
}
- /// Serialize the column indexes and transform to
`Option<ParquetColumnIndex>`
- fn finalize_column_indexes(&mut self) ->
Result<Option<Vec<Vec<Option<ColumnIndexMetaData>>>>> {
- let column_indexes = std::mem::take(&mut self.column_indexes);
-
- // Write column indexes to file
- if let Some(column_indexes) = column_indexes.as_ref() {
- self.write_column_indexes(column_indexes)?;
- }
-
- // check to see if the index is `None` for every row group and column
chunk
- let all_none = column_indexes
- .as_ref()
- .is_some_and(|ci| ci.iter().all(|cii| cii.iter().all(|idx|
idx.is_none())));
-
- if all_none {
- Ok(None)
- } else {
- Ok(column_indexes)
- }
- }
-
- /// Serialize the offset indexes and transform to
`Option<ParquetOffsetIndex>`
- fn finalize_offset_indexes(&mut self) ->
Result<Option<Vec<Vec<Option<OffsetIndexMetaData>>>>> {
- let offset_indexes = std::mem::take(&mut self.offset_indexes);
-
- // Write offset indexes to file
- if let Some(offset_indexes) = offset_indexes.as_ref() {
- self.write_offset_indexes(offset_indexes)?;
- }
-
- // check to see if the index is `None` for every row group and column
chunk
- let all_none = offset_indexes
- .as_ref()
- .is_some_and(|oi| oi.iter().all(|oii| oii.iter().all(|idx|
idx.is_none())));
-
- if all_none {
- Ok(None)
- } else {
- Ok(offset_indexes)
+ fn clear_page_indexes(&mut self) {
+ for row_group in &mut self.row_groups {
+ for column_metadata in &mut row_group.columns {
+ column_metadata.column_index_offset = None;
+ column_metadata.column_index_length = None;
+ column_metadata.offset_index_offset = None;
+ column_metadata.offset_index_length = None;
+ }
}
}
/// Assembles and writes the final metadata to self.buf
- pub fn finish(mut self) -> Result<ParquetMetaData> {
+ pub(crate) fn finish(mut self) -> Result<ParquetMetaData> {
let num_rows = self.row_groups.iter().map(|x| x.num_rows).sum();
- // serialize page indexes and transform to the proper form for use in
ParquetMetaData
- let column_indexes = self.finalize_column_indexes()?;
- let offset_indexes = self.finalize_offset_indexes()?;
+ // serialize page indexes and update index locations
+ let page_index = self.page_index.take();
+ if let Some(pi) = page_index.as_deref() {
+ self.write_column_indexes(pi)?;
+ self.write_offset_indexes(pi)?;
+ } else if !self.preserve_page_index_locations {
+ // no page indexes at all, then clear index locations
+ self.clear_page_indexes();
+ }
// We only include ColumnOrder for leaf nodes.
let column_orders = self
@@ -249,65 +225,61 @@ impl<'a, W: Write> ThriftMetadataWriter<'a, W> {
self.buf.write_all(&metadata_len.to_le_bytes())?;
self.buf.write_all(self.object_writer.get_file_magic())?;
+ // Note: we simply pass the original reference counted page index
provider to the
+ // builder. Either it came from ParquetMetaDataWriter where it was
cloned, and this
+ // reference will go away when ParquetMetaDataWriter::finish drops the
newly created
+ // metadata, or it was passed an owned version from
SerializedFileWriter which then
+ // takes it back via the returned metadata.
+ let builder =
ParquetMetaDataBuilder::new(file_metadata).set_page_index(page_index);
+
// If row group metadata was encrypted, we replace the encrypted row
groups with
// unencrypted metadata before it is returned to users. This allows
the metadata
// to be usable for retrieving the row group statistics for example,
without users
// needing to decrypt the metadata.
- let builder =
ParquetMetaDataBuilder::new(file_metadata).set_page_index(Some(Arc::new(
- PageIndex::new(column_indexes, offset_indexes),
- )));
-
Ok(match unencrypted_row_groups {
Some(rg) => builder.set_row_groups(rg).build(),
None => builder.set_row_groups(row_groups).build(),
})
}
- pub fn new(
+ pub(crate) fn new(
buf: &'a mut TrackedWrite<W>,
schema_descr: &'a SchemaDescPtr,
row_groups: Vec<RowGroupMetaData>,
created_by: Option<String>,
writer_version: i32,
write_path_in_schema: bool,
+ preserve_page_index_locations: bool,
) -> Self {
Self {
buf,
schema_descr,
row_groups,
- column_indexes: None,
- offset_indexes: None,
+ page_index: None,
key_value_metadata: None,
created_by,
object_writer: Default::default(),
writer_version,
write_path_in_schema,
+ preserve_page_index_locations,
}
}
- pub fn with_column_indexes(
- mut self,
- column_indexes: Vec<Vec<Option<ColumnIndexMetaData>>>,
- ) -> Self {
- self.column_indexes = Some(column_indexes);
- self
- }
-
- pub fn with_offset_indexes(
- mut self,
- offset_indexes: Vec<Vec<Option<OffsetIndexMetaData>>>,
- ) -> Self {
- self.offset_indexes = Some(offset_indexes);
+ pub(crate) fn with_page_index(mut self, page_index: Arc<dyn
PageIndexProvider>) -> Self {
+ self.page_index = Some(page_index);
self
}
- pub fn with_key_value_metadata(mut self, key_value_metadata:
Vec<KeyValue>) -> Self {
+ pub(crate) fn with_key_value_metadata(mut self, key_value_metadata:
Vec<KeyValue>) -> Self {
self.key_value_metadata = Some(key_value_metadata);
self
}
#[cfg(feature = "encryption")]
- pub fn with_file_encryptor(mut self, file_encryptor:
Option<Arc<FileEncryptor>>) -> Self {
+ pub(crate) fn with_file_encryptor(
+ mut self,
+ file_encryptor: Option<Arc<FileEncryptor>>,
+ ) -> Self {
self.object_writer =
self.object_writer.with_file_encryptor(file_encryptor);
self
}
@@ -333,16 +305,13 @@ impl<'a, W: Write> ThriftMetadataWriter<'a, W> {
/// metadata writer. Then set the corresponding `bloom_filter_offset` and
/// `bloom_filter_length` on [`ColumnChunkMetaData`] passed to this writer.
///
-/// <div class="warning">
-///
-/// **NOTE:**
-/// The serialization of custom [`PageIndexProvider`]s is not currently
supported.
-/// The only supported page index structure is [`PageIndex`]. If the metadata
-/// contains any other [`PageIndexProvider`] implementation, the
[`ColumnIndex`]
-/// and [`OffsetIndex`] structures are silently omitted from the output. See
-/// <https://github.com/apache/arrow-rs/issues/11030> for more details.
-///
-/// </div>
+/// The writer serializes the page index of any [`PageIndexProvider`] attached
+/// to the input [`ParquetMetaData`]. By default, if the provider has no index
+/// for a column chunk, the offset and length of that index in the
[`ColumnChunkMetaData`]
+/// are preserved in the output metadata. This is done so one can store the
footer
+/// metadata externally but keep the page indexes in the original file. If this
+/// behavior is not desired, then [`Self::with_preserve_page_index_locations`]
should
+/// be set to `false`.
///
/// # Output Format
///
@@ -406,6 +375,7 @@ pub struct ParquetMetaDataWriter<'a, W: Write> {
buf: TrackedWrite<W>,
metadata: &'a ParquetMetaData,
write_path_in_schema: bool,
+ preserve_page_index_locations: bool,
}
impl<'a, W: Write> ParquetMetaDataWriter<'a, W> {
@@ -431,6 +401,7 @@ impl<'a, W: Write> ParquetMetaDataWriter<'a, W> {
buf,
metadata,
write_path_in_schema: true,
+ preserve_page_index_locations: true,
}
}
@@ -443,6 +414,23 @@ impl<'a, W: Write> ParquetMetaDataWriter<'a, W> {
}
}
+ /// Set whether or not to preserve the page index location metadata in the
Thrift
+ /// `ColumnMetaData` (defaults to `true`).
+ ///
+ /// Because this struct is often used to externalize the footer metadata,
it is
+ /// usually desirable to preserve this location information, even when the
+ /// page indexes are not duplicated (for instance if the provided
`ParquetMetaData`
+ /// returns no `PageIndexProvider`). As such, this defaults to `true`.
+ ///
+ /// Set this to `false` to reset the location metadata if no page indexes
are
+ /// present in the provided [`ParquetMetaData`].
+ pub fn with_preserve_page_index_locations(self, val: bool) -> Self {
+ Self {
+ preserve_page_index_locations: val,
+ ..self
+ }
+ }
+
/// Write the metadata to the buffer
pub fn finish(mut self) -> Result<()> {
let file_metadata = self.metadata.file_metadata();
@@ -462,23 +450,11 @@ impl<'a, W: Write> ParquetMetaDataWriter<'a, W> {
created_by,
file_metadata.version(),
self.write_path_in_schema,
+ self.preserve_page_index_locations,
);
- // Downcast to PageIndex to access raw index structures for
serialization.
- // Page indexes from custom PageIndexProviders are not written. See
- // <https://github.com/apache/arrow-rs/issues/11030>
- if let Some(page_index_arc) = self.metadata.page_index.as_ref()
- && let Some(page_index) = page_index_arc
- .as_any()
- .downcast_ref::<crate::file::metadata::PageIndex>()
- {
- if let Some(column_indexes) = page_index.column_indexes_raw() {
- encoder = encoder.with_column_indexes(column_indexes.clone());
- }
-
- if let Some(offset_indexes) = page_index.offset_indexes_raw() {
- encoder = encoder.with_offset_indexes(offset_indexes.clone());
- }
+ if let Some(page_index_arc) = self.metadata.page_index.clone() {
+ encoder = encoder.with_page_index(page_index_arc);
}
if let Some(key_value_metadata) = key_value_metadata {
diff --git a/parquet/src/file/writer.rs b/parquet/src/file/writer.rs
index 6c0f3e8dd2..af60326ca3 100644
--- a/parquet/src/file/writer.rs
+++ b/parquet/src/file/writer.rs
@@ -18,6 +18,7 @@
//! [`SerializedFileWriter`]: Low level Parquet writer API
use crate::bloom_filter::Sbbf;
+use crate::file::metadata::page_index::PageIndex;
use crate::file::metadata::thrift::PageHeader;
use crate::file::page_index::column_index::ColumnIndexMetaData;
use crate::file::page_index::offset_index::OffsetIndexMetaData;
@@ -366,6 +367,7 @@ impl<W: Write + Send> SerializedFileWriter<W> {
Some(self.props.created_by().to_string()),
self.props.writer_version().as_num(),
write_path_in_schema,
+ false,
);
#[cfg(feature = "encryption")]
@@ -377,10 +379,27 @@ impl<W: Write + Send> SerializedFileWriter<W> {
encoder = encoder.with_key_value_metadata(key_value_metadata)
}
- encoder = encoder.with_column_indexes(column_indexes);
- if !self.props.offset_index_disabled() {
- encoder = encoder.with_offset_indexes(offset_indexes);
+ // check for empty column index
+ let column_indexes = if column_indexes.is_empty()
+ || column_indexes
+ .iter()
+ .all(|cis| cis.iter().all(|ci| ci.is_none()))
+ {
+ None
+ } else {
+ Some(column_indexes)
+ };
+ // offset index will always be created unless explicitly disabled
+ let offset_indexes = if self.props.offset_index_disabled() {
+ None
+ } else {
+ Some(offset_indexes)
+ };
+ if column_indexes.is_some() || offset_indexes.is_some() {
+ let page_index = PageIndex::new(column_indexes, offset_indexes);
+ encoder = encoder.with_page_index(Arc::new(page_index));
}
+
encoder.finish()
}
@@ -2201,6 +2220,55 @@ mod tests {
assert!(b_idx.is_none(), "{b_idx:?}");
}
+ #[test]
+ fn test_offset_index_disabled() {
+ let message_type = "
+ message test_schema {
+ REQUIRED INT32 a;
+ REQUIRED INT32 b;
+ }
+ ";
+ // write file with indexes disabled (including offset indexes)
+ let schema = Arc::new(parse_message_type(message_type).unwrap());
+ let props = WriterProperties::builder()
+ .set_statistics_enabled(EnabledStatistics::None)
+ .set_offset_index_disabled(true)
+ .build();
+ let mut file = Vec::with_capacity(1024);
+ let mut file_writer =
+ SerializedFileWriter::new(&mut file, schema,
Arc::new(props)).unwrap();
+
+ let mut row_group_writer = file_writer.next_row_group().unwrap();
+ let mut a_writer = row_group_writer.next_column().unwrap().unwrap();
+ let col_writer = a_writer.typed::<Int32Type>();
+ col_writer.write_batch(&[1, 2, 3], None, None).unwrap();
+ a_writer.close().unwrap();
+
+ let mut b_writer = row_group_writer.next_column().unwrap().unwrap();
+ let col_writer = b_writer.typed::<Int32Type>();
+ col_writer.write_batch(&[4, 5, 6], None, None).unwrap();
+ b_writer.close().unwrap();
+ row_group_writer.close().unwrap();
+
+ let metadata = file_writer.finish().unwrap();
+ assert_eq!(metadata.num_row_groups(), 1);
+ let row_group = metadata.row_group(0);
+ assert_eq!(row_group.num_columns(), 2);
+ // no page indexes should exist
+ assert!(row_group.column(0).offset_index_offset().is_none());
+ assert!(row_group.column(0).column_index_offset().is_none());
+ assert!(row_group.column(1).offset_index_offset().is_none());
+ assert!(row_group.column(1).column_index_offset().is_none());
+
+ drop(file_writer);
+
+ // read file and request page index...should be `None`
+ let options = ReadOptionsBuilder::new().with_page_index().build();
+ let reader = SerializedFileReader::new_with_options(Bytes::from(file),
options).unwrap();
+
+ assert!(reader.metadata().page_index().is_none());
+ }
+
#[test]
fn test_byte_array_size_statistics() {
let message_type = "