use parquet::basic::{Compression, Encoding, ZstdLevel};
use parquet::file::properties::{EnabledStatistics, WriterProperties};
use parquet::schema::types::ColumnPath;
use crate::encode::schema::{BLOOM_FILTER_COLUMNS, DELTA_ENCODED_COLUMNS, col};
const BLOOM_FPP: f64 = 0.01;
const PAGE_ROW_LIMIT: usize = 20_000;
const ROW_GROUP_ROW_LIMIT: usize = 256 * 1024;
pub fn writer_properties(expected_malo_ids: u64) -> WriterProperties {
let mut builder = WriterProperties::builder()
.set_compression(Compression::ZSTD(
ZstdLevel::try_new(3).expect("level 3 is valid"),
))
.set_statistics_enabled(EnabledStatistics::Page)
.set_data_page_row_count_limit(PAGE_ROW_LIMIT)
.set_max_row_group_row_count(Some(ROW_GROUP_ROW_LIMIT))
.set_bloom_filter_enabled(false);
for name in BLOOM_FILTER_COLUMNS {
let path = ColumnPath::from(name);
builder = builder
.set_column_bloom_filter_enabled(path.clone(), true)
.set_column_bloom_filter_fpp(path.clone(), BLOOM_FPP)
.set_column_bloom_filter_ndv(path, expected_malo_ids.max(1));
}
for name in DELTA_ENCODED_COLUMNS {
let path = ColumnPath::from(name);
builder = builder
.set_column_dictionary_enabled(path.clone(), false)
.set_column_encoding(path, Encoding::DELTA_BINARY_PACKED);
}
for name in [
col::MALO_ID,
col::MELO_ID,
col::OBIS_CODE,
col::QUALITY,
col::RESOLUTION,
col::SOURCE_KIND,
col::VERSION_SCOPE,
] {
builder = builder.set_column_dictionary_enabled(ColumnPath::from(name), true);
}
builder = builder.set_sorting_columns(Some(sorting_columns()));
builder.build()
}
fn sorting_columns() -> Vec<parquet::file::metadata::SortingColumn> {
let schema = crate::encode::schema::storage_schema(&[]);
crate::encode::schema::SORT_COLUMNS
.iter()
.filter_map(|name| schema.index_of(name).ok())
.map(|index| parquet::file::metadata::SortingColumn {
column_idx: index as i32,
descending: false,
nulls_first: false,
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_row_group_size_is_ours_rather_than_the_library_default() {
let props = writer_properties(1_000);
assert_eq!(props.max_row_group_row_count(), Some(ROW_GROUP_ROW_LIMIT));
let inherited = WriterProperties::builder()
.build()
.max_row_group_row_count()
.unwrap_or(usize::MAX);
assert!(
ROW_GROUP_ROW_LIMIT < inherited,
"the point is to be below the parquet default ({inherited}), not merely explicit"
);
assert!(
ROW_GROUP_ROW_LIMIT > props.data_page_row_count_limit(),
"a row group must hold more than one page or page pruning does nothing"
);
}
#[test]
fn bloom_filters_are_enabled_only_on_lookup_columns() {
let props = writer_properties(1_000);
for name in BLOOM_FILTER_COLUMNS {
assert!(
props
.bloom_filter_properties(&ColumnPath::from(name))
.is_some(),
"{name} must carry a bloom filter"
);
}
assert!(
props
.bloom_filter_properties(&ColumnPath::from(col::VALUE))
.is_none()
);
}
#[test]
fn bloom_filter_ndv_tracks_the_supplied_cardinality() {
let small = writer_properties(10);
let large = writer_properties(1_000_000);
let path = ColumnPath::from(col::MALO_ID);
let a = small.bloom_filter_properties(&path).unwrap().ndv;
let b = large.bloom_filter_properties(&path).unwrap().ndv;
assert!(a < b, "ndv must reflect the window's distinct meters");
}
#[test]
fn bloom_filter_ndv_is_never_zero() {
let props = writer_properties(0);
let ndv = props
.bloom_filter_properties(&ColumnPath::from(col::MALO_ID))
.unwrap()
.ndv;
assert!(ndv >= 1);
}
#[test]
fn timestamp_and_value_columns_use_delta_encoding() {
let props = writer_properties(100);
for name in DELTA_ENCODED_COLUMNS {
let path = ColumnPath::from(name);
assert_eq!(
props.encoding(&path),
Some(Encoding::DELTA_BINARY_PACKED),
"{name} must be delta encoded"
);
assert!(
!props.dictionary_enabled(&path),
"{name} must not be dictionary encoded, or delta encoding is ignored"
);
}
}
#[test]
fn identifier_columns_stay_dictionary_encoded() {
let props = writer_properties(100);
assert!(props.dictionary_enabled(&ColumnPath::from(col::MALO_ID)));
assert!(props.dictionary_enabled(&ColumnPath::from(col::OBIS_CODE)));
}
#[test]
fn page_level_statistics_are_enabled() {
let props = writer_properties(100);
assert_eq!(
props.statistics_enabled(&ColumnPath::from(col::FROM)),
EnabledStatistics::Page
);
}
#[test]
fn the_footer_declares_the_sort_order() {
let props = writer_properties(100);
let declared = props.sorting_columns().expect("a declared sort order");
let schema = crate::encode::schema::storage_schema(&[]);
let expected: Vec<i32> = crate::encode::schema::SORT_COLUMNS
.iter()
.map(|name| schema.index_of(name).unwrap() as i32)
.collect();
assert_eq!(
declared.iter().map(|c| c.column_idx).collect::<Vec<_>>(),
expected
);
assert!(declared.iter().all(|c| !c.descending), "ascending only");
}
#[test]
fn the_declared_sort_order_is_a_prefix_of_the_scan_cursor() {
let cursor = crate::tiering::store::ScanSpec::core().cursor_columns();
for (i, name) in crate::encode::schema::SORT_COLUMNS.iter().enumerate() {
assert_eq!(&cursor[i], name);
}
}
#[test]
fn compression_is_zstd() {
let props = writer_properties(100);
assert!(matches!(
props.compression(&ColumnPath::from(col::VALUE)),
Compression::ZSTD(_)
));
}
}