use std::collections::BTreeSet;
use parquet::{
basic::SortOrder,
file::{
metadata::{ColumnChunkMetaData, ParquetMetaData, RowGroupMetaData},
statistics::Statistics,
},
schema::types::SchemaDescriptor,
};
pub(crate) fn root_column_leaf_indices(schema: &SchemaDescriptor, root_index: usize) -> Vec<usize> {
(0..schema.num_columns()).filter(|leaf_index| schema.get_column_root_idx(*leaf_index) == root_index).collect()
}
pub fn single_leaf_primitive_root_column_index(schema: &SchemaDescriptor, root_index: usize) -> Option<usize> {
if !schema.root_schema().get_fields().get(root_index)?.is_primitive() {
return None;
}
let mut leaf_indices =
(0..schema.num_columns()).filter(|leaf_index| schema.get_column_root_idx(*leaf_index) == root_index);
let leaf_index = leaf_indices.next()?;
leaf_indices.next().is_none().then_some(leaf_index)
}
pub fn leaf_column_chunk<'a>(
row_group: &'a RowGroupMetaData,
schema: &SchemaDescriptor,
root_column_index: usize,
) -> Option<&'a ColumnChunkMetaData> {
let leaf_column_index = single_leaf_primitive_root_column_index(schema, root_column_index)?;
row_group.columns().get(leaf_column_index)
}
pub fn leaf_column_statistics<'a>(
row_group: &'a RowGroupMetaData,
schema: &SchemaDescriptor,
root_column_index: usize,
) -> Option<&'a Statistics> {
leaf_column_chunk(row_group, schema, root_column_index)?.statistics()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColumnByteKind {
Compressed,
Uncompressed,
}
pub fn row_group_column_bytes(
row_group: &RowGroupMetaData,
column_indexes: &BTreeSet<usize>,
kind: ColumnByteKind,
) -> Option<u64> {
column_indexes.iter().try_fold(0u64, |total, column_index| {
let column = row_group.columns().get(*column_index)?;
let bytes = match kind {
ColumnByteKind::Compressed => column.compressed_size(),
ColumnByteKind::Uncompressed => column.uncompressed_size(),
};
let bytes = u64::try_from(bytes).ok()?;
total.checked_add(bytes)
})
}
pub fn row_group_null_counts_for_root_column(
metadata: &ParquetMetaData,
root_column_index: usize,
) -> Option<Vec<Option<u64>>> {
let schema = metadata.file_metadata().schema_descr();
let leaf_index = single_leaf_primitive_root_column_index(schema, root_column_index)?;
Some(
metadata
.row_groups()
.iter()
.map(|row_group| {
row_group
.columns()
.get(leaf_index)
.and_then(|column| column.statistics())
.and_then(|statistics| statistics.null_count_opt())
})
.collect(),
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IntegerStatisticsRange {
pub min: i128,
pub max: i128,
}
pub fn integer_statistics_range(column: &ColumnChunkMetaData) -> Option<IntegerStatisticsRange> {
let statistics = column.statistics()?;
let unsigned = column.column_descr().sort_order() == SortOrder::UNSIGNED;
match statistics {
Statistics::Int32(stats) => {
let min = *stats.min_opt()?;
let max = *stats.max_opt()?;
Some(if unsigned {
IntegerStatisticsRange { min: i128::from(min as u32), max: i128::from(max as u32) }
} else {
IntegerStatisticsRange { min: i128::from(min), max: i128::from(max) }
})
},
Statistics::Int64(stats) => {
let min = *stats.min_opt()?;
let max = *stats.max_opt()?;
Some(if unsigned {
IntegerStatisticsRange { min: i128::from(min as u64), max: i128::from(max as u64) }
} else {
IntegerStatisticsRange { min: i128::from(min), max: i128::from(max) }
})
},
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct I64StatisticsRange {
pub min: i64,
pub max: i64,
}
pub(crate) fn row_group_i64_range(
row_group: &RowGroupMetaData,
leaf_column_index: usize,
) -> Option<I64StatisticsRange> {
let range = integer_statistics_range(row_group.columns().get(leaf_column_index)?)?;
Some(I64StatisticsRange { min: i64::try_from(range.min).ok()?, max: i64::try_from(range.max).ok()? })
}
pub fn row_group_i64_range_for_root_column(
row_group: &RowGroupMetaData,
schema: &SchemaDescriptor,
root_column_index: usize,
) -> Option<I64StatisticsRange> {
let leaf_column_index = single_leaf_primitive_root_column_index(schema, root_column_index)?;
row_group_i64_range(row_group, leaf_column_index)
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use parquet::{
file::{
metadata::{ColumnChunkMetaData, RowGroupMetaData},
statistics::Statistics,
},
schema::{parser::parse_message_type, types::SchemaDescriptor},
};
use super::{I64StatisticsRange, row_group_i64_range, single_leaf_primitive_root_column_index};
fn row_group_with_statistics(schema_body: &str, statistics: Statistics) -> RowGroupMetaData {
let parquet_type = parse_message_type(schema_body).expect("test Parquet schema should parse");
let schema = Arc::new(SchemaDescriptor::new(Arc::new(parquet_type)));
let column = ColumnChunkMetaData::builder(Arc::clone(&schema.columns()[0]))
.set_statistics(statistics)
.build()
.expect("test column metadata should build");
RowGroupMetaData::builder(Arc::clone(&schema))
.set_num_rows(2)
.add_column_metadata(column)
.build()
.expect("test row-group metadata should build")
}
#[test]
fn row_group_i64_range_decodes_unsigned_int32_statistics_as_unsigned() {
let row_group = row_group_with_statistics(
"message schema { required int32 k (INTEGER(32,false)); }",
Statistics::int32(Some(100), Some(3_000_000_000u32 as i32), None, None, false),
);
let range = row_group_i64_range(&row_group, 0).expect("unsigned int32 stats should decode");
assert_eq!(range, I64StatisticsRange { min: 100, max: 3_000_000_000 });
}
#[test]
fn row_group_i64_range_is_none_for_unsigned_int64_beyond_i64_max() {
let row_group = row_group_with_statistics(
"message schema { required int64 k (INTEGER(64,false)); }",
Statistics::int64(Some(0), Some(u64::MAX as i64), None, None, false),
);
assert_eq!(row_group_i64_range(&row_group, 0), None);
}
#[test]
fn row_group_i64_range_keeps_signed_int32_statistics_unchanged() {
let row_group = row_group_with_statistics(
"message schema { required int32 k; }",
Statistics::int32(Some(-5), Some(42), None, None, false),
);
let range = row_group_i64_range(&row_group, 0).expect("signed int32 stats should decode");
assert_eq!(range, I64StatisticsRange { min: -5, max: 42 });
}
#[test]
fn single_leaf_mapping_refuses_nested_roots_and_maps_later_primitives() {
let parquet_type = parse_message_type(
"
message schema {
optional group payload {
optional int32 x;
optional int32 y;
}
optional int32 fact_key;
}
",
)
.expect("test Parquet schema should parse");
let schema = SchemaDescriptor::new(Arc::new(parquet_type));
assert_eq!(single_leaf_primitive_root_column_index(&schema, 0), None);
assert_eq!(single_leaf_primitive_root_column_index(&schema, 1), Some(2));
}
#[test]
fn single_leaf_mapping_refuses_list_roots_with_one_required_leaf() {
let parquet_type = parse_message_type(
"
message schema {
optional group items (LIST) {
repeated group list {
required int32 element;
}
}
optional int32 fact_key;
}
",
)
.expect("test Parquet schema should parse");
let schema = SchemaDescriptor::new(Arc::new(parquet_type));
assert_eq!(schema.num_columns(), 2);
assert_eq!(single_leaf_primitive_root_column_index(&schema, 0), None);
assert_eq!(single_leaf_primitive_root_column_index(&schema, 1), Some(1));
}
#[test]
fn single_leaf_mapping_uses_parquet_root_indexes_not_names() {
let parquet_type = parse_message_type(
"
message schema {
optional int32 duplicate_name;
optional int64 duplicate_name;
}
",
)
.expect("test Parquet schema should parse");
let schema = SchemaDescriptor::new(Arc::new(parquet_type));
assert_eq!(single_leaf_primitive_root_column_index(&schema, 0), Some(0));
assert_eq!(single_leaf_primitive_root_column_index(&schema, 1), Some(1));
}
}