use vortex_array::dtype::Field;
use vortex_array::dtype::FieldPath;
use vortex_error::VortexResult;
use vortex_error::vortex_err;
use vortex_layout::LayoutChildType;
use vortex_layout::LayoutRef;
use vortex_layout::segments::SegmentId;
use vortex_utils::aliases::hash_map::Entry;
use vortex_utils::aliases::hash_map::HashMap;
use crate::footer::SegmentSpec;
#[derive(Debug, Clone)]
pub struct CompressedFieldSizes {
sizes: HashMap<FieldPath, u64>,
}
impl CompressedFieldSizes {
pub(crate) fn try_new(root: &LayoutRef, segments: &[SegmentSpec]) -> VortexResult<Self> {
let mut attribution = HashMap::<SegmentId, FieldPath>::default();
let mut sizes = HashMap::<FieldPath, u64>::default();
sizes.insert(FieldPath::root(), 0);
let mut stack = vec![(LayoutRef::clone(root), FieldPath::root())];
while let Some((layout, path)) = stack.pop() {
for segment_id in layout.segment_ids() {
match attribution.entry(segment_id) {
Entry::Occupied(mut entry) => {
let ancestor = common_prefix(entry.get(), &path);
entry.insert(ancestor);
}
Entry::Vacant(entry) => {
entry.insert(path.clone());
}
}
}
for idx in 0..layout.nchildren() {
let child_path = match layout.child_type(idx) {
LayoutChildType::Field(name) => {
let child_path = path.clone().push(name);
sizes.entry(child_path.clone()).or_insert(0);
child_path
}
_ => path.clone(),
};
stack.push((layout.child(idx)?, child_path));
}
}
for (segment_id, path) in attribution {
let segment = segments.get(*segment_id as usize).ok_or_else(|| {
vortex_err!(
"layout references missing segment {} (segment count: {})",
*segment_id,
segments.len()
)
})?;
for len in 0..=path.parts().len() {
*sizes
.entry(FieldPath::from(path.parts()[..len].to_vec()))
.or_insert(0) += u64::from(segment.length);
}
}
Ok(Self { sizes })
}
pub fn get(&self, path: &FieldPath) -> Option<u64> {
self.sizes.get(path).copied()
}
pub fn total(&self) -> u64 {
self.get(&FieldPath::root()).unwrap_or_default()
}
pub fn iter(&self) -> impl Iterator<Item = (&FieldPath, u64)> {
self.sizes.iter().map(|(path, size)| (path, *size))
}
}
fn common_prefix(left: &FieldPath, right: &FieldPath) -> FieldPath {
left.parts()
.iter()
.zip(right.parts())
.take_while(|(l, r)| l == r)
.map(|(l, _)| l.clone())
.collect::<Vec<Field>>()
.into()
}
#[cfg(test)]
mod tests {
use std::sync::LazyLock;
use vortex_array::IntoArray;
use vortex_array::array_session;
use vortex_array::arrays::StructArray;
use vortex_array::dtype::FieldPath;
use vortex_array::field_path;
use vortex_buffer::ByteBufferMut;
use vortex_buffer::buffer;
use vortex_error::VortexResult;
use vortex_io::session::RuntimeSession;
use vortex_layout::session::LayoutSession;
use vortex_session::VortexSession;
use crate::WriteOptionsSessionExt;
use crate::writer::WriteSummary;
static SESSION: LazyLock<VortexSession> = LazyLock::new(|| {
let session = array_session()
.with::<LayoutSession>()
.with::<RuntimeSession>();
crate::register_default_encodings(&session);
session
});
async fn write_nested() -> VortexResult<WriteSummary> {
let inner = StructArray::from_fields(&[
("c", buffer![10i64, 20, 30, 40].into_array()),
(
"d",
StructArray::from_fields(&[("e", buffer![1.0f64, 2.0, 3.0, 4.0].into_array())])?
.into_array(),
),
])?;
let root = StructArray::from_fields(&[
("a", buffer![1u32, 2, 3, 4].into_array()),
("b", inner.into_array()),
])?;
let mut buf = ByteBufferMut::empty();
SESSION
.write_options()
.write(&mut buf, root.into_array().to_array_stream())
.await
}
#[tokio::test]
async fn nested_field_sizes() -> VortexResult<()> {
let summary = write_nested().await?;
let sizes = summary.footer().compressed_field_sizes()?;
for path in [
field_path!(a),
field_path!(b),
field_path!(b.c),
field_path!(b.d),
field_path!(b.d.e),
] {
assert!(
sizes.get(&path).is_some_and(|size| size > 0),
"expected non-zero size for {path}, got {:?}",
sizes.get(&path)
);
}
assert_eq!(sizes.get(&field_path!(missing)), None);
assert_eq!(sizes.get(&field_path!(b.d.e.missing)), None);
let size = |path: FieldPath| sizes.get(&path).unwrap_or_default();
assert_eq!(
size(field_path!(b)),
size(field_path!(b.c)) + size(field_path!(b.d))
);
assert_eq!(size(field_path!(b.d)), size(field_path!(b.d.e)));
assert_eq!(sizes.total(), size(field_path!(a)) + size(field_path!(b)));
let segment_total: u64 = summary
.footer()
.segment_map()
.iter()
.map(|segment| u64::from(segment.length))
.sum();
assert_eq!(sizes.total(), segment_total);
Ok(())
}
#[tokio::test]
async fn column_sizes_in_schema_order() -> VortexResult<()> {
let summary = write_nested().await?;
let sizes = summary.footer().compressed_field_sizes()?;
assert_eq!(
summary.compressed_column_sizes()?,
vec![
sizes.get(&field_path!(a)).unwrap_or_default(),
sizes.get(&field_path!(b)).unwrap_or_default(),
]
);
Ok(())
}
#[tokio::test]
async fn non_struct_file_reports_single_column() -> VortexResult<()> {
let mut buf = ByteBufferMut::empty();
let summary = SESSION
.write_options()
.write(
&mut buf,
buffer![1i32, 2, 3, 4].into_array().to_array_stream(),
)
.await?;
let sizes = summary.footer().compressed_field_sizes()?;
assert!(sizes.total() > 0);
assert_eq!(sizes.get(&FieldPath::root()), Some(sizes.total()));
assert_eq!(summary.compressed_column_sizes()?, vec![sizes.total()]);
Ok(())
}
}