use tracing::warn;
use crate::actions::{MAX_VALUES, MIN_VALUES, NULL_COUNT};
use crate::content_tree::{
AVG_VALUE_SIZE_IN_BYTES, LOWER_BOUND, NAN_VALUE_COUNT, NULL_VALUE_COUNT, TIGHT_BOUNDS,
UPPER_BOUND, VALUE_COUNT,
};
use crate::expressions::ColumnName;
use crate::schema::{
ColumnMetadataKey, DataType, MetadataValue, PrimitiveType, StructField, StructType,
};
use crate::transforms::{transform_output_type, SchemaTransform};
use crate::{DeltaResult, Error};
const STATS_OFFSET_LOWER_BOUND: i32 = 1;
const STATS_OFFSET_UPPER_BOUND: i32 = 2;
const STATS_OFFSET_TIGHT_BOUNDS: i32 = 3;
const STATS_OFFSET_VALUE_COUNT: i32 = 4;
const STATS_OFFSET_NULL_VALUE_COUNT: i32 = 5;
const STATS_OFFSET_NAN_VALUE_COUNT: i32 = 6;
const STATS_OFFSET_AVG_VALUE_SIZE_IN_BYTES: i32 = 7;
const NUM_SUPPORTED_STATS_PER_COLUMN: i32 = 200;
const STATS_SPACE_FIELD_ID_START_FOR_DATA_FIELDS: i32 = 10_000;
const STATS_SPACE_FIELD_ID_START_FOR_METADATA_FIELDS: i32 = 9_000;
const STATS_SPACE_FIELD_ID_END: i32 = 200_000_000;
const MAX_DATA_STATS_FIELD_ID: i32 = STATS_SPACE_FIELD_ID_END - NUM_SUPPORTED_STATS_PER_COLUMN;
const MAX_DATA_FIELD_ID: i32 = (MAX_DATA_STATS_FIELD_ID
- STATS_SPACE_FIELD_ID_START_FOR_DATA_FIELDS)
/ NUM_SUPPORTED_STATS_PER_COLUMN;
const LAST_UPDATED_SEQUENCE_NUMBER_FIELD_ID: i32 = 2_147_483_539;
const ROW_ID_FIELD_ID: i32 = 2_147_483_540;
const SUPPORTED_METADATA_FIELD_IDS: [i32; 2] =
[LAST_UPDATED_SEQUENCE_NUMBER_FIELD_ID, ROW_ID_FIELD_ID];
const FIRST_SUPPORTED_METADATA_FIELD_ID: i32 = SUPPORTED_METADATA_FIELD_IDS[0];
struct StatsSpace {
start: i32,
field_base: i32,
}
impl StatsSpace {
const fn base(&self, field_id: i32) -> i32 {
self.start + NUM_SUPPORTED_STATS_PER_COLUMN * (field_id - self.field_base)
}
}
const METADATA_SPACE: StatsSpace = StatsSpace {
start: STATS_SPACE_FIELD_ID_START_FOR_METADATA_FIELDS,
field_base: FIRST_SUPPORTED_METADATA_FIELD_ID,
};
const DATA_SPACE: StatsSpace = StatsSpace {
start: STATS_SPACE_FIELD_ID_START_FOR_DATA_FIELDS,
field_base: 0,
};
pub(crate) fn field_id_to_statistics_base(field_id: i32) -> Option<i32> {
if SUPPORTED_METADATA_FIELD_IDS.contains(&field_id) {
Some(METADATA_SPACE.base(field_id))
} else if (0..=MAX_DATA_FIELD_ID).contains(&field_id) {
Some(DATA_SPACE.base(field_id))
} else {
None
}
}
fn field_with_id(name: &str, data_type: DataType, nullable: bool, field_id: i32) -> StructField {
StructField::new(name, data_type, nullable).with_metadata([(
ColumnMetadataKey::ParquetFieldId.as_ref(),
MetadataValue::Number(field_id as i64),
)])
}
fn get_field_id(field: &StructField) -> Option<i32> {
match field.get_config_value(&ColumnMetadataKey::ParquetFieldId) {
Some(MetadataValue::Number(id)) => (*id).try_into().ok(),
_ => None,
}
}
#[derive(Clone, Copy)]
struct StatCategories {
null_count: bool,
min_values: bool,
max_values: bool,
}
impl StatCategories {
const ALL: StatCategories = StatCategories {
null_count: true,
min_values: true,
max_values: true,
};
fn any(&self) -> bool {
self.null_count || self.min_values || self.max_values
}
}
fn build_stats_struct(
base_field_id: i32,
bounds_type: &DataType,
categories: Option<StatCategories>,
) -> StructType {
let is_variant = matches!(bounds_type, DataType::Variant(_));
let (has_nan_count, has_size_stats) = match bounds_type {
DataType::Primitive(ptype) => (
matches!(ptype, PrimitiveType::Float | PrimitiveType::Double),
matches!(ptype, PrimitiveType::String | PrimitiveType::Binary),
),
DataType::Variant(_) => (false, true),
_ => (false, false),
};
let projecting = categories.is_some();
let StatCategories {
null_count: has_null,
min_values: has_min,
max_values: has_max,
} = categories.unwrap_or(StatCategories::ALL);
let has_bounds = has_min || has_max;
let specs = [
(
LOWER_BOUND,
bounds_type.clone(),
STATS_OFFSET_LOWER_BOUND,
has_min,
),
(
UPPER_BOUND,
bounds_type.clone(),
STATS_OFFSET_UPPER_BOUND,
has_max,
),
(
TIGHT_BOUNDS,
DataType::BOOLEAN,
STATS_OFFSET_TIGHT_BOUNDS,
!is_variant && has_bounds,
),
(
VALUE_COUNT,
DataType::LONG,
STATS_OFFSET_VALUE_COUNT,
has_null,
),
(
NULL_VALUE_COUNT,
DataType::LONG,
STATS_OFFSET_NULL_VALUE_COUNT,
has_null,
),
(
NAN_VALUE_COUNT,
DataType::LONG,
STATS_OFFSET_NAN_VALUE_COUNT,
has_nan_count && has_bounds,
),
(
AVG_VALUE_SIZE_IN_BYTES,
DataType::INTEGER,
STATS_OFFSET_AVG_VALUE_SIZE_IN_BYTES,
has_size_stats && !projecting,
),
];
let fields = specs.into_iter().filter_map(|(name, ty, offset, include)| {
include.then(|| field_with_id(name, ty, true, base_field_id + offset))
});
StructType::new_unchecked(fields)
}
fn leaf_stats_field(
field: &StructField,
path: &[String],
categories: Option<StatCategories>,
) -> DeltaResult<Option<StructField>> {
let field_id = get_field_id(field).ok_or_else(|| {
Error::generic(format!(
"Field '{}' has no usable (present, i32-representable) field ID. metadata: {:#?}",
field.name(),
field.metadata()
))
})?;
#[cfg(feature = "geo-type-in-dev")]
if matches!(
field.data_type(),
DataType::Primitive(PrimitiveType::Geometry(_) | PrimitiveType::Geography(_))
) {
return Err(Error::unsupported(format!(
"AMT stats schema generation is not yet implemented for geospatial column '{}' (type {})",
field.name(),
field.data_type(),
)));
}
let Some(base_stats_id) = field_id_to_statistics_base(field_id) else {
warn!(
"Skipping stats for field '{}' (field_id={field_id}): outside supported stats range",
field.name(),
);
return Ok(None);
};
let stats_struct = match field.data_type() {
DataType::Primitive(_) => build_stats_struct(base_stats_id, field.data_type(), categories),
DataType::Variant(_) => {
build_stats_struct(base_stats_id, &DataType::unshredded_variant(), categories)
}
_ => return Ok(None),
};
let name = ColumnName::new(path).to_string();
Ok(Some(field_with_id(
&name,
stats_struct.into(),
true,
base_stats_id,
)))
}
enum SubSchema<'a> {
Absent,
Struct(&'a StructType),
Mismatch,
}
fn struct_sub_schema<'a>(schema: &'a StructType, field_name: &str) -> SubSchema<'a> {
match schema.field(field_name).map(StructField::data_type) {
None => SubSchema::Absent,
Some(DataType::Struct(s)) => SubSchema::Struct(s.as_ref()),
Some(_) => SubSchema::Mismatch,
}
}
#[derive(Clone, Copy)]
struct CategoryScopes<'a> {
categories: [Option<&'a StructType>; 3],
}
const STAT_CATEGORIES: [&str; 3] = [NULL_COUNT, MIN_VALUES, MAX_VALUES];
impl<'a> CategoryScopes<'a> {
fn from_delta_stats_schema(delta_stats_schema: &'a StructType) -> DeltaResult<Self> {
let mut categories = [None; 3];
for (i, category) in STAT_CATEGORIES.iter().enumerate() {
categories[i] = match struct_sub_schema(delta_stats_schema, category) {
SubSchema::Absent => None,
SubSchema::Struct(s) => Some(s),
SubSchema::Mismatch => {
return Err(Error::generic(format!(
"Delta stats schema invariant violation: category '{category}' is a scalar, \
but it must be a struct mirroring the table"
)))
}
};
}
Ok(CategoryScopes { categories })
}
fn descend(&self, name: &str, path: &[String]) -> DeltaResult<CategoryScopes<'a>> {
let mut categories = [None; 3];
for (i, scope) in self.categories.iter().enumerate() {
categories[i] = match scope {
None => None,
Some(s) => match struct_sub_schema(s, name) {
SubSchema::Absent => None,
SubSchema::Struct(sub) => Some(sub),
SubSchema::Mismatch => {
return Err(Error::generic(format!(
"Delta stats schema invariant violation at '{}': category '{}' has \
'{name}' as a scalar, but the table nests a struct there",
ColumnName::new(path),
STAT_CATEGORIES[i],
)))
}
},
};
}
Ok(CategoryScopes { categories })
}
fn leaf_categories(&self, leaf_name: &str) -> StatCategories {
let [null_count, min_values, max_values] = self
.categories
.map(|scope| scope.is_some_and(|s| s.field(leaf_name).is_some()));
StatCategories {
null_count,
min_values,
max_values,
}
}
}
struct StatsLeafWalker<'a, F> {
path: Vec<String>,
projection: Option<CategoryScopes<'a>>,
on_leaf: F,
}
impl<'a, F> SchemaTransform<'a> for StatsLeafWalker<'a, F>
where
F: FnMut(&'a StructField, &[String], Option<StatCategories>) -> Result<(), Error>,
{
transform_output_type!(|'a, T| Result<(), Error>);
fn transform_struct_field(&mut self, field: &'a StructField) -> Result<(), Error> {
self.path.push(field.name().to_string());
let result = if let DataType::Struct(_) = field.data_type() {
let saved_projection = self.projection;
self.projection = self
.projection
.map(|s| s.descend(field.name(), &self.path))
.transpose()?;
let result = self.recurse_into_struct_field(field);
self.projection = saved_projection;
result
} else {
let categories = self.projection.map(|s| s.leaf_categories(field.name()));
if categories.is_some_and(|c| !c.any()) {
Ok(())
} else {
(self.on_leaf)(field, &self.path, categories)
}
};
self.path.pop();
result
}
}
pub(crate) fn stats_schema(table_struct: &StructType) -> DeltaResult<StructType> {
collect_stats_schema(table_struct, None)
}
pub(crate) fn projected_stats_schema(
table_struct: &StructType,
delta_stats_schema: &StructType,
) -> DeltaResult<StructType> {
collect_stats_schema(
table_struct,
Some(CategoryScopes::from_delta_stats_schema(delta_stats_schema)?),
)
}
fn collect_stats_schema<'a>(
table_struct: &'a StructType,
projection: Option<CategoryScopes<'a>>,
) -> DeltaResult<StructType> {
let mut fields: Vec<StructField> = Vec::new();
{
let mut walker = StatsLeafWalker {
path: Vec::new(),
projection,
on_leaf: |field: &'a StructField, path: &[String], categories| {
leaf_stats_field(field, path, categories).map(|stats| fields.extend(stats))
},
};
walker.transform_struct(table_struct)?;
}
Ok(StructType::new_unchecked(fields))
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
use crate::scan::data_skipping::stats_schema::{expected_stats_schema, StatsConfig};
use crate::schema::{ArrayType, MapType};
#[cfg(feature = "geo-type-in-dev")]
use crate::schema::{EdgeInterpolationAlgorithm, GeographyType, GeometryType};
use crate::table_properties::DataSkippingNumIndexedCols;
#[rstest]
#[case(0, 10_000)]
#[case(1, 10_200)]
#[case(2, 10_400)]
#[case(5, 11_000)]
#[case(100, 30_000)]
#[case(MAX_DATA_FIELD_ID, MAX_DATA_STATS_FIELD_ID)]
#[case(LAST_UPDATED_SEQUENCE_NUMBER_FIELD_ID, 9_000)]
#[case(ROW_ID_FIELD_ID, 9_200)]
fn valid_mapping_roundtrips(#[case] field_id: i32, #[case] stats_base: i32) {
assert_eq!(field_id_to_statistics_base(field_id), Some(stats_base));
}
#[rstest]
#[case(-1)] #[case(MAX_DATA_FIELD_ID + 1)] #[case(2_147_483_541)] #[case(2_147_483_645)] #[case(2_147_483_646)] fn field_id_to_statistics_base_rejects_invalid(#[case] field_id: i32) {
assert_eq!(field_id_to_statistics_base(field_id), None);
}
fn stats_struct_for_name(name: &str, stats: &StructType) -> StructType {
let field_stats = stats.field(name).expect("stats field should exist");
match field_stats.data_type() {
DataType::Struct(s) => s.as_ref().clone(),
other => panic!("expected struct stats, got {other:?}"),
}
}
fn assert_stats_field_ids(stats_struct: &StructType, base_id: i32, field: &StructField) {
let is_variant = matches!(field.data_type(), DataType::Variant(_));
let assert_offset = |name: &str, offset: i32| {
assert_eq!(
get_field_id(stats_struct.field(name).unwrap()),
Some(base_id + offset)
);
};
assert_offset(VALUE_COUNT, STATS_OFFSET_VALUE_COUNT);
assert_offset(NULL_VALUE_COUNT, STATS_OFFSET_NULL_VALUE_COUNT);
if field.data_type() == &DataType::FLOAT || field.data_type() == &DataType::DOUBLE {
assert_offset(NAN_VALUE_COUNT, STATS_OFFSET_NAN_VALUE_COUNT);
}
if is_variant
|| field.data_type() == &DataType::STRING
|| field.data_type() == &DataType::BINARY
{
assert_offset(
AVG_VALUE_SIZE_IN_BYTES,
STATS_OFFSET_AVG_VALUE_SIZE_IN_BYTES,
);
}
assert_offset(LOWER_BOUND, STATS_OFFSET_LOWER_BOUND);
assert_offset(UPPER_BOUND, STATS_OFFSET_UPPER_BOUND);
if !is_variant {
assert_offset(TIGHT_BOUNDS, STATS_OFFSET_TIGHT_BOUNDS);
}
}
#[rstest]
#[case(DataType::INTEGER, false, 1, 10_200, 5)] #[case(DataType::STRING, true, 2, 10_400, 6)] #[case(DataType::DOUBLE, true, 5, 11_000, 6)] #[case(DataType::FLOAT, false, 100, 30_000, 6)] #[case(DataType::LONG, true, 42, 18_400, 5)] #[case(DataType::BINARY, true, 3, 10_600, 6)] #[case(DataType::BINARY, false, 4, 10_800, 6)] #[case(
DataType::INTEGER,
false,
MAX_DATA_FIELD_ID,
MAX_DATA_STATS_FIELD_ID,
5
)] fn stats_schema_primitive_field(
#[case] data_type: DataType,
#[case] nullable: bool,
#[case] field_id: i32,
#[case] expected_base: i32,
#[case] expected_count: usize,
) {
let field = field_with_id("c", data_type.clone(), nullable, field_id);
let schema = StructType::new_unchecked([field.clone()]);
let stats = stats_schema(&schema).expect("stats_schema should succeed");
let stats_struct = stats_struct_for_name(field.name(), &stats);
assert_eq!(stats_struct.fields().count(), expected_count);
assert_eq!(
stats_struct.field(LOWER_BOUND).unwrap().data_type(),
&data_type
);
assert_eq!(
stats_struct.field(UPPER_BOUND).unwrap().data_type(),
&data_type
);
assert_eq!(get_field_id(stats.field("c").unwrap()), Some(expected_base));
assert_stats_field_ids(&stats_struct, expected_base, &field);
}
#[test]
fn stats_schema_multiple_fields() {
let schema = StructType::new_unchecked([
field_with_id("id", DataType::LONG, false, 0),
field_with_id("name", DataType::STRING, true, 1),
field_with_id("score", DataType::DOUBLE, true, 2),
]);
let stats = stats_schema(&schema).expect("stats_schema should succeed");
assert_eq!(stats.fields().count(), 3);
assert!(stats.field("id").is_some());
assert!(stats.field("name").is_some());
assert!(stats.field("score").is_some());
}
#[test]
fn stats_schema_missing_field_id_errors() {
let schema = StructType::new_unchecked([StructField::not_null("id", DataType::INTEGER)]);
assert!(stats_schema(&schema).is_err());
}
#[test]
fn stats_schema_nested_missing_field_id_errors() {
let inner = StructType::new_unchecked([StructField::not_null("b", DataType::INTEGER)]);
let schema = StructType::new_unchecked([field_with_id("a", inner.into(), true, 1)]);
assert!(stats_schema(&schema).is_err());
}
#[test]
fn stats_schema_non_numeric_field_id_metadata_errors() {
let field = StructField::not_null("c", DataType::INTEGER).with_metadata([(
ColumnMetadataKey::ParquetFieldId.as_ref(),
MetadataValue::String("1".to_string()),
)]);
assert!(stats_schema(&StructType::new_unchecked([field])).is_err());
}
#[test]
fn stats_schema_nested_struct() {
let field_b = field_with_id("b", DataType::INTEGER, false, 2);
let field_c = field_with_id("c", DataType::DOUBLE, true, 3);
let inner = StructType::new_unchecked([field_b.clone(), field_c.clone()]);
let schema = StructType::new_unchecked([field_with_id("a", inner.into(), true, 1)]);
let stats = stats_schema(&schema).expect("stats_schema should succeed");
assert_eq!(stats.fields().count(), 2);
assert!(stats.field("a").is_none());
let b_stats = stats_struct_for_name("a.b", &stats);
assert_eq!(b_stats.fields().count(), 5);
assert_eq!(get_field_id(stats.field("a.b").unwrap()), Some(10_400));
assert_stats_field_ids(&b_stats, 10_400, &field_b);
let c_stats = stats_struct_for_name("a.c", &stats);
assert_eq!(c_stats.fields().count(), 6); assert_eq!(get_field_id(stats.field("a.c").unwrap()), Some(10_600));
assert_stats_field_ids(&c_stats, 10_600, &field_c);
}
#[rstest]
#[case::array(DataType::Array(Box::new(ArrayType::new(DataType::INTEGER, false))))]
#[case::map(DataType::Map(Box::new(MapType::new(
DataType::STRING,
DataType::INTEGER,
false
))))]
fn stats_schema_complex_leaf_is_omitted(#[case] data_type: DataType) {
let schema = StructType::new_unchecked([field_with_id("c", data_type, true, 1)]);
let stats = stats_schema(&schema).expect("stats_schema should succeed");
assert_eq!(stats.fields().count(), 0);
assert!(stats.field("c").is_none());
}
#[test]
fn stats_schema_struct_of_only_complex_is_omitted() {
let inner = StructType::new_unchecked([field_with_id(
"f0",
DataType::Array(Box::new(ArrayType::new(DataType::FLOAT, true))),
true,
2,
)]);
let schema = StructType::new_unchecked([field_with_id("s", inner.into(), true, 1)]);
let stats = stats_schema(&schema).expect("stats_schema should succeed");
assert_eq!(stats.fields().count(), 0);
assert!(stats.field("s").is_none());
}
#[test]
fn stats_schema_struct_with_primitive_and_complex_keeps_primitive() {
let inner = StructType::new_unchecked([
field_with_id("p", DataType::INTEGER, true, 2),
field_with_id(
"a",
DataType::Array(Box::new(ArrayType::new(DataType::FLOAT, true))),
true,
3,
),
]);
let schema = StructType::new_unchecked([field_with_id("s", inner.into(), true, 1)]);
let stats = stats_schema(&schema).expect("stats_schema should succeed");
assert_eq!(stats.fields().count(), 1);
assert!(stats.field("s").is_none());
assert!(stats.field("s.p").is_some());
assert!(stats.field("s.a").is_none());
assert_eq!(get_field_id(stats.field("s.p").unwrap()), Some(10_400));
}
#[test]
fn stats_schema_deeply_nested() {
let innermost =
StructType::new_unchecked([field_with_id("c", DataType::INTEGER, false, 3)]);
let middle = StructType::new_unchecked([field_with_id("b", innermost.into(), true, 2)]);
let schema = StructType::new_unchecked([field_with_id("a", middle.into(), true, 1)]);
let stats = stats_schema(&schema).expect("stats_schema should succeed");
assert_eq!(stats.fields().count(), 1);
assert!(stats.field("a").is_none());
assert!(stats.field("a.b").is_none());
let c_stats = stats_struct_for_name("a.b.c", &stats);
assert_eq!(c_stats.fields().count(), 5);
assert!(c_stats.field(VALUE_COUNT).is_some());
assert!(c_stats.field(LOWER_BOUND).is_some());
assert!(c_stats.field(NULL_VALUE_COUNT).is_some());
assert_eq!(get_field_id(stats.field("a.b.c").unwrap()), Some(10_600));
}
#[rstest]
#[case::unshredded(DataType::unshredded_variant())]
#[case::shredded(
DataType::variant_type([
StructField::not_null("metadata", DataType::BINARY),
StructField::not_null("value", DataType::BINARY),
StructField::nullable("typed_value", DataType::INTEGER),
])
.expect("variant type")
)]
fn stats_schema_variant_column(#[case] variant_type: DataType) {
let field = field_with_id("v", variant_type, false, 3);
let schema = StructType::new_unchecked([field.clone()]);
let stats = stats_schema(&schema).expect("stats_schema should succeed");
let stats_field = stats.field("v").expect("variant column should exist");
assert!(matches!(stats_field.data_type(), DataType::Struct(_)));
assert_eq!(get_field_id(stats_field), Some(10_600));
let v_stats = stats_struct_for_name(field.name(), &stats);
assert_eq!(
v_stats.field(LOWER_BOUND).unwrap().data_type(),
&DataType::unshredded_variant()
);
assert_eq!(
v_stats.field(UPPER_BOUND).unwrap().data_type(),
&DataType::unshredded_variant()
);
assert!(v_stats.field(TIGHT_BOUNDS).is_none());
assert!(v_stats.field(AVG_VALUE_SIZE_IN_BYTES).is_some());
assert_stats_field_ids(&v_stats, 10_600, &field);
}
#[test]
fn stats_schema_variant_nested_in_struct() {
let data = field_with_id("data", DataType::unshredded_variant(), false, 6);
let inner = StructType::new_unchecked([
field_with_id("id", DataType::LONG, false, 5),
data.clone(),
]);
let schema = StructType::new_unchecked([field_with_id("record", inner.into(), false, 3)]);
let stats = stats_schema(&schema).expect("stats_schema should succeed");
assert_eq!(stats.fields().count(), 2);
assert!(stats.field("record").is_none());
assert!(stats.field("record.id").is_some());
assert_eq!(
get_field_id(stats.field("record.id").unwrap()),
Some(11_000)
);
let data_stats = stats_struct_for_name("record.data", &stats);
assert_eq!(
data_stats.field(LOWER_BOUND).unwrap().data_type(),
&DataType::unshredded_variant()
);
assert_eq!(
get_field_id(stats.field("record.data").unwrap()),
Some(11_200)
); }
#[rstest]
#[case::top_level_not_null_int(DataType::INTEGER, false, None)]
#[case::top_level_nullable_variant(DataType::unshredded_variant(), true, None)]
#[case::not_null_int_under_not_null_struct(DataType::INTEGER, false, Some(false))]
#[case::not_null_variant_under_not_null_struct(
DataType::unshredded_variant(),
false,
Some(false)
)]
fn null_value_count_always_present(
#[case] leaf_type: DataType,
#[case] leaf_nullable: bool,
#[case] parent_nullable: Option<bool>,
) {
let leaf = field_with_id("leaf", leaf_type, leaf_nullable, 2);
let (schema, leaf_name) = match parent_nullable {
None => (StructType::new_unchecked([leaf]), "leaf"),
Some(nullable) => (
StructType::new_unchecked([field_with_id(
"parent",
StructType::new_unchecked([leaf]).into(),
nullable,
1,
)]),
"parent.leaf",
),
};
let stats = stats_schema(&schema).expect("stats_schema should succeed");
let leaf_stats = stats_struct_for_name(leaf_name, &stats);
assert!(leaf_stats.field(NULL_VALUE_COUNT).is_some());
}
#[test]
fn stats_schema_variant_stats_independent_of_physical_layout() {
let variant = DataType::Variant(Box::new(StructType::new_unchecked([
StructField::not_null("metadata", DataType::BINARY),
])));
let field = field_with_id("v", variant, false, 3);
let schema = StructType::new_unchecked([field]);
let stats = stats_schema(&schema).expect("stats_schema should succeed");
let v_stats = stats_struct_for_name("v", &stats);
assert_eq!(
v_stats.field(LOWER_BOUND).unwrap().data_type(),
&DataType::unshredded_variant()
);
}
#[cfg(feature = "geo-type-in-dev")]
#[rstest]
#[case::geometry_in_range(DataType::from(GeometryType::try_new("EPSG:4326").expect("valid crs")), 1)]
#[case::geography_in_range(DataType::from(
GeographyType::try_new("EPSG:4326", EdgeInterpolationAlgorithm::Spherical).expect("valid crs")
), 1)]
#[case::geometry_out_of_range(
DataType::from(GeometryType::try_new("EPSG:4326").expect("valid crs")),
MAX_DATA_FIELD_ID + 1
)]
fn stats_schema_geospatial_column_errors(#[case] geo_type: DataType, #[case] field_id: i32) {
let schema = StructType::new_unchecked([field_with_id("g", geo_type, true, field_id)]);
let err = stats_schema(&schema).expect_err("geospatial columns are not yet supported");
assert!(
err.to_string().contains("geospatial"),
"unexpected error: {err}"
);
}
#[test]
fn stats_schema_out_of_range_data_field_id_is_dropped() {
let ok = field_with_id("hi", DataType::INTEGER, false, MAX_DATA_FIELD_ID);
let over = field_with_id("over", DataType::INTEGER, false, MAX_DATA_FIELD_ID + 1);
let schema = StructType::new_unchecked([ok, over]);
let stats = stats_schema(&schema).expect("stats_schema should succeed");
assert_eq!(
get_field_id(stats.field("hi").unwrap()),
Some(MAX_DATA_STATS_FIELD_ID)
);
assert!(stats.field("over").is_none());
}
#[rstest]
#[case(MAX_DATA_FIELD_ID + 1)] #[case(-5)] fn stats_schema_nested_out_of_range_child_is_dropped(#[case] bad_id: i32) {
let inner = StructType::new_unchecked([
field_with_id("keep", DataType::INTEGER, false, 2),
field_with_id("drop", DataType::INTEGER, false, bad_id),
]);
let schema = StructType::new_unchecked([field_with_id("a", inner.into(), true, 1)]);
let stats = stats_schema(&schema).expect("out-of-range child warn-drops, not errors");
assert_eq!(stats.fields().count(), 1);
assert!(stats.field("a.keep").is_some());
assert!(stats.field("a.drop").is_none());
assert_eq!(get_field_id(stats.field("a.keep").unwrap()), Some(10_400));
}
#[rstest]
#[case(MAX_DATA_FIELD_ID + 1)] #[case(-5)] fn stats_schema_out_of_range_struct_ancestor_keeps_leaves(#[case] bad_id: i32) {
let inner = StructType::new_unchecked([field_with_id("b", DataType::INTEGER, false, 2)]);
let schema = StructType::new_unchecked([field_with_id("a", inner.into(), true, bad_id)]);
let stats = stats_schema(&schema).expect("struct ancestor id is irrelevant");
assert_eq!(stats.fields().count(), 1);
assert_eq!(get_field_id(stats.field("a.b").unwrap()), Some(10_400));
}
#[test]
fn stats_schema_struct_ancestor_missing_field_id_keeps_leaves() {
let inner = StructType::new_unchecked([field_with_id("b", DataType::INTEGER, false, 2)]);
let schema = StructType::new_unchecked([StructField::nullable("a", inner)]);
let stats = stats_schema(&schema).expect("struct ancestor missing id is tolerated");
assert_eq!(stats.fields().count(), 1);
assert_eq!(get_field_id(stats.field("a.b").unwrap()), Some(10_400));
}
#[test]
fn stats_schema_empty_input_is_empty() {
let stats = stats_schema(&StructType::new_unchecked([])).expect("should succeed");
assert_eq!(stats.fields().count(), 0);
}
#[test]
fn stats_schema_with_metadata_columns_skips_unsupported() {
let id = field_with_id("id", DataType::LONG, false, 0);
let name = field_with_id("name", DataType::STRING, true, 1);
let score = field_with_id("score", DataType::DOUBLE, true, 2);
let file = field_with_id("_file", DataType::STRING, false, 2_147_483_646);
let pos = field_with_id("_pos", DataType::LONG, false, 2_147_483_645);
let row_id = field_with_id("_row_id", DataType::LONG, false, 2_147_483_540);
let last_updated_seq_no = field_with_id(
"_last_updated_sequence_number",
DataType::LONG,
false,
2_147_483_539,
);
let schema = StructType::new_unchecked([
id.clone(),
name.clone(),
score.clone(),
file,
pos,
row_id.clone(),
last_updated_seq_no.clone(),
]);
let stats = stats_schema(&schema).expect("stats_schema should succeed");
assert_eq!(stats.fields().count(), 5);
assert_stats_field_ids(&stats_struct_for_name(id.name(), &stats), 10_000, &id);
assert_stats_field_ids(&stats_struct_for_name(name.name(), &stats), 10_200, &name);
assert_stats_field_ids(&stats_struct_for_name(score.name(), &stats), 10_400, &score);
assert!(stats.field("_file").is_none());
assert!(stats.field("_pos").is_none());
assert_stats_field_ids(
&stats_struct_for_name(row_id.name(), &stats),
9_200,
&row_id,
);
assert_stats_field_ids(
&stats_struct_for_name(last_updated_seq_no.name(), &stats),
9_000,
&last_updated_seq_no,
);
}
#[test]
fn stats_schema_field_id_exceeding_i32_range_errors() {
let field = StructField::not_null("c", DataType::INTEGER).with_metadata([(
ColumnMetadataKey::ParquetFieldId.as_ref(),
MetadataValue::Number(i32::MAX as i64 + 1),
)]);
assert!(stats_schema(&StructType::new_unchecked([field])).is_err());
}
#[test]
fn stats_schema_leaf_name_with_dot_is_escaped_and_does_not_collide() {
let inner = StructType::new_unchecked([field_with_id("b", DataType::INTEGER, false, 2)]);
let dotted = field_with_id("a.b", DataType::INTEGER, false, 3);
let schema = StructType::new_unchecked([field_with_id("a", inner.into(), true, 1), dotted]);
let stats = stats_schema(&schema).expect("stats_schema should succeed");
assert_eq!(stats.fields().count(), 2);
assert_eq!(get_field_id(stats.field("a.b").unwrap()), Some(10_400));
let escaped = ColumnName::new(["a.b"]).to_string();
assert_ne!(
escaped, "a.b",
"a dotted leaf name must escape to avoid collision"
);
assert_eq!(get_field_id(stats.field(&escaped).unwrap()), Some(10_600));
}
#[test]
fn stats_schema_sibling_structs_preserve_order_and_path_prefixes() {
let a = field_with_id(
"a",
StructType::new_unchecked([
field_with_id("x", DataType::INTEGER, false, 10),
field_with_id("y", DataType::INTEGER, false, 11),
])
.into(),
true,
1,
);
let b = field_with_id(
"b",
StructType::new_unchecked([field_with_id("z", DataType::INTEGER, false, 20)]).into(),
true,
2,
);
let schema = StructType::new_unchecked([
field_with_id("top0", DataType::INTEGER, false, 5),
a,
b,
field_with_id("top1", DataType::INTEGER, false, 6),
]);
let stats = stats_schema(&schema).expect("stats_schema should succeed");
let names: Vec<_> = stats.fields().map(|f| f.name().to_string()).collect();
assert_eq!(names, ["top0", "a.x", "a.y", "b.z", "top1"]);
assert_eq!(
get_field_id(stats.field("a.y").unwrap()),
Some(10_000 + 200 * 11)
);
assert_eq!(
get_field_id(stats.field("b.z").unwrap()),
Some(10_000 + 200 * 20)
);
}
fn delta_stats(
null_count: Option<StructType>,
min_values: Option<StructType>,
max_values: Option<StructType>,
) -> StructType {
let categories = [
(NULL_COUNT, null_count),
(MIN_VALUES, min_values),
(MAX_VALUES, max_values),
];
let fields = categories
.into_iter()
.filter_map(|(name, sub)| sub.map(|s| StructField::nullable(name, s)));
StructType::new_unchecked(fields)
}
fn stat_cols<'a>(names: impl IntoIterator<Item = &'a str>) -> StructType {
StructType::new_unchecked(
names
.into_iter()
.map(|n| StructField::nullable(n, DataType::LONG)),
)
}
fn nested_cat<'a>(name: &str, leaves: impl IntoIterator<Item = &'a str>) -> StructType {
StructType::new_unchecked([StructField::nullable(name, stat_cols(leaves))])
}
fn struct_field_with_id(
name: &str,
field_id: i32,
children: impl IntoIterator<Item = StructField>,
) -> StructField {
field_with_id(
name,
StructType::new_unchecked(children).into(),
true,
field_id,
)
}
fn expected_leaf(
name: &str,
bounds_type: DataType,
field_id: i32,
categories: &[&str],
) -> StructField {
let present = StatCategories {
null_count: categories.contains(&NULL_COUNT),
min_values: categories.contains(&MIN_VALUES),
max_values: categories.contains(&MAX_VALUES),
};
let base =
field_id_to_statistics_base(field_id).expect("field id in supported stats range");
field_with_id(
name,
build_stats_struct(base, &bounds_type, Some(present)).into(),
true,
base,
)
}
#[test]
fn projected_leaf_in_all_categories_keeps_all_category_backed_subfields() {
let table = StructType::new_unchecked([
field_with_id("id", DataType::LONG, false, 0),
struct_field_with_id("a", 1, [field_with_id("b", DataType::INTEGER, true, 2)]),
field_with_id("v", DataType::unshredded_variant(), true, 3),
]);
let category = || {
StructType::new_unchecked([
StructField::nullable("id", DataType::LONG),
StructField::nullable("a", stat_cols(["b"])),
StructField::nullable("v", DataType::LONG),
])
};
let delta = delta_stats(Some(category()), Some(category()), Some(category()));
let projected = projected_stats_schema(&table, &delta).expect("projected should succeed");
let expected = StructType::new_unchecked([
expected_leaf("id", DataType::LONG, 0, &STAT_CATEGORIES),
expected_leaf("a.b", DataType::INTEGER, 2, &STAT_CATEGORIES),
expected_leaf("v", DataType::unshredded_variant(), 3, &STAT_CATEGORIES),
]);
assert_eq!(projected, expected);
}
#[test]
fn projected_drops_array_and_map_even_when_in_null_count() {
let table = StructType::new_unchecked([
field_with_id("id", DataType::LONG, false, 0),
field_with_id(
"arr",
DataType::Array(Box::new(ArrayType::new(DataType::INTEGER, true))),
true,
1,
),
field_with_id(
"m",
DataType::Map(Box::new(MapType::new(
DataType::STRING,
DataType::INTEGER,
true,
))),
true,
2,
),
]);
let delta = delta_stats(
Some(stat_cols(["id", "arr", "m"])),
Some(stat_cols(["id"])),
Some(stat_cols(["id"])),
);
let projected = projected_stats_schema(&table, &delta).expect("should succeed");
let expected =
StructType::new_unchecked([expected_leaf("id", DataType::LONG, 0, &STAT_CATEGORIES)]);
assert_eq!(projected, expected);
}
#[rstest]
#[case::int_null_count(
DataType::INTEGER,
delta_stats(Some(stat_cols(["c"])), None, None),
&[VALUE_COUNT, NULL_VALUE_COUNT],
)]
#[case::int_min(
DataType::INTEGER,
delta_stats(None, Some(stat_cols(["c"])), None),
&[LOWER_BOUND, TIGHT_BOUNDS],
)]
#[case::int_max(
DataType::INTEGER,
delta_stats(None, None, Some(stat_cols(["c"]))),
&[UPPER_BOUND, TIGHT_BOUNDS],
)]
#[case::int_min_and_max(
DataType::INTEGER,
delta_stats(None, Some(stat_cols(["c"])), Some(stat_cols(["c"]))),
&[LOWER_BOUND, UPPER_BOUND, TIGHT_BOUNDS],
)]
#[case::int_all_categories(
DataType::INTEGER,
delta_stats(Some(stat_cols(["c"])), Some(stat_cols(["c"])), Some(stat_cols(["c"]))),
&[LOWER_BOUND, UPPER_BOUND, TIGHT_BOUNDS, VALUE_COUNT, NULL_VALUE_COUNT],
)]
#[case::double_min_keeps_nan(
DataType::DOUBLE,
delta_stats(None, Some(stat_cols(["c"])), None),
&[LOWER_BOUND, TIGHT_BOUNDS, NAN_VALUE_COUNT],
)]
#[case::double_null_count_drops_nan(
DataType::DOUBLE,
delta_stats(Some(stat_cols(["c"])), None, None),
&[VALUE_COUNT, NULL_VALUE_COUNT],
)]
#[case::string_all_drops_avg(
DataType::STRING,
delta_stats(Some(stat_cols(["c"])), Some(stat_cols(["c"])), Some(stat_cols(["c"]))),
&[LOWER_BOUND, UPPER_BOUND, TIGHT_BOUNDS, VALUE_COUNT, NULL_VALUE_COUNT],
)]
#[case::variant_null_count_drops_avg(
DataType::unshredded_variant(),
delta_stats(Some(stat_cols(["c"])), None, None),
&[VALUE_COUNT, NULL_VALUE_COUNT],
)]
fn projected_leaf_keeps_only_category_backed_subfields(
#[case] leaf_type: DataType,
#[case] delta: StructType,
#[case] expected_subfields: &[&str],
) {
let table = StructType::new_unchecked([field_with_id("c", leaf_type, false, 1)]);
let projected = projected_stats_schema(&table, &delta).expect("should succeed");
let stats = stats_struct_for_name("c", &projected);
let names: Vec<&str> = stats.fields().map(|f| f.name().as_str()).collect();
assert_eq!(names, expected_subfields);
}
#[rstest]
#[case::each_via_a_different_category(
delta_stats(Some(nested_cat("a", ["c"])), Some(nested_cat("a", ["b"])), None),
StructType::new_unchecked([
expected_leaf("a.b", DataType::INTEGER, 2, &[MIN_VALUES]),
expected_leaf("a.c", DataType::STRING, 3, &[NULL_COUNT]),
]),
)]
#[case::sibling_absent_is_dropped(
delta_stats(None, Some(nested_cat("a", ["b"])), None),
StructType::new_unchecked([expected_leaf("a.b", DataType::INTEGER, 2, &[MIN_VALUES])]),
)]
fn projected_nested_leaf_membership_is_per_leaf(
#[case] delta: StructType,
#[case] expected: StructType,
) {
let table = StructType::new_unchecked([struct_field_with_id(
"a",
1,
[
field_with_id("b", DataType::INTEGER, true, 2),
field_with_id("c", DataType::STRING, true, 3),
],
)]);
let projected = projected_stats_schema(&table, &delta).expect("should succeed");
assert_eq!(projected, expected);
}
#[test]
fn projected_drops_entire_substruct_absent_from_all_categories() {
let table = StructType::new_unchecked([
struct_field_with_id("a", 1, [field_with_id("b", DataType::INTEGER, true, 2)]),
field_with_id("d", DataType::LONG, true, 3),
]);
let delta = delta_stats(Some(stat_cols(["d"])), None, None);
let projected = projected_stats_schema(&table, &delta).expect("should succeed");
let expected =
StructType::new_unchecked([expected_leaf("d", DataType::LONG, 3, &[NULL_COUNT])]);
assert_eq!(projected, expected);
}
#[test]
fn projected_errors_when_category_node_is_scalar_where_table_nests_struct() {
let table = StructType::new_unchecked([struct_field_with_id(
"zzz",
1,
[field_with_id("b", DataType::INTEGER, true, 2)],
)]);
let delta = delta_stats(Some(stat_cols(["zzz"])), None, None);
let err = projected_stats_schema(&table, &delta)
.expect_err("scalar-where-struct is an invariant violation")
.to_string();
assert!(
err.contains("invariant violation"),
"unexpected error: {err}"
);
assert!(
err.contains("nullCount"),
"error should name the category: {err}"
);
assert!(err.contains("zzz"), "error should name the path: {err}");
}
#[rstest]
#[case::present_in_null_count(
delta_stats(Some(stat_cols(["v"])), None, None),
StructType::new_unchecked([expected_leaf("v", DataType::unshredded_variant(), 3, &[NULL_COUNT])]),
)]
#[case::absent_from_all(delta_stats(None, None, None), StructType::new_unchecked([]))]
fn projected_variant_membership_matches_scalar_presence(
#[case] delta: StructType,
#[case] expected: StructType,
) {
let table = StructType::new_unchecked([field_with_id(
"v",
DataType::unshredded_variant(),
true,
3,
)]);
let projected = projected_stats_schema(&table, &delta).expect("should succeed");
assert_eq!(projected, expected);
}
#[rstest]
#[case::mismatch_in_min_values(delta_stats(None, Some(stat_cols(["a"])), None))]
#[case::mismatch_in_max_values(delta_stats(None, None, Some(stat_cols(["a"]))))]
fn projected_errors_on_shape_mismatch_in_any_category(#[case] delta: StructType) {
let table = StructType::new_unchecked([struct_field_with_id(
"a",
1,
[field_with_id("b", DataType::INTEGER, true, 2)],
)]);
assert!(projected_stats_schema(&table, &delta).is_err());
}
#[test]
fn projected_errors_on_nested_shape_mismatch() {
let table = StructType::new_unchecked([struct_field_with_id(
"a",
1,
[struct_field_with_id(
"b",
2,
[field_with_id("c", DataType::INTEGER, true, 3)],
)],
)]);
let delta = delta_stats(Some(nested_cat("a", ["b"])), None, None);
let err = projected_stats_schema(&table, &delta)
.expect_err("nested scalar-where-struct is an invariant violation")
.to_string();
assert!(
err.contains("invariant violation"),
"unexpected error: {err}"
);
}
#[test]
fn projected_errors_when_category_header_is_scalar() {
let table = StructType::new_unchecked([field_with_id("c", DataType::INTEGER, false, 1)]);
let delta = StructType::new_unchecked([StructField::nullable(NULL_COUNT, DataType::LONG)]);
let err = projected_stats_schema(&table, &delta)
.expect_err("scalar category header is an invariant violation")
.to_string();
assert!(
err.contains("invariant violation"),
"unexpected error: {err}"
);
assert!(
err.contains("nullCount"),
"error should name the category: {err}"
);
}
#[test]
fn projected_prunes_leaves_from_real_expected_stats_schema() {
let table = StructType::new_unchecked([
field_with_id("id", DataType::LONG, false, 0),
field_with_id("s", DataType::STRING, true, 1),
field_with_id("v", DataType::unshredded_variant(), true, 2),
]);
let config = StatsConfig {
data_skipping_stats_columns: None,
data_skipping_num_indexed_cols: Some(DataSkippingNumIndexedCols::AllColumns),
};
let delta = expected_stats_schema(&table, &config, None, None).expect("stats schema");
let projected = projected_stats_schema(&table, &delta).expect("projected should succeed");
let expected = StructType::new_unchecked([
expected_leaf("id", DataType::LONG, 0, &STAT_CATEGORIES),
expected_leaf("s", DataType::STRING, 1, &STAT_CATEGORIES),
expected_leaf("v", DataType::unshredded_variant(), 2, &[NULL_COUNT]),
]);
assert_eq!(projected, expected);
}
#[test]
fn projected_leaf_in_category_still_warn_dropped_when_out_of_range() {
let table = StructType::new_unchecked([field_with_id(
"c",
DataType::INTEGER,
false,
MAX_DATA_FIELD_ID + 1,
)]);
let delta = delta_stats(Some(stat_cols(["c"])), None, None);
let projected = projected_stats_schema(&table, &delta).expect("out-of-range warn-drops");
assert_eq!(projected.fields().count(), 0);
}
#[test]
fn projected_leaf_in_category_missing_field_id_still_errors() {
let table = StructType::new_unchecked([StructField::not_null("c", DataType::INTEGER)]);
let delta = delta_stats(Some(stat_cols(["c"])), None, None);
assert!(projected_stats_schema(&table, &delta).is_err());
}
#[test]
fn projected_restores_scope_for_sibling_struct_at_nested_level() {
let table = StructType::new_unchecked([struct_field_with_id(
"p",
1,
[
struct_field_with_id("x", 2, [field_with_id("a", DataType::INTEGER, true, 3)]),
struct_field_with_id("y", 4, [field_with_id("b", DataType::INTEGER, true, 5)]),
],
)]);
let null_count = StructType::new_unchecked([StructField::nullable(
"p",
StructType::new_unchecked([StructField::nullable("x", stat_cols(["a"]))]),
)]);
let min_values = StructType::new_unchecked([StructField::nullable(
"p",
StructType::new_unchecked([StructField::nullable("y", stat_cols(["b"]))]),
)]);
let delta = delta_stats(Some(null_count), Some(min_values), None);
let projected = projected_stats_schema(&table, &delta).expect("should succeed");
let expected = StructType::new_unchecked([
expected_leaf("p.x.a", DataType::INTEGER, 3, &[NULL_COUNT]),
expected_leaf("p.y.b", DataType::INTEGER, 5, &[MIN_VALUES]),
]);
assert_eq!(projected, expected);
}
#[rstest]
#[case::leaf_inside_category(
stat_cols(["nullCount"]),
StructType::new_unchecked([expected_leaf("nullCount", DataType::LONG, 1, &[NULL_COUNT])]),
)]
#[case::only_the_category_header(stat_cols(["other"]), StructType::new_unchecked([]))]
fn projected_handles_column_named_like_a_stat_category(
#[case] null_count: StructType,
#[case] expected: StructType,
) {
let table =
StructType::new_unchecked([field_with_id("nullCount", DataType::LONG, true, 1)]);
let delta = delta_stats(Some(null_count), None, None);
let projected = projected_stats_schema(&table, &delta).expect("should succeed");
assert_eq!(projected, expected);
}
#[rstest]
#[case::included_survives(
delta_stats(Some(stat_cols(["_row_id"])), None, None),
StructType::new_unchecked([expected_leaf("_row_id", DataType::LONG, ROW_ID_FIELD_ID, &[NULL_COUNT])]),
)]
#[case::omitted_dropped(delta_stats(None, None, None), StructType::new_unchecked([]))]
fn projected_reserved_metadata_leaf_requires_caller_inclusion(
#[case] delta: StructType,
#[case] expected: StructType,
) {
let table = StructType::new_unchecked([field_with_id(
"_row_id",
DataType::LONG,
true,
ROW_ID_FIELD_ID,
)]);
assert!(stats_schema(&table)
.expect("stats_schema should succeed")
.field("_row_id")
.is_some());
let projected = projected_stats_schema(&table, &delta).expect("should succeed");
assert_eq!(projected, expected);
}
}