1mod builder;
15mod pruning;
16mod reader;
17mod schema;
18pub mod writer;
19pub mod zone_map;
20
21use std::num::NonZeroUsize;
22use std::sync::Arc;
23
24pub(crate) use builder::AggregateStatsAccumulator;
25pub(crate) use builder::aggregate_partials;
26use prost::Message;
27pub use schema::MAX_IS_TRUNCATED;
28pub use schema::MIN_IS_TRUNCATED;
29use vortex_array::DeserializeMetadata;
30use vortex_array::SerializeMetadata;
31use vortex_array::aggregate_fn::AggregateFnRef;
32use vortex_array::dtype::DType;
33use vortex_array::dtype::TryFromBytes;
34use vortex_array::expr::stats::Stat;
35use vortex_array::stats::as_stat_bitset_bytes;
36use vortex_array::stats::stats_from_bitset_bytes;
37use vortex_error::VortexExpect;
38use vortex_error::VortexResult;
39use vortex_error::vortex_bail;
40use vortex_error::vortex_ensure;
41use vortex_error::vortex_ensure_eq;
42use vortex_error::vortex_panic;
43use vortex_session::VortexSession;
44use vortex_session::registry::CachedId;
45
46use crate::LayoutBuildContext;
47use crate::LayoutChildType;
48use crate::LayoutEncodingRef;
49use crate::LayoutId;
50use crate::LayoutReaderRef;
51use crate::LayoutRef;
52use crate::VTable;
53use crate::children::LayoutChildren;
54use crate::children::OwnedLayoutChildren;
55use crate::layouts::zoned::reader::ZonedReader;
56use crate::layouts::zoned::schema::AggregateSpecProto;
57use crate::layouts::zoned::schema::aggregate_fns_from_specs;
58use crate::layouts::zoned::schema::aggregate_specs_from_fns;
59use crate::layouts::zoned::schema::aggregate_stats_table_dtype;
60use crate::layouts::zoned::schema::legacy_stats_table_dtype;
61use crate::segments::SegmentId;
62use crate::segments::SegmentSource;
63use crate::vtable;
64
65vtable!(Zoned);
66vtable!(LegacyStats);
67
68impl VTable for Zoned {
69 type Layout = ZonedLayout;
70 type Encoding = ZonedLayoutEncoding;
71 type Metadata = ZonedMetadata;
72
73 fn id(_encoding: &Self::Encoding) -> LayoutId {
74 static ID: CachedId = CachedId::new("vortex.zoned");
75 *ID
76 }
77
78 fn encoding(_layout: &Self::Layout) -> LayoutEncodingRef {
79 LayoutEncodingRef::new_ref(ZonedLayoutEncoding.as_ref())
80 }
81
82 fn row_count(layout: &Self::Layout) -> u64 {
83 layout.children.child_row_count(0)
84 }
85
86 fn dtype(layout: &Self::Layout) -> &DType {
87 &layout.dtype
88 }
89
90 fn metadata(layout: &Self::Layout) -> Self::Metadata {
91 ZonedMetadata {
92 zone_len: u32::try_from(layout.zone_len).vortex_expect("Invalid zone length"),
93 aggregate_specs: match &layout.zone_map_schema {
94 ZoneMapSchema::AggregateFns(aggregate_fns) => {
95 aggregate_specs_from_fns(aggregate_fns).vortex_expect(
96 "aggregate functions should be validated as serializable during build",
97 )
98 }
99 ZoneMapSchema::LegacyStats(_) => {
100 vortex_panic!("Cannot serialize legacy stats schema as vortex.zoned")
101 }
102 },
103 }
104 }
105
106 fn segment_ids(_layout: &Self::Layout) -> Vec<SegmentId> {
107 vec![]
108 }
109
110 fn nchildren(_layout: &Self::Layout) -> usize {
111 2
112 }
113
114 fn child(layout: &Self::Layout, idx: usize) -> VortexResult<LayoutRef> {
115 match idx {
116 0 => layout.children.child(0, layout.dtype()),
117 1 => layout.children.child(1, &layout.stats_table_dtype),
118 _ => vortex_bail!("Invalid child index: {}", idx),
119 }
120 }
121
122 fn child_type(_layout: &Self::Layout, idx: usize) -> LayoutChildType {
123 match idx {
124 0 => LayoutChildType::Transparent("data".into()),
125 1 => LayoutChildType::Auxiliary("zones".into()),
126 _ => vortex_panic!("Invalid child index: {}", idx),
127 }
128 }
129
130 fn new_reader(
131 layout: &Self::Layout,
132 name: Arc<str>,
133 segment_source: Arc<dyn SegmentSource>,
134 session: &VortexSession,
135 ctx: &crate::LayoutReaderContext,
136 ) -> VortexResult<LayoutReaderRef> {
137 Ok(Arc::new(ZonedReader::try_new(
138 layout.clone(),
139 name,
140 segment_source,
141 session.clone(),
142 ctx.clone(),
143 )?))
144 }
145
146 fn build(
147 _encoding: &Self::Encoding,
148 dtype: &DType,
149 _row_count: u64,
150 metadata: &ZonedMetadata,
151 _segment_ids: Vec<SegmentId>,
152 children: &dyn LayoutChildren,
153 build_ctx: &LayoutBuildContext<'_>,
154 ) -> VortexResult<Self::Layout> {
155 vortex_ensure_eq!(
156 children.nchildren(),
157 2,
158 "ZonedLayout expects exactly 2 children (data, zones)"
159 );
160 let aggregate_fns = aggregate_fns_from_specs(&metadata.aggregate_specs, build_ctx.session)?;
161 aggregate_specs_from_fns(&aggregate_fns)?;
162 let stats_table_dtype = aggregate_stats_table_dtype(dtype, &aggregate_fns);
163 Ok(ZonedLayout {
164 dtype: dtype.clone(),
165 children: children.to_arc(),
166 zone_len: metadata.zone_len as usize,
167 zone_map_schema: ZoneMapSchema::AggregateFns(aggregate_fns),
168 stats_table_dtype,
169 })
170 }
171
172 fn with_children(layout: &mut Self::Layout, children: Vec<LayoutRef>) -> VortexResult<()> {
173 if children.len() != 2 {
174 vortex_bail!(
175 "ZonedLayout expects exactly 2 children (data, zones), got {}",
176 children.len()
177 );
178 }
179 layout.children = OwnedLayoutChildren::layout_children(children);
180 Ok(())
181 }
182}
183
184impl VTable for LegacyStats {
187 type Layout = LegacyStatsLayout;
188 type Encoding = LegacyStatsLayoutEncoding;
189 type Metadata = LegacyStatsMetadata;
190
191 fn id(_encoding: &Self::Encoding) -> LayoutId {
192 static ID: CachedId = CachedId::new("vortex.stats");
193 *ID
194 }
195
196 fn encoding(_layout: &Self::Layout) -> LayoutEncodingRef {
197 LayoutEncodingRef::new_ref(LegacyStatsLayoutEncoding.as_ref())
198 }
199
200 fn row_count(layout: &Self::Layout) -> u64 {
201 <Zoned as VTable>::row_count(&layout.0)
202 }
203
204 fn dtype(layout: &Self::Layout) -> &DType {
205 <Zoned as VTable>::dtype(&layout.0)
206 }
207
208 fn metadata(layout: &Self::Layout) -> Self::Metadata {
209 LegacyStatsMetadata {
210 zone_len: u32::try_from(layout.0.zone_len).vortex_expect("Invalid zone length"),
211 zone_map_schema: layout.0.zone_map_schema.clone(),
212 }
213 }
214
215 fn segment_ids(layout: &Self::Layout) -> Vec<SegmentId> {
216 <Zoned as VTable>::segment_ids(&layout.0)
217 }
218
219 fn nchildren(layout: &Self::Layout) -> usize {
220 <Zoned as VTable>::nchildren(&layout.0)
221 }
222
223 fn child(layout: &Self::Layout, idx: usize) -> VortexResult<LayoutRef> {
224 <Zoned as VTable>::child(&layout.0, idx)
225 }
226
227 fn child_type(layout: &Self::Layout, idx: usize) -> LayoutChildType {
228 <Zoned as VTable>::child_type(&layout.0, idx)
229 }
230
231 fn new_reader(
232 layout: &Self::Layout,
233 name: Arc<str>,
234 segment_source: Arc<dyn SegmentSource>,
235 session: &VortexSession,
236 ctx: &crate::LayoutReaderContext,
237 ) -> VortexResult<LayoutReaderRef> {
238 Ok(Arc::new(ZonedReader::try_new(
239 layout.0.clone(),
240 name,
241 segment_source,
242 session.clone(),
243 ctx.clone(),
244 )?))
245 }
246
247 fn build(
248 _encoding: &Self::Encoding,
249 dtype: &DType,
250 _row_count: u64,
251 metadata: &LegacyStatsMetadata,
252 _segment_ids: Vec<SegmentId>,
253 children: &dyn LayoutChildren,
254 _build_ctx: &LayoutBuildContext<'_>,
255 ) -> VortexResult<Self::Layout> {
256 vortex_ensure_eq!(
257 children.nchildren(),
258 2,
259 "LegacyStatsLayout expects exactly 2 children (data, zones)"
260 );
261 let stats_table_dtype = match &metadata.zone_map_schema {
262 ZoneMapSchema::LegacyStats(stats) => legacy_stats_table_dtype(dtype, stats),
263 ZoneMapSchema::AggregateFns(aggregate_fns) => {
264 aggregate_stats_table_dtype(dtype, aggregate_fns)
265 }
266 };
267 Ok(LegacyStatsLayout(ZonedLayout {
268 dtype: dtype.clone(),
269 children: children.to_arc(),
270 zone_len: metadata.zone_len as usize,
271 zone_map_schema: metadata.zone_map_schema.clone(),
272 stats_table_dtype,
273 }))
274 }
275
276 fn with_children(layout: &mut Self::Layout, children: Vec<LayoutRef>) -> VortexResult<()> {
277 <Zoned as VTable>::with_children(&mut layout.0, children)
278 }
279}
280
281#[derive(Debug)]
283pub struct ZonedLayoutEncoding;
284
285#[derive(Debug)]
287pub struct LegacyStatsLayoutEncoding;
288
289#[derive(Clone, Debug)]
295pub struct ZonedLayout {
296 dtype: DType,
297 children: Arc<dyn LayoutChildren>,
298 zone_len: usize,
299 zone_map_schema: ZoneMapSchema,
300 stats_table_dtype: DType,
301}
302
303#[derive(Clone, Debug)]
305pub struct LegacyStatsLayout(ZonedLayout);
306
307impl LegacyStatsLayout {
308 pub fn present_aggregates(&self) -> Arc<[String]> {
310 self.0.present_aggregates()
311 }
312}
313
314#[derive(Clone, Debug, PartialEq, Eq)]
315pub(crate) enum ZoneMapSchema {
316 LegacyStats(Arc<[Stat]>),
317 AggregateFns(Arc<[AggregateFnRef]>),
318}
319
320impl ZonedLayout {
321 pub fn try_new(
324 data: LayoutRef,
325 zones: LayoutRef,
326 zone_len: NonZeroUsize,
327 aggregate_fns: Arc<[AggregateFnRef]>,
328 ) -> VortexResult<Self> {
329 let expected_dtype = aggregate_stats_table_dtype(data.dtype(), &aggregate_fns);
330 if zones.dtype() != &expected_dtype {
331 vortex_bail!("Invalid zone map layout: zones dtype does not match expected dtype");
332 }
333 aggregate_specs_from_fns(&aggregate_fns)?;
334
335 Ok(Self {
336 dtype: data.dtype().clone(),
337 children: OwnedLayoutChildren::layout_children(vec![data, zones]),
338 zone_len: zone_len.get(),
339 zone_map_schema: ZoneMapSchema::AggregateFns(aggregate_fns),
340 stats_table_dtype: expected_dtype,
341 })
342 }
343
344 pub fn nzones(&self) -> usize {
345 usize::try_from(self.children.child_row_count(1)).vortex_expect("Invalid number of zones")
346 }
347
348 pub fn zone_len(&self) -> usize {
349 self.zone_len
350 }
351
352 pub fn present_aggregates(&self) -> Arc<[String]> {
354 match &self.zone_map_schema {
355 ZoneMapSchema::LegacyStats(stats) => stats
356 .iter()
357 .filter_map(Stat::aggregate_fn)
358 .map(|aggregate_fn| aggregate_fn.to_string())
359 .collect::<Vec<_>>()
360 .into(),
361 ZoneMapSchema::AggregateFns(aggregate_fns) => aggregate_fns
362 .iter()
363 .map(ToString::to_string)
364 .collect::<Vec<_>>()
365 .into(),
366 }
367 }
368
369 pub(super) fn aggregate_fns(
370 &self,
371 _session: &VortexSession,
372 ) -> VortexResult<Arc<[AggregateFnRef]>> {
373 match &self.zone_map_schema {
374 ZoneMapSchema::LegacyStats(stats) => Ok(stats
375 .iter()
376 .filter_map(Stat::aggregate_fn)
377 .collect::<Vec<_>>()
378 .into()),
379 ZoneMapSchema::AggregateFns(aggregate_fns) => Ok(Arc::clone(aggregate_fns)),
380 }
381 }
382
383 pub(super) fn stats_table_dtype_for(&self, aggregate_fns: &[AggregateFnRef]) -> DType {
384 if let ZoneMapSchema::LegacyStats(stats) = &self.zone_map_schema {
385 return legacy_stats_table_dtype(&self.dtype, stats);
386 }
387
388 aggregate_stats_table_dtype(&self.dtype, aggregate_fns)
389 }
390}
391
392#[derive(Debug, PartialEq, Eq, Clone)]
397pub struct ZonedMetadata {
398 pub(super) zone_len: u32,
399 pub(super) aggregate_specs: Arc<[AggregateSpecProto]>,
400}
401
402#[derive(Debug, PartialEq, Eq, Clone)]
404pub struct LegacyStatsMetadata {
405 pub(super) zone_len: u32,
406 pub(crate) zone_map_schema: ZoneMapSchema,
407}
408
409const ZONED_METADATA_PROTO_VERSION: u8 = 1;
410
411#[derive(Clone, PartialEq, Message)]
412struct ZonedMetadataProto {
413 #[prost(uint32, tag = "1")]
414 zone_len: u32,
415 #[prost(message, repeated, tag = "2")]
416 aggregate_specs: Vec<AggregateSpecProto>,
417}
418
419impl DeserializeMetadata for ZonedMetadata {
420 type Output = Self;
421
422 fn deserialize(metadata: &[u8]) -> VortexResult<Self::Output> {
423 let Some((&version, proto_bytes)) = metadata.split_first() else {
424 vortex_bail!("Zoned metadata missing protobuf version");
425 };
426
427 vortex_ensure!(
428 version == ZONED_METADATA_PROTO_VERSION,
429 "Unsupported zoned metadata version: {}",
430 version
431 );
432 vortex_ensure!(!proto_bytes.is_empty(), "Zoned metadata missing protobuf");
433
434 let proto = ZonedMetadataProto::decode(proto_bytes)?;
435 Ok(Self {
436 zone_len: proto.zone_len,
437 aggregate_specs: proto.aggregate_specs.into(),
438 })
439 }
440}
441
442impl SerializeMetadata for ZonedMetadata {
443 fn serialize(self) -> Vec<u8> {
444 let proto = ZonedMetadataProto {
445 zone_len: self.zone_len,
446 aggregate_specs: self.aggregate_specs.to_vec(),
447 };
448 let mut metadata = vec![ZONED_METADATA_PROTO_VERSION];
449 metadata.extend(proto.encode_to_vec());
450 metadata
451 }
452}
453
454impl DeserializeMetadata for LegacyStatsMetadata {
455 type Output = Self;
456
457 fn deserialize(metadata: &[u8]) -> VortexResult<Self::Output> {
458 vortex_ensure!(
459 metadata.len() >= 4,
460 "Legacy zoned metadata must contain at least 4 bytes for zone length, got {}",
461 metadata.len()
462 );
463
464 let zone_len = u32::try_from_le_bytes(&metadata[0..4])?;
468 let present_stats: Arc<[Stat]> = stats_from_bitset_bytes(&metadata[4..]).into();
469
470 Ok(Self {
471 zone_len,
472 zone_map_schema: ZoneMapSchema::LegacyStats(present_stats),
473 })
474 }
475}
476
477impl SerializeMetadata for LegacyStatsMetadata {
478 fn serialize(self) -> Vec<u8> {
479 match self.zone_map_schema {
480 ZoneMapSchema::LegacyStats(stats) => {
481 let mut metadata = self.zone_len.to_le_bytes().to_vec();
482 metadata.extend(as_stat_bitset_bytes(&stats));
483 metadata
484 }
485 ZoneMapSchema::AggregateFns(_) => {
486 vortex_panic!("Cannot serialize aggregate specs as legacy stats metadata")
487 }
488 }
489 }
490}
491
492#[cfg(test)]
493mod tests {
494 use std::panic;
495
496 use rstest::rstest;
497 use vortex_array::aggregate_fn::AggregateFnRef;
498 use vortex_array::aggregate_fn::AggregateFnVTableExt;
499 use vortex_array::aggregate_fn::NumericalAggregateOpts;
500 use vortex_array::aggregate_fn::fns::bounded_max::BoundedMax;
501 use vortex_array::aggregate_fn::fns::bounded_max::BoundedMaxOptions;
502 use vortex_array::aggregate_fn::fns::max::Max;
503 use vortex_array::aggregate_fn::fns::min::Min;
504 use vortex_array::aggregate_fn::session::AggregateFnSession;
505 use vortex_array::dtype::DType;
506 use vortex_array::dtype::Nullability;
507 use vortex_array::dtype::PType;
508 use vortex_array::stats::as_stat_bitset_bytes;
509 use vortex_session::VortexSession;
510 use vortex_session::registry::ReadContext;
511
512 use super::*;
513 use crate::IntoLayout;
514 use crate::children::OwnedLayoutChildren;
515 use crate::layouts::flat::FlatLayout;
516 use crate::segments::SegmentId;
517
518 fn aggregate_spec(aggregate_fn: AggregateFnRef) -> AggregateSpecProto {
519 AggregateSpecProto::try_from_aggregate_fn(&aggregate_fn).unwrap()
520 }
521
522 #[rstest]
523 #[case(ZonedMetadata {
524 zone_len: u32::MAX,
525 aggregate_specs: Arc::new([]),
526 })]
527 #[case::min_max(ZonedMetadata {
528 zone_len: 314,
529 aggregate_specs: Arc::new([
530 aggregate_spec(Max.bind(NumericalAggregateOpts::skip_nans())),
531 aggregate_spec(Min.bind(NumericalAggregateOpts::skip_nans())),
532 ]),
533 })]
534 fn test_metadata_serialization(#[case] metadata: ZonedMetadata) {
535 let serialized = metadata.clone().serialize();
536 assert_eq!(serialized[0], ZONED_METADATA_PROTO_VERSION);
537 let deserialized = ZonedMetadata::deserialize(&serialized).unwrap();
538 assert_eq!(deserialized, metadata);
539 }
540
541 #[test]
542 fn test_metadata_serialization_preserves_aggregate_options() -> VortexResult<()> {
543 let aggregate_fn = BoundedMax.bind(BoundedMaxOptions {
544 max_bytes: unsafe { NonZeroUsize::new_unchecked(128) },
546 });
547 let metadata = ZonedMetadata {
548 zone_len: 314,
549 aggregate_specs: Arc::new([AggregateSpecProto::try_from_aggregate_fn(&aggregate_fn)?]),
550 };
551
552 let deserialized = ZonedMetadata::deserialize(&metadata.serialize())?;
553 let session = VortexSession::empty().with::<AggregateFnSession>();
554 let aggregate_fns = aggregate_fns_from_specs(&deserialized.aggregate_specs, &session)?;
555
556 assert_eq!(aggregate_fns.as_ref(), std::slice::from_ref(&aggregate_fn));
557 Ok(())
558 }
559
560 #[test]
561 fn test_deserialize_legacy_stat_bitset_as_legacy_stats() {
562 let mut serialized = u32::MAX.to_le_bytes().to_vec();
563 serialized.extend(as_stat_bitset_bytes(&[
564 Stat::IsStrictSorted,
565 Stat::IsSorted,
566 Stat::Max,
567 ]));
568 let deserialized = LegacyStatsMetadata::deserialize(&serialized).unwrap();
569 let ZoneMapSchema::LegacyStats(legacy_stats) = deserialized.zone_map_schema else {
570 panic!("legacy bitset metadata should deserialize as legacy stats");
571 };
572
573 assert!(legacy_stats.is_sorted());
574 assert_eq!(
575 legacy_stats.as_ref(),
576 &[Stat::IsSorted, Stat::IsStrictSorted, Stat::Max]
577 );
578 }
579
580 #[rstest]
581 #[case::empty(vec![])]
582 #[case::unsupported_version(vec![0])]
583 #[case::missing_proto(vec![ZONED_METADATA_PROTO_VERSION])]
584 #[case::malformed_proto(vec![ZONED_METADATA_PROTO_VERSION, 0])]
585 fn test_deserialize_short_metadata_errors(#[case] metadata: Vec<u8>) {
586 assert!(ZonedMetadata::deserialize(&metadata).is_err());
587 }
588
589 #[test]
590 fn test_deserialize_short_metadata_returns_error_not_panic() {
591 let result = panic::catch_unwind(|| ZonedMetadata::deserialize(&[]));
592 assert!(
593 result.is_ok(),
594 "deserialize should return an error, not panic"
595 );
596 assert!(result.unwrap().is_err());
597 }
598
599 #[test]
600 fn test_deserialize_zero_zone_len_is_allowed_for_backcompat() {
601 let metadata = 0u32.to_le_bytes();
602 let deserialized = LegacyStatsMetadata::deserialize(&metadata).unwrap();
603 assert_eq!(deserialized.zone_len, 0);
604 let ZoneMapSchema::LegacyStats(legacy_stats) = deserialized.zone_map_schema else {
605 panic!("legacy bitset metadata should deserialize as legacy stats");
606 };
607 assert!(legacy_stats.is_empty());
608 }
609
610 #[test]
611 fn test_build_allows_zero_zone_len_for_backcompat() -> VortexResult<()> {
612 let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
613 let read_ctx = ReadContext::new([]);
614 let children = OwnedLayoutChildren::layout_children(vec![
615 FlatLayout::new(0, dtype.clone(), SegmentId::from(0), read_ctx.clone()).into_layout(),
616 FlatLayout::new(
617 0,
618 legacy_stats_table_dtype(&dtype, &[]),
619 SegmentId::from(1),
620 read_ctx,
621 )
622 .into_layout(),
623 ]);
624 let session = vortex_array::array_session();
625 let build_read_ctx = ReadContext::new([]);
626 let build_ctx = LayoutBuildContext {
627 session: &session,
628 array_read_ctx: &build_read_ctx,
629 };
630
631 let layout = <LegacyStats as VTable>::build(
632 &LegacyStatsLayoutEncoding,
633 &dtype,
634 0,
635 &LegacyStatsMetadata {
636 zone_len: 0,
637 zone_map_schema: ZoneMapSchema::LegacyStats(Arc::new([])),
638 },
639 vec![],
640 children.as_ref(),
641 &build_ctx,
642 )?;
643
644 assert_eq!(layout.0.zone_len, 0);
645 Ok(())
646 }
647
648 #[test]
649 fn test_build_rejects_invalid_child_count() {
650 let metadata = ZonedMetadata {
651 zone_len: 3,
652 aggregate_specs: Arc::new([]),
653 };
654 let children = OwnedLayoutChildren::layout_children(vec![]);
655 let session = vortex_array::array_session();
656 let build_read_ctx = ReadContext::new([]);
657 let build_ctx = LayoutBuildContext {
658 session: &session,
659 array_read_ctx: &build_read_ctx,
660 };
661
662 let result = <Zoned as VTable>::build(
663 &ZonedLayoutEncoding,
664 &DType::Primitive(PType::I32, Nullability::NonNullable),
665 0,
666 &metadata,
667 vec![],
668 children.as_ref(),
669 &build_ctx,
670 );
671
672 assert!(result.is_err());
673 }
674}