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