1use vortex_array::ArrayRef;
7use vortex_array::ArraySlots;
8use vortex_array::Canonical;
9use vortex_array::CanonicalValidity;
10use vortex_array::ExecutionCtx;
11use vortex_array::IntoArray;
12use vortex_array::arrays::Constant;
13use vortex_array::arrays::ConstantArray;
14use vortex_array::arrays::ExtensionArray;
15use vortex_array::arrays::FixedSizeListArray;
16use vortex_array::arrays::ListArray;
17use vortex_array::arrays::ListViewArray;
18use vortex_array::arrays::Masked;
19use vortex_array::arrays::PrimitiveArray;
20use vortex_array::arrays::StructArray;
21use vortex_array::arrays::Variant;
22use vortex_array::arrays::VariantArray;
23use vortex_array::arrays::extension::ExtensionArrayExt;
24use vortex_array::arrays::fixed_size_list::FixedSizeListArrayExt;
25use vortex_array::arrays::list::ListArrayExt;
26use vortex_array::arrays::listview::ListViewArrayExt;
27use vortex_array::arrays::listview::list_from_list_view;
28use vortex_array::arrays::masked::MaskedArraySlotsExt;
29use vortex_array::arrays::primitive::PrimitiveArrayExt;
30use vortex_array::arrays::scalar_fn::AnyScalarFn;
31use vortex_array::arrays::struct_::StructArrayExt;
32use vortex_array::arrays::variant::VariantArrayExt;
33use vortex_array::scalar::Scalar;
34use vortex_error::VortexResult;
35
36use crate::builtins::IntDictScheme;
37use crate::constant;
38use crate::ctx::CompressorContext;
39use crate::estimate::CompressionEstimate;
40use crate::estimate::DeferredEstimate;
41use crate::estimate::EstimateScore;
42use crate::estimate::EstimateVerdict;
43use crate::estimate::WinnerEstimate;
44use crate::estimate::estimate_compression_ratio_with_sampling;
45use crate::estimate::is_better_score;
46use crate::scheme::ChildSelection;
47use crate::scheme::DescendantExclusion;
48use crate::scheme::Scheme;
49use crate::scheme::SchemeExt;
50use crate::scheme::SchemeId;
51use crate::stats::ArrayAndStats;
52use crate::stats::GenerateStatsOptions;
53use crate::trace;
54
55pub(crate) const ROOT_SCHEME_ID: SchemeId = SchemeId {
57 name: "vortex.compressor.root",
58};
59
60mod root_list_children {
62 pub const OFFSETS: usize = 1;
64 pub const SIZES: usize = 2;
66}
67
68#[derive(Debug, Clone)]
86pub struct CascadingCompressor {
87 schemes: Vec<&'static dyn Scheme>,
89
90 root_exclusions: Vec<DescendantExclusion>,
93}
94
95impl CascadingCompressor {
96 pub fn new(schemes: Vec<&'static dyn Scheme>) -> Self {
101 let root_exclusions = vec![DescendantExclusion {
104 excluded: IntDictScheme.id(),
105 children: ChildSelection::One(root_list_children::OFFSETS),
106 }];
107 Self {
108 schemes,
109 root_exclusions,
110 }
111 }
112
113 pub fn compress(
121 &self,
122 array: &ArrayRef,
123 exec_ctx: &mut ExecutionCtx,
124 ) -> VortexResult<ArrayRef> {
125 let before_nbytes = array.nbytes();
126 let span = trace::compress_span(array.len(), array.dtype(), before_nbytes);
127 let _enter = span.enter();
128
129 let canonical = array.clone().execute::<CanonicalValidity>(exec_ctx)?.0;
130 let compact = canonical.compact(exec_ctx)?;
131 let compressed = self.compress_canonical(compact, CompressorContext::new(), exec_ctx)?;
132
133 trace::record_compress_outcome(&span, before_nbytes, compressed.nbytes());
134
135 Ok(compressed)
136 }
137
138 pub fn compress_child(
148 &self,
149 child: &ArrayRef,
150 parent_ctx: &CompressorContext,
151 parent_id: SchemeId,
152 child_index: usize,
153 exec_ctx: &mut ExecutionCtx,
154 ) -> VortexResult<ArrayRef> {
155 if parent_ctx.finished_cascading() {
156 trace::cascade_exhausted(parent_id, child_index);
157 return Ok(child.clone());
158 }
159
160 let canonical = child.clone().execute::<CanonicalValidity>(exec_ctx)?.0;
161 let compact = canonical.compact(exec_ctx)?;
162
163 let child_ctx = parent_ctx
164 .clone()
165 .descend_with_scheme(parent_id, child_index);
166 self.compress_canonical(compact, child_ctx, exec_ctx)
167 }
168
169 fn compress_canonical(
175 &self,
176 array: Canonical,
177 compress_ctx: CompressorContext,
178 exec_ctx: &mut ExecutionCtx,
179 ) -> VortexResult<ArrayRef> {
180 match array {
181 Canonical::Null(null_array) => Ok(null_array.into_array()),
182 Canonical::Bool(bool_array) => {
183 self.choose_and_compress(Canonical::Bool(bool_array), compress_ctx, exec_ctx)
184 }
185 Canonical::Primitive(primitive) => {
186 self.choose_and_compress(Canonical::Primitive(primitive), compress_ctx, exec_ctx)
187 }
188 Canonical::Decimal(decimal) => {
189 self.choose_and_compress(Canonical::Decimal(decimal), compress_ctx, exec_ctx)
190 }
191 Canonical::Struct(struct_array) => {
192 let fields = struct_array
193 .iter_unmasked_fields()
194 .map(|field| self.compress(field, exec_ctx))
195 .collect::<Result<Vec<_>, _>>()?;
196
197 Ok(StructArray::try_new(
198 struct_array.names().clone(),
199 fields,
200 struct_array.len(),
201 struct_array.validity()?,
202 )?
203 .into_array())
204 }
205 Canonical::List(list_view_array) => {
206 if list_view_array.is_zero_copy_to_list() || list_view_array.elements().is_empty() {
207 let list_array = list_from_list_view(list_view_array, exec_ctx)?;
208 self.compress_list_array(list_array, compress_ctx, exec_ctx)
209 } else {
210 self.compress_list_view_array(list_view_array, compress_ctx, exec_ctx)
211 }
212 }
213 Canonical::FixedSizeList(fsl_array) => {
214 let compressed_elems = self.compress(fsl_array.elements(), exec_ctx)?;
215
216 Ok(FixedSizeListArray::try_new(
217 compressed_elems,
218 fsl_array.list_size(),
219 fsl_array.validity()?,
220 fsl_array.len(),
221 )?
222 .into_array())
223 }
224 Canonical::VarBinView(varbinview) => {
225 self.choose_and_compress(Canonical::VarBinView(varbinview), compress_ctx, exec_ctx)
226 }
227 Canonical::Extension(ext_array) => {
228 let scheme_compressed = self.choose_and_compress(
230 Canonical::Extension(ext_array.clone()),
231 compress_ctx,
232 exec_ctx,
233 )?;
234 if scheme_compressed.is::<AnyScalarFn>() {
236 return Ok(scheme_compressed);
237 }
238
239 if scheme_compressed.is::<Constant>() {
242 return Ok(scheme_compressed);
243 }
244 if let Some(masked) = scheme_compressed.as_opt::<Masked>()
245 && masked.child().is::<Constant>()
246 {
247 return Ok(scheme_compressed);
248 }
249
250 let compressed_storage = self.compress(ext_array.storage_array(), exec_ctx)?;
253 let storage_compressed =
254 ExtensionArray::new(ext_array.ext_dtype().clone(), compressed_storage)
255 .into_array();
256
257 if scheme_compressed.nbytes() < storage_compressed.nbytes() {
258 Ok(scheme_compressed)
259 } else {
260 Ok(storage_compressed)
261 }
262 }
263 Canonical::Variant(variant_array) => {
264 let core_storage =
265 self.compress_physical_slots(variant_array.core_storage(), exec_ctx)?;
266 let shredded = variant_array
267 .shredded()
268 .map(|arr| {
269 if arr.is::<Variant>() {
271 self.compress_physical_slots(arr, exec_ctx)
272 } else {
273 self.compress(arr, exec_ctx)
274 }
275 })
276 .transpose()?;
277
278 Ok(VariantArray::try_new(core_storage, shredded)?.into_array())
279 }
280 }
281 }
282
283 fn choose_and_compress(
298 &self,
299 canonical: Canonical,
300 compress_ctx: CompressorContext,
301 exec_ctx: &mut ExecutionCtx,
302 ) -> VortexResult<ArrayRef> {
303 let eligible_schemes: Vec<&'static dyn Scheme> = self
304 .schemes
305 .iter()
306 .copied()
307 .filter(|s| s.matches(&canonical) && !self.is_excluded(*s, &compress_ctx))
308 .collect();
309
310 let array: ArrayRef = canonical.into();
311
312 if array.is_empty() {
313 return Ok(array);
314 }
315
316 if array.all_invalid(exec_ctx)? {
317 return Ok(
318 ConstantArray::new(Scalar::null(array.dtype().clone()), array.len()).into_array(),
319 );
320 }
321
322 let before_nbytes = array.nbytes();
323
324 let merged_opts = eligible_schemes
325 .iter()
326 .fold(GenerateStatsOptions::default(), |acc, s| {
327 acc.merge(s.stats_options())
328 });
329 let compress_ctx = compress_ctx.with_merged_stats_options(merged_opts);
330
331 let data = ArrayAndStats::new(array, merged_opts);
332
333 if !compress_ctx.is_sample() && constant::is_constant_for_compression(&data, exec_ctx)? {
337 let _winner_span =
338 trace::winner_compress_span(constant::CONSTANT_SCHEME_ID, before_nbytes).entered();
339 let compressed = constant::compress_constant(data.array(), exec_ctx)?;
340
341 let after_nbytes = compressed.nbytes();
342 let actual_ratio =
343 (after_nbytes != 0).then(|| before_nbytes as f64 / after_nbytes as f64);
344 let accepted = after_nbytes < before_nbytes;
345 trace::record_winner_compress_result(after_nbytes, None, actual_ratio, accepted);
346
347 return if accepted {
348 Ok(compressed)
349 } else {
350 Ok(data.into_array())
351 };
352 }
353
354 if eligible_schemes.is_empty() {
355 return Ok(data.into_array());
356 }
357
358 let Some((winner, winner_estimate)) =
359 self.choose_best_scheme(&eligible_schemes, &data, compress_ctx.clone(), exec_ctx)?
360 else {
361 return Ok(data.into_array());
362 };
363
364 let error_ctx = trace::enabled_error_context(&compress_ctx);
367 let _winner_span = trace::winner_compress_span(winner.id(), before_nbytes).entered();
368 let compressed = winner
369 .compress(self, &data, compress_ctx, exec_ctx)
370 .inspect_err(|err| {
371 trace::scheme_compress_failed(winner.id(), before_nbytes, error_ctx.as_ref(), err);
375 })?;
376
377 let after_nbytes = compressed.nbytes();
378 let actual_ratio = (after_nbytes != 0).then(|| before_nbytes as f64 / after_nbytes as f64);
379
380 let accepted = after_nbytes < before_nbytes || compressed.is::<AnyScalarFn>();
382
383 trace::record_winner_compress_result(
384 after_nbytes,
385 winner_estimate.trace_ratio(),
386 actual_ratio,
387 accepted,
388 );
389
390 if accepted {
391 Ok(compressed)
392 } else {
393 Ok(data.into_array())
394 }
395 }
396
397 fn choose_best_scheme(
413 &self,
414 schemes: &[&'static dyn Scheme],
415 data: &ArrayAndStats,
416 compress_ctx: CompressorContext,
417 exec_ctx: &mut ExecutionCtx,
418 ) -> VortexResult<Option<(&'static dyn Scheme, WinnerEstimate)>> {
419 let mut best: Option<(&'static dyn Scheme, EstimateScore)> = None;
420 let mut deferred: Vec<(&'static dyn Scheme, DeferredEstimate)> = Vec::new();
421
422 {
424 let _verdict_pass = trace::verdict_pass_span().entered();
425 for &scheme in schemes {
426 match scheme.expected_compression_ratio(data, compress_ctx.clone(), exec_ctx) {
427 CompressionEstimate::Verdict(EstimateVerdict::Skip) => {}
428 CompressionEstimate::Verdict(EstimateVerdict::AlwaysUse) => {
429 return Ok(Some((scheme, WinnerEstimate::AlwaysUse)));
430 }
431 CompressionEstimate::Verdict(EstimateVerdict::Ratio(ratio)) => {
432 let score = EstimateScore::FiniteCompression(ratio);
433
434 if is_better_score(score, best.as_ref()) {
435 best = Some((scheme, score));
436 }
437 }
438 CompressionEstimate::Deferred(deferred_estimate) => {
439 deferred.push((scheme, deferred_estimate));
440 }
441 }
442 }
443 }
444
445 for (scheme, deferred_estimate) in deferred {
448 let _span = trace::scheme_eval_span(scheme.id()).entered();
449 let threshold: Option<EstimateScore> = best.map(|(_, score)| score);
450 match deferred_estimate {
451 DeferredEstimate::Sample => {
452 let score = estimate_compression_ratio_with_sampling(
453 self,
454 scheme,
455 data.array(),
456 compress_ctx.clone(),
457 exec_ctx,
458 )?;
459
460 if is_better_score(score, best.as_ref()) {
461 best = Some((scheme, score));
462 }
463 }
464 DeferredEstimate::Callback(callback) => {
465 match callback(self, data, threshold, compress_ctx.clone(), exec_ctx)? {
466 EstimateVerdict::Skip => {}
467 EstimateVerdict::AlwaysUse => {
468 return Ok(Some((scheme, WinnerEstimate::AlwaysUse)));
469 }
470 EstimateVerdict::Ratio(ratio) => {
471 let score = EstimateScore::FiniteCompression(ratio);
472
473 if is_better_score(score, best.as_ref()) {
474 best = Some((scheme, score));
475 }
476 }
477 }
478 }
479 }
480 }
481
482 Ok(best.map(|(scheme, score)| (scheme, WinnerEstimate::Score(score))))
483 }
484
485 fn is_excluded(&self, candidate: &dyn Scheme, ctx: &CompressorContext) -> bool {
489 let id = candidate.id();
490 let history = ctx.cascade_history();
491
492 if history.iter().any(|&(sid, _)| sid == id) {
494 return true;
495 }
496
497 let mut iter = history.iter().copied().peekable();
498
499 if let Some((_, child_idx)) = iter.next_if(|&(sid, _)| sid == ROOT_SCHEME_ID)
502 && self
503 .root_exclusions
504 .iter()
505 .any(|rule| rule.excluded == id && rule.children.contains(child_idx))
506 {
507 return true;
508 }
509
510 for (ancestor_id, child_idx) in iter {
512 if let Some(ancestor) = self.schemes.iter().find(|s| s.id() == ancestor_id)
513 && ancestor
514 .descendant_exclusions()
515 .iter()
516 .any(|rule| rule.excluded == id && rule.children.contains(child_idx))
517 {
518 return true;
519 }
520 }
521
522 for rule in candidate.ancestor_exclusions() {
524 if history
525 .iter()
526 .any(|(sid, cidx)| *sid == rule.ancestor && rule.children.contains(*cidx))
527 {
528 return true;
529 }
530 }
531
532 false
533 }
534
535 fn compress_list_array(
537 &self,
538 list_array: ListArray,
539 compress_ctx: CompressorContext,
540 exec_ctx: &mut ExecutionCtx,
541 ) -> VortexResult<ArrayRef> {
542 let list_array = list_array.reset_offsets(true, exec_ctx)?;
543
544 let compressed_elems = self.compress(list_array.elements(), exec_ctx)?;
545
546 let offset_ctx =
548 compress_ctx.descend_with_scheme(ROOT_SCHEME_ID, root_list_children::OFFSETS);
549 let list_offsets_primitive = list_array
550 .offsets()
551 .clone()
552 .execute::<PrimitiveArray>(exec_ctx)?
553 .narrow(exec_ctx)?;
554 let compressed_offsets = self.compress_canonical(
555 Canonical::Primitive(list_offsets_primitive),
556 offset_ctx,
557 exec_ctx,
558 )?;
559
560 Ok(
561 ListArray::try_new(compressed_elems, compressed_offsets, list_array.validity()?)?
562 .into_array(),
563 )
564 }
565
566 fn compress_list_view_array(
569 &self,
570 list_view: ListViewArray,
571 compress_ctx: CompressorContext,
572 exec_ctx: &mut ExecutionCtx,
573 ) -> VortexResult<ArrayRef> {
574 let compressed_elems = self.compress(list_view.elements(), exec_ctx)?;
575
576 let offset_ctx = compress_ctx
577 .clone()
578 .descend_with_scheme(ROOT_SCHEME_ID, root_list_children::OFFSETS);
579 let list_view_offsets_primitive = list_view
580 .offsets()
581 .clone()
582 .execute::<PrimitiveArray>(exec_ctx)?
583 .narrow(exec_ctx)?;
584 let compressed_offsets = self.compress_canonical(
585 Canonical::Primitive(list_view_offsets_primitive),
586 offset_ctx,
587 exec_ctx,
588 )?;
589
590 let sizes_ctx = compress_ctx.descend_with_scheme(ROOT_SCHEME_ID, root_list_children::SIZES);
591 let list_view_sizes_primitive = list_view
592 .sizes()
593 .clone()
594 .execute::<PrimitiveArray>(exec_ctx)?
595 .narrow(exec_ctx)?;
596 let compressed_sizes = self.compress_canonical(
597 Canonical::Primitive(list_view_sizes_primitive),
598 sizes_ctx,
599 exec_ctx,
600 )?;
601
602 Ok(ListViewArray::try_new(
603 compressed_elems,
604 compressed_offsets,
605 compressed_sizes,
606 list_view.validity()?,
607 )?
608 .into_array())
609 }
610
611 fn compress_physical_slots(
613 &self,
614 array: &ArrayRef,
615 exec_ctx: &mut ExecutionCtx,
616 ) -> VortexResult<ArrayRef> {
617 let slots = array
618 .slots()
619 .iter()
620 .map(|slot| {
621 slot.as_ref()
622 .map(|child| self.compress(child, exec_ctx))
623 .transpose()
624 })
625 .collect::<VortexResult<ArraySlots>>()?;
626
627 unsafe { array.clone().with_slots(slots) }
630 }
631}
632
633#[cfg(test)]
634mod tests {
635 use std::sync::LazyLock;
636
637 use parking_lot::Mutex;
638 use vortex_array::ArrayRef;
639 use vortex_array::Canonical;
640 use vortex_array::VortexSessionExecute;
641 use vortex_array::arrays::BoolArray;
642 use vortex_array::arrays::Constant;
643 use vortex_array::arrays::NullArray;
644 use vortex_array::arrays::PrimitiveArray;
645 use vortex_array::validity::Validity;
646 use vortex_buffer::buffer;
647 use vortex_session::VortexSession;
648
649 use super::*;
650 use crate::builtins::FloatDictScheme;
651 use crate::builtins::IntDictScheme;
652 use crate::builtins::StringDictScheme;
653 use crate::ctx::CompressorContext;
654 use crate::estimate::CompressionEstimate;
655 use crate::estimate::DeferredEstimate;
656 use crate::estimate::EstimateScore;
657 use crate::estimate::EstimateVerdict;
658 use crate::estimate::WinnerEstimate;
659 use crate::scheme::SchemeExt;
660
661 static SESSION: LazyLock<VortexSession> = LazyLock::new(vortex_array::array_session);
662
663 fn compressor() -> CascadingCompressor {
664 CascadingCompressor::new(vec![&IntDictScheme, &FloatDictScheme, &StringDictScheme])
665 }
666
667 fn estimate_test_data() -> ArrayAndStats {
668 let array = PrimitiveArray::new(buffer![1i32, 2, 3, 4], Validity::NonNullable).into_array();
669 ArrayAndStats::new(array, GenerateStatsOptions::default())
670 }
671
672 fn matches_integer_primitive(canonical: &Canonical) -> bool {
673 matches!(canonical, Canonical::Primitive(primitive) if primitive.ptype().is_int())
674 }
675
676 #[derive(Debug)]
677 struct DirectRatioScheme;
678
679 impl Scheme for DirectRatioScheme {
680 fn scheme_name(&self) -> &'static str {
681 "test.direct_ratio"
682 }
683
684 fn matches(&self, canonical: &Canonical) -> bool {
685 matches_integer_primitive(canonical)
686 }
687
688 fn expected_compression_ratio(
689 &self,
690 _data: &ArrayAndStats,
691 _compress_ctx: CompressorContext,
692 _exec_ctx: &mut ExecutionCtx,
693 ) -> CompressionEstimate {
694 CompressionEstimate::Verdict(EstimateVerdict::Ratio(2.0))
695 }
696
697 fn compress(
698 &self,
699 _compressor: &CascadingCompressor,
700 _data: &ArrayAndStats,
701 _compress_ctx: CompressorContext,
702 _exec_ctx: &mut ExecutionCtx,
703 ) -> VortexResult<ArrayRef> {
704 unreachable!("test helper should never be selected for compression")
705 }
706 }
707
708 #[derive(Debug)]
709 struct ImmediateAlwaysUseScheme;
710
711 impl Scheme for ImmediateAlwaysUseScheme {
712 fn scheme_name(&self) -> &'static str {
713 "test.immediate_always_use"
714 }
715
716 fn matches(&self, canonical: &Canonical) -> bool {
717 matches_integer_primitive(canonical)
718 }
719
720 fn expected_compression_ratio(
721 &self,
722 _data: &ArrayAndStats,
723 _compress_ctx: CompressorContext,
724 _exec_ctx: &mut ExecutionCtx,
725 ) -> CompressionEstimate {
726 CompressionEstimate::Verdict(EstimateVerdict::AlwaysUse)
727 }
728
729 fn compress(
730 &self,
731 _compressor: &CascadingCompressor,
732 _data: &ArrayAndStats,
733 _compress_ctx: CompressorContext,
734 _exec_ctx: &mut ExecutionCtx,
735 ) -> VortexResult<ArrayRef> {
736 unreachable!("test helper should never be selected for compression")
737 }
738 }
739
740 #[derive(Debug)]
741 struct CallbackAlwaysUseScheme;
742
743 impl Scheme for CallbackAlwaysUseScheme {
744 fn scheme_name(&self) -> &'static str {
745 "test.callback_always_use"
746 }
747
748 fn matches(&self, canonical: &Canonical) -> bool {
749 matches_integer_primitive(canonical)
750 }
751
752 fn expected_compression_ratio(
753 &self,
754 _data: &ArrayAndStats,
755 _compress_ctx: CompressorContext,
756 _exec_ctx: &mut ExecutionCtx,
757 ) -> CompressionEstimate {
758 CompressionEstimate::Deferred(DeferredEstimate::Callback(Box::new(
759 |_compressor, _data, _ctx, _exec_ctx, _best_so_far| Ok(EstimateVerdict::AlwaysUse),
760 )))
761 }
762
763 fn compress(
764 &self,
765 _compressor: &CascadingCompressor,
766 _data: &ArrayAndStats,
767 _compress_ctx: CompressorContext,
768 _exec_ctx: &mut ExecutionCtx,
769 ) -> VortexResult<ArrayRef> {
770 unreachable!("test helper should never be selected for compression")
771 }
772 }
773
774 #[derive(Debug)]
775 struct CallbackSkipScheme;
776
777 impl Scheme for CallbackSkipScheme {
778 fn scheme_name(&self) -> &'static str {
779 "test.callback_skip"
780 }
781
782 fn matches(&self, canonical: &Canonical) -> bool {
783 matches_integer_primitive(canonical)
784 }
785
786 fn expected_compression_ratio(
787 &self,
788 _data: &ArrayAndStats,
789 _compress_ctx: CompressorContext,
790 _exec_ctx: &mut ExecutionCtx,
791 ) -> CompressionEstimate {
792 CompressionEstimate::Deferred(DeferredEstimate::Callback(Box::new(
793 |_compressor, _data, _ctx, _exec_ctx, _best_so_far| Ok(EstimateVerdict::Skip),
794 )))
795 }
796
797 fn compress(
798 &self,
799 _compressor: &CascadingCompressor,
800 _data: &ArrayAndStats,
801 _compress_ctx: CompressorContext,
802 _exec_ctx: &mut ExecutionCtx,
803 ) -> VortexResult<ArrayRef> {
804 unreachable!("test helper should never be selected for compression")
805 }
806 }
807
808 #[derive(Debug)]
809 struct CallbackRatioScheme;
810
811 impl Scheme for CallbackRatioScheme {
812 fn scheme_name(&self) -> &'static str {
813 "test.callback_ratio"
814 }
815
816 fn matches(&self, canonical: &Canonical) -> bool {
817 matches_integer_primitive(canonical)
818 }
819
820 fn expected_compression_ratio(
821 &self,
822 _data: &ArrayAndStats,
823 _compress_ctx: CompressorContext,
824 _exec_ctx: &mut ExecutionCtx,
825 ) -> CompressionEstimate {
826 CompressionEstimate::Deferred(DeferredEstimate::Callback(Box::new(
827 |_compressor, _data, _ctx, _exec_ctx, _best_so_far| Ok(EstimateVerdict::Ratio(3.0)),
828 )))
829 }
830
831 fn compress(
832 &self,
833 _compressor: &CascadingCompressor,
834 _data: &ArrayAndStats,
835 _compress_ctx: CompressorContext,
836 _exec_ctx: &mut ExecutionCtx,
837 ) -> VortexResult<ArrayRef> {
838 unreachable!("test helper should never be selected for compression")
839 }
840 }
841
842 #[derive(Debug)]
843 struct HugeRatioScheme;
844
845 impl Scheme for HugeRatioScheme {
846 fn scheme_name(&self) -> &'static str {
847 "test.huge_ratio"
848 }
849
850 fn matches(&self, canonical: &Canonical) -> bool {
851 matches_integer_primitive(canonical)
852 }
853
854 fn expected_compression_ratio(
855 &self,
856 _data: &ArrayAndStats,
857 _compress_ctx: CompressorContext,
858 _exec_ctx: &mut ExecutionCtx,
859 ) -> CompressionEstimate {
860 CompressionEstimate::Verdict(EstimateVerdict::Ratio(100.0))
861 }
862
863 fn compress(
864 &self,
865 _compressor: &CascadingCompressor,
866 _data: &ArrayAndStats,
867 _compress_ctx: CompressorContext,
868 _exec_ctx: &mut ExecutionCtx,
869 ) -> VortexResult<ArrayRef> {
870 unreachable!("test helper should never be selected for compression")
871 }
872 }
873
874 #[derive(Debug)]
875 struct ZeroBytesSamplingScheme;
876
877 impl Scheme for ZeroBytesSamplingScheme {
878 fn scheme_name(&self) -> &'static str {
879 "test.zero_bytes_sampling"
880 }
881
882 fn matches(&self, canonical: &Canonical) -> bool {
883 matches_integer_primitive(canonical)
884 }
885
886 fn expected_compression_ratio(
887 &self,
888 _data: &ArrayAndStats,
889 _compress_ctx: CompressorContext,
890 _exec_ctx: &mut ExecutionCtx,
891 ) -> CompressionEstimate {
892 CompressionEstimate::Deferred(DeferredEstimate::Sample)
893 }
894
895 fn compress(
896 &self,
897 _compressor: &CascadingCompressor,
898 data: &ArrayAndStats,
899 _compress_ctx: CompressorContext,
900 _exec_ctx: &mut ExecutionCtx,
901 ) -> VortexResult<ArrayRef> {
902 Ok(NullArray::new(data.array().len()).into_array())
903 }
904 }
905
906 #[test]
907 fn test_self_exclusion() {
908 let c = compressor();
909 let ctx = CompressorContext::default().descend_with_scheme(IntDictScheme.id(), 0);
910
911 assert!(c.is_excluded(&IntDictScheme, &ctx));
913 }
914
915 #[test]
916 fn test_root_exclusion_list_offsets() {
917 let c = compressor();
918 let ctx = CompressorContext::default()
919 .descend_with_scheme(ROOT_SCHEME_ID, root_list_children::OFFSETS);
920
921 assert!(c.is_excluded(&IntDictScheme, &ctx));
923 }
924
925 #[test]
926 fn test_push_rule_float_dict_excludes_int_dict_from_codes() {
927 let c = compressor();
928 let ctx = CompressorContext::default().descend_with_scheme(FloatDictScheme.id(), 1);
930
931 assert!(c.is_excluded(&IntDictScheme, &ctx));
933 }
934
935 #[test]
936 fn test_push_rule_float_dict_excludes_int_dict_from_values() {
937 let c = compressor();
938 let ctx = CompressorContext::default().descend_with_scheme(FloatDictScheme.id(), 0);
940
941 assert!(c.is_excluded(&IntDictScheme, &ctx));
944 }
945
946 #[test]
947 fn test_no_exclusion_without_history() {
948 let c = compressor();
949 let ctx = CompressorContext::default();
950
951 assert!(!c.is_excluded(&IntDictScheme, &ctx));
953 }
954
955 #[test]
956 fn immediate_always_use_wins_immediately() -> VortexResult<()> {
957 let compressor =
958 CascadingCompressor::new(vec![&DirectRatioScheme, &ImmediateAlwaysUseScheme]);
959 let schemes: [&'static dyn Scheme; 2] = [&DirectRatioScheme, &ImmediateAlwaysUseScheme];
960 let data = estimate_test_data();
961 let mut exec_ctx = SESSION.create_execution_ctx();
962
963 let winner = compressor.choose_best_scheme(
964 &schemes,
965 &data,
966 CompressorContext::new(),
967 &mut exec_ctx,
968 )?;
969
970 assert!(matches!(
971 winner,
972 Some((scheme, WinnerEstimate::AlwaysUse))
973 if scheme.id() == ImmediateAlwaysUseScheme.id()
974 ));
975 Ok(())
976 }
977
978 #[test]
979 fn callback_always_use_wins_immediately() -> VortexResult<()> {
980 let compressor =
981 CascadingCompressor::new(vec![&DirectRatioScheme, &CallbackAlwaysUseScheme]);
982 let schemes: [&'static dyn Scheme; 2] = [&DirectRatioScheme, &CallbackAlwaysUseScheme];
983 let data = estimate_test_data();
984 let mut exec_ctx = SESSION.create_execution_ctx();
985
986 let winner = compressor.choose_best_scheme(
987 &schemes,
988 &data,
989 CompressorContext::new(),
990 &mut exec_ctx,
991 )?;
992
993 assert!(matches!(
994 winner,
995 Some((scheme, WinnerEstimate::AlwaysUse))
996 if scheme.id() == CallbackAlwaysUseScheme.id()
997 ));
998 Ok(())
999 }
1000
1001 #[test]
1002 fn callback_skip_is_ignored() -> VortexResult<()> {
1003 let compressor = CascadingCompressor::new(vec![&CallbackSkipScheme, &DirectRatioScheme]);
1004 let schemes: [&'static dyn Scheme; 2] = [&CallbackSkipScheme, &DirectRatioScheme];
1005 let data = estimate_test_data();
1006 let mut exec_ctx = SESSION.create_execution_ctx();
1007
1008 let winner = compressor.choose_best_scheme(
1009 &schemes,
1010 &data,
1011 CompressorContext::new(),
1012 &mut exec_ctx,
1013 )?;
1014
1015 assert!(matches!(
1016 winner,
1017 Some((scheme, WinnerEstimate::Score(EstimateScore::FiniteCompression(2.0))))
1018 if scheme.id() == DirectRatioScheme.id()
1019 ));
1020 Ok(())
1021 }
1022
1023 #[test]
1024 fn callback_ratio_competes_numerically() -> VortexResult<()> {
1025 let compressor = CascadingCompressor::new(vec![&DirectRatioScheme, &CallbackRatioScheme]);
1026 let schemes: [&'static dyn Scheme; 2] = [&DirectRatioScheme, &CallbackRatioScheme];
1027 let data = estimate_test_data();
1028 let mut exec_ctx = SESSION.create_execution_ctx();
1029
1030 let winner = compressor.choose_best_scheme(
1031 &schemes,
1032 &data,
1033 CompressorContext::new(),
1034 &mut exec_ctx,
1035 )?;
1036
1037 assert!(matches!(
1038 winner,
1039 Some((scheme, WinnerEstimate::Score(EstimateScore::FiniteCompression(3.0))))
1040 if scheme.id() == CallbackRatioScheme.id()
1041 ));
1042 Ok(())
1043 }
1044
1045 #[test]
1046 fn zero_byte_sample_loses_to_finite_ratio() -> VortexResult<()> {
1047 let compressor = CascadingCompressor::new(vec![&HugeRatioScheme, &ZeroBytesSamplingScheme]);
1048 let schemes: [&'static dyn Scheme; 2] = [&HugeRatioScheme, &ZeroBytesSamplingScheme];
1049 let data = estimate_test_data();
1050 let mut exec_ctx = SESSION.create_execution_ctx();
1051
1052 let winner = compressor.choose_best_scheme(
1053 &schemes,
1054 &data,
1055 CompressorContext::new(),
1056 &mut exec_ctx,
1057 )?;
1058
1059 assert!(matches!(
1060 winner,
1061 Some((scheme, WinnerEstimate::Score(EstimateScore::FiniteCompression(100.0))))
1062 if scheme.id() == HugeRatioScheme.id()
1063 ));
1064 Ok(())
1065 }
1066
1067 #[test]
1068 fn finite_ratio_displaces_zero_byte_sample() -> VortexResult<()> {
1069 let compressor = CascadingCompressor::new(vec![&ZeroBytesSamplingScheme, &HugeRatioScheme]);
1070 let schemes: [&'static dyn Scheme; 2] = [&ZeroBytesSamplingScheme, &HugeRatioScheme];
1071 let data = estimate_test_data();
1072 let mut exec_ctx = SESSION.create_execution_ctx();
1073
1074 let winner = compressor.choose_best_scheme(
1075 &schemes,
1076 &data,
1077 CompressorContext::new(),
1078 &mut exec_ctx,
1079 )?;
1080
1081 assert!(matches!(
1082 winner,
1083 Some((scheme, WinnerEstimate::Score(EstimateScore::FiniteCompression(100.0))))
1084 if scheme.id() == HugeRatioScheme.id()
1085 ));
1086 Ok(())
1087 }
1088
1089 #[test]
1090 fn zero_byte_sample_alone_selects_no_scheme() -> VortexResult<()> {
1091 let compressor = CascadingCompressor::new(vec![&ZeroBytesSamplingScheme]);
1092 let schemes: [&'static dyn Scheme; 1] = [&ZeroBytesSamplingScheme];
1093 let data = estimate_test_data();
1094 let mut exec_ctx = SESSION.create_execution_ctx();
1095
1096 let winner = compressor.choose_best_scheme(
1097 &schemes,
1098 &data,
1099 CompressorContext::new(),
1100 &mut exec_ctx,
1101 )?;
1102
1103 assert!(winner.is_none());
1104 Ok(())
1105 }
1106
1107 static OBSERVER_LOCK: Mutex<()> = Mutex::new(());
1111 static OBSERVED_THRESHOLD: Mutex<Option<Option<EstimateScore>>> = Mutex::new(None);
1112
1113 #[derive(Debug)]
1114 struct ThresholdObservingScheme;
1115
1116 impl Scheme for ThresholdObservingScheme {
1117 fn scheme_name(&self) -> &'static str {
1118 "test.threshold_observing"
1119 }
1120
1121 fn matches(&self, canonical: &Canonical) -> bool {
1122 matches_integer_primitive(canonical)
1123 }
1124
1125 fn expected_compression_ratio(
1126 &self,
1127 _data: &ArrayAndStats,
1128 _compress_ctx: CompressorContext,
1129 _exec_ctx: &mut ExecutionCtx,
1130 ) -> CompressionEstimate {
1131 CompressionEstimate::Deferred(DeferredEstimate::Callback(Box::new(
1132 |_compressor, _data, best_so_far, _ctx, _exec_ctx| {
1133 *OBSERVED_THRESHOLD.lock() = Some(best_so_far);
1134 Ok(EstimateVerdict::Skip)
1135 },
1136 )))
1137 }
1138
1139 fn compress(
1140 &self,
1141 _compressor: &CascadingCompressor,
1142 _data: &ArrayAndStats,
1143 _compress_ctx: CompressorContext,
1144 _exec_ctx: &mut ExecutionCtx,
1145 ) -> VortexResult<ArrayRef> {
1146 unreachable!("test helper should never be selected for compression")
1147 }
1148 }
1149
1150 #[derive(Debug)]
1151 struct CallbackMatchingRatioScheme;
1152
1153 impl Scheme for CallbackMatchingRatioScheme {
1154 fn scheme_name(&self) -> &'static str {
1155 "test.callback_matching_ratio"
1156 }
1157
1158 fn matches(&self, canonical: &Canonical) -> bool {
1159 matches_integer_primitive(canonical)
1160 }
1161
1162 fn expected_compression_ratio(
1163 &self,
1164 _data: &ArrayAndStats,
1165 _compress_ctx: CompressorContext,
1166 _exec_ctx: &mut ExecutionCtx,
1167 ) -> CompressionEstimate {
1168 CompressionEstimate::Deferred(DeferredEstimate::Callback(Box::new(
1169 |_compressor, _data, _ctx, _exec_ctx, _best_so_far| Ok(EstimateVerdict::Ratio(2.0)),
1170 )))
1171 }
1172
1173 fn compress(
1174 &self,
1175 _compressor: &CascadingCompressor,
1176 _data: &ArrayAndStats,
1177 _compress_ctx: CompressorContext,
1178 _exec_ctx: &mut ExecutionCtx,
1179 ) -> VortexResult<ArrayRef> {
1180 unreachable!("test helper should never be selected for compression")
1181 }
1182 }
1183
1184 #[test]
1185 fn callback_always_use_overrides_pass_one_best() -> VortexResult<()> {
1186 let compressor = CascadingCompressor::new(vec![&HugeRatioScheme, &CallbackAlwaysUseScheme]);
1190 let schemes: [&'static dyn Scheme; 2] = [&HugeRatioScheme, &CallbackAlwaysUseScheme];
1191 let data = estimate_test_data();
1192 let mut exec_ctx = SESSION.create_execution_ctx();
1193
1194 let winner = compressor.choose_best_scheme(
1195 &schemes,
1196 &data,
1197 CompressorContext::new(),
1198 &mut exec_ctx,
1199 )?;
1200
1201 assert!(matches!(
1202 winner,
1203 Some((scheme, WinnerEstimate::AlwaysUse))
1204 if scheme.id() == CallbackAlwaysUseScheme.id()
1205 ));
1206 Ok(())
1207 }
1208
1209 #[test]
1210 fn threshold_reflects_pass_one_best() -> VortexResult<()> {
1211 let _guard = OBSERVER_LOCK.lock();
1212 *OBSERVED_THRESHOLD.lock() = None;
1213
1214 let compressor =
1215 CascadingCompressor::new(vec![&DirectRatioScheme, &ThresholdObservingScheme]);
1216 let schemes: [&'static dyn Scheme; 2] = [&DirectRatioScheme, &ThresholdObservingScheme];
1217 let data = estimate_test_data();
1218 let mut exec_ctx = SESSION.create_execution_ctx();
1219
1220 compressor.choose_best_scheme(&schemes, &data, CompressorContext::new(), &mut exec_ctx)?;
1221
1222 let observed = *OBSERVED_THRESHOLD.lock();
1223 assert!(matches!(
1224 observed,
1225 Some(Some(EstimateScore::FiniteCompression(r))) if r == 2.0
1226 ));
1227 Ok(())
1228 }
1229
1230 #[test]
1231 fn threshold_is_none_when_only_prior_is_zero_bytes() -> VortexResult<()> {
1232 let _guard = OBSERVER_LOCK.lock();
1233 *OBSERVED_THRESHOLD.lock() = None;
1234
1235 let compressor =
1236 CascadingCompressor::new(vec![&ZeroBytesSamplingScheme, &ThresholdObservingScheme]);
1237 let schemes: [&'static dyn Scheme; 2] =
1238 [&ZeroBytesSamplingScheme, &ThresholdObservingScheme];
1239 let data = estimate_test_data();
1240 let mut exec_ctx = SESSION.create_execution_ctx();
1241
1242 compressor.choose_best_scheme(&schemes, &data, CompressorContext::new(), &mut exec_ctx)?;
1243
1244 let observed = *OBSERVED_THRESHOLD.lock();
1247 assert_eq!(observed, Some(None));
1248 Ok(())
1249 }
1250
1251 #[test]
1252 fn threshold_is_none_when_no_prior_scheme() -> VortexResult<()> {
1253 let _guard = OBSERVER_LOCK.lock();
1254 *OBSERVED_THRESHOLD.lock() = None;
1255
1256 let compressor = CascadingCompressor::new(vec![&ThresholdObservingScheme]);
1257 let schemes: [&'static dyn Scheme; 1] = [&ThresholdObservingScheme];
1258 let data = estimate_test_data();
1259 let mut exec_ctx = SESSION.create_execution_ctx();
1260
1261 compressor.choose_best_scheme(&schemes, &data, CompressorContext::new(), &mut exec_ctx)?;
1262
1263 let observed = *OBSERVED_THRESHOLD.lock();
1264 assert_eq!(observed, Some(None));
1265 Ok(())
1266 }
1267
1268 #[test]
1269 fn threshold_updates_from_earlier_deferred_callback() -> VortexResult<()> {
1270 let _guard = OBSERVER_LOCK.lock();
1271 *OBSERVED_THRESHOLD.lock() = None;
1272
1273 let compressor =
1276 CascadingCompressor::new(vec![&CallbackRatioScheme, &ThresholdObservingScheme]);
1277 let schemes: [&'static dyn Scheme; 2] = [&CallbackRatioScheme, &ThresholdObservingScheme];
1278 let data = estimate_test_data();
1279 let mut exec_ctx = SESSION.create_execution_ctx();
1280
1281 compressor.choose_best_scheme(&schemes, &data, CompressorContext::new(), &mut exec_ctx)?;
1282
1283 let observed = *OBSERVED_THRESHOLD.lock();
1284 assert!(matches!(
1285 observed,
1286 Some(Some(EstimateScore::FiniteCompression(r))) if r == 3.0
1287 ));
1288 Ok(())
1289 }
1290
1291 #[test]
1292 fn ratio_tie_between_immediate_and_deferred_favors_immediate() -> VortexResult<()> {
1293 let compressor =
1297 CascadingCompressor::new(vec![&CallbackMatchingRatioScheme, &DirectRatioScheme]);
1298 let schemes: [&'static dyn Scheme; 2] = [&CallbackMatchingRatioScheme, &DirectRatioScheme];
1299 let data = estimate_test_data();
1300 let mut exec_ctx = SESSION.create_execution_ctx();
1301
1302 let winner = compressor.choose_best_scheme(
1303 &schemes,
1304 &data,
1305 CompressorContext::new(),
1306 &mut exec_ctx,
1307 )?;
1308
1309 assert!(matches!(
1310 winner,
1311 Some((scheme, WinnerEstimate::Score(EstimateScore::FiniteCompression(r))))
1312 if scheme.id() == DirectRatioScheme.id() && r == 2.0
1313 ));
1314 Ok(())
1315 }
1316
1317 #[test]
1318 fn all_null_array_compresses_to_constant() -> VortexResult<()> {
1319 let array = PrimitiveArray::new(
1320 buffer![0i32, 0, 0, 0, 0],
1321 Validity::Array(BoolArray::from_iter([false, false, false, false, false]).into_array()),
1322 )
1323 .into_array();
1324
1325 let compressor = CascadingCompressor::new(vec![&IntDictScheme]);
1328 let mut exec_ctx = SESSION.create_execution_ctx();
1329 let compressed = compressor.compress(&array, &mut exec_ctx)?;
1330 assert!(compressed.is::<Constant>());
1331 Ok(())
1332 }
1333
1334 #[test]
1341 fn sampling_uses_scheme_stats_options() -> VortexResult<()> {
1342 let array = PrimitiveArray::new(
1344 buffer![1.0f32, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0],
1345 Validity::NonNullable,
1346 )
1347 .into_array();
1348
1349 let compressor = CascadingCompressor::new(vec![&FloatDictScheme]);
1350
1351 let ctx = CompressorContext::new().with_sampling();
1355
1356 let mut exec_ctx = SESSION.create_execution_ctx();
1359 let score = estimate_compression_ratio_with_sampling(
1360 &compressor,
1361 &FloatDictScheme,
1362 &array,
1363 ctx,
1364 &mut exec_ctx,
1365 )?;
1366 assert!(matches!(score, EstimateScore::FiniteCompression(ratio) if ratio.is_finite()));
1367 Ok(())
1368 }
1369}