use tracing::warn;
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,
}
}
fn build_stats_struct(base_field_id: i32, bounds_type: &DataType) -> 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 specs = [
(
LOWER_BOUND,
bounds_type.clone(),
STATS_OFFSET_LOWER_BOUND,
true,
),
(
UPPER_BOUND,
bounds_type.clone(),
STATS_OFFSET_UPPER_BOUND,
true,
),
(
TIGHT_BOUNDS,
DataType::BOOLEAN,
STATS_OFFSET_TIGHT_BOUNDS,
!is_variant,
),
(VALUE_COUNT, DataType::LONG, STATS_OFFSET_VALUE_COUNT, true),
(
NULL_VALUE_COUNT,
DataType::LONG,
STATS_OFFSET_NULL_VALUE_COUNT,
true,
),
(
NAN_VALUE_COUNT,
DataType::LONG,
STATS_OFFSET_NAN_VALUE_COUNT,
has_nan_count,
),
(
AVG_VALUE_SIZE_IN_BYTES,
DataType::INTEGER,
STATS_OFFSET_AVG_VALUE_SIZE_IN_BYTES,
has_size_stats,
),
];
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]) -> 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()),
DataType::Variant(_) => build_stats_struct(base_stats_id, &DataType::unshredded_variant()),
_ => return Ok(None),
};
let name = ColumnName::new(path).to_string();
Ok(Some(field_with_id(
&name,
stats_struct.into(),
true,
base_stats_id,
)))
}
struct StatsSchemaCollector {
path: Vec<String>,
fields: Vec<StructField>,
}
impl<'a> SchemaTransform<'a> for StatsSchemaCollector {
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() {
self.recurse_into_struct_field(field)
} else {
leaf_stats_field(field, &self.path).map(|stats| self.fields.extend(stats))
};
self.path.pop();
result
}
}
pub(crate) fn stats_schema(table_struct: &StructType) -> DeltaResult<StructType> {
let mut collector = StatsSchemaCollector {
path: Vec::new(),
fields: Vec::new(),
};
collector.transform_struct(table_struct)?;
Ok(StructType::new_unchecked(collector.fields))
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
use crate::schema::{ArrayType, MapType};
#[cfg(feature = "geo-type-in-dev")]
use crate::schema::{EdgeInterpolationAlgorithm, GeographyType, GeometryType};
#[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(_));
assert_eq!(
get_field_id(stats_struct.field(VALUE_COUNT).unwrap()),
Some(base_id + STATS_OFFSET_VALUE_COUNT)
);
assert_eq!(
get_field_id(stats_struct.field(NULL_VALUE_COUNT).unwrap()),
Some(base_id + STATS_OFFSET_NULL_VALUE_COUNT)
);
if field.data_type() == &DataType::FLOAT || field.data_type() == &DataType::DOUBLE {
assert_eq!(
get_field_id(stats_struct.field(NAN_VALUE_COUNT).unwrap()),
Some(base_id + STATS_OFFSET_NAN_VALUE_COUNT)
);
}
if is_variant
|| field.data_type() == &DataType::STRING
|| field.data_type() == &DataType::BINARY
{
assert_eq!(
get_field_id(stats_struct.field(AVG_VALUE_SIZE_IN_BYTES).unwrap()),
Some(base_id + STATS_OFFSET_AVG_VALUE_SIZE_IN_BYTES)
);
}
assert_eq!(
get_field_id(stats_struct.field(LOWER_BOUND).unwrap()),
Some(base_id + STATS_OFFSET_LOWER_BOUND)
);
assert_eq!(
get_field_id(stats_struct.field(UPPER_BOUND).unwrap()),
Some(base_id + STATS_OFFSET_UPPER_BOUND)
);
if !is_variant {
assert_eq!(
get_field_id(stats_struct.field(TIGHT_BOUNDS).unwrap()),
Some(base_id + 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)
);
}
}