Skip to main content

vortex_compressor/scheme/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Everything a scheme author implements or receives: the [`Scheme`] trait, exclusion rules,
5//! compression estimates, and the compression context.
6
7mod ctx;
8pub use ctx::CompressorContext;
9pub use ctx::MAX_CASCADE;
10
11pub(crate) mod estimate;
12mod exclusion;
13use std::fmt;
14use std::fmt::Debug;
15use std::hash::Hash;
16use std::hash::Hasher;
17
18pub use estimate::CompressionEstimate;
19pub use estimate::DeferredEstimate;
20pub use estimate::EstimateFn;
21pub use estimate::EstimateScore;
22pub use estimate::EstimateVerdict;
23pub use exclusion::AncestorExclusion;
24pub use exclusion::ChildSelection;
25pub use exclusion::DescendantExclusion;
26use vortex_array::ArrayRef;
27use vortex_array::Canonical;
28use vortex_array::ExecutionCtx;
29use vortex_error::VortexResult;
30
31use crate::CascadingCompressor;
32use crate::stats::ArrayAndStats;
33use crate::stats::GenerateStatsOptions;
34
35/// Unique identifier for a compression scheme.
36///
37/// The only way to obtain a [`SchemeId`] is through [`SchemeExt::id()`], which is auto-implemented
38/// for all [`Scheme`] types. There is no public constructor.
39///
40/// The only exception to this is for the compressor's synthetic `ROOT_SCHEME_ID`.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub struct SchemeId {
43    /// Only constructable within `vortex-compressor`.
44    ///
45    /// The only public way to obtain a [`SchemeId`] is through [`SchemeExt::id()`].
46    pub(super) name: &'static str,
47}
48
49impl fmt::Display for SchemeId {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        f.write_str(self.name)
52    }
53}
54
55// TODO(connor): Remove all default implemented methods.
56/// A single compression encoding that the [`CascadingCompressor`] can select from.
57///
58/// The compressor evaluates every registered scheme whose [`matches`] returns `true` for a given
59/// array, picks the one with the highest [`expected_compression_ratio`], and calls [`compress`] on
60/// the winner.
61///
62/// One of the key features of the compressor in this crate is that schemes may "cascade". A
63/// scheme's [`compress`] can call back into the compressor via
64/// [`CascadingCompressor::compress_child`] to compress child or transformed arrays, building up
65/// multiple encoding layers (e.g. frame-of-reference and then bit-packing).
66///
67/// # Scheme IDs
68///
69/// Every scheme has a globally unique name returned by [`scheme_name`]. The [`SchemeExt::id`]
70/// method (auto-implemented, cannot be overridden) wraps that name in an opaque [`SchemeId`] used
71/// for equality, hashing, and exclusion rules (see below).
72///
73/// # Cascading and children
74///
75/// Schemes that produce child arrays for further compression must declare [`num_children`] > 0.
76/// Each child should be identified by a stable index. Cascading schemes should use
77/// [`CascadingCompressor::compress_child`] to compress each child array, which handles cascade
78/// level / budget tracking and context management automatically.
79///
80/// No scheme may appear twice in a cascade (descendant) chain (enforced by the compressor). This
81/// keeps the search space a tree.
82///
83/// # Exclusion rules
84///
85/// Schemes declare exclusion rules to prevent incompatible scheme combinations in the cascade
86/// chain:
87///
88/// - [`descendant_exclusions`] (push): "exclude scheme X from my child Y's subtree." Used when the
89///   declaring scheme knows about the excluded scheme.
90/// - [`ancestor_exclusions`] (pull): "exclude me if ancestor X's child Y is above me." Used when
91///   the declaring scheme knows about the ancestor.
92///
93/// We do this because different schemes will live in different crates, and we cannot know the
94/// dependency direction ahead of time.
95///
96/// # Implementing a scheme
97///
98/// [`expected_compression_ratio`] should return
99/// `CompressionEstimate::Deferred(DeferredEstimate::Sample)` when a cheap heuristic is not
100/// available, asking the compressor to estimate via sampling. Implementors should return an
101/// immediate [`CompressionEstimate::Verdict`] when possible.
102///
103/// Schemes that need statistics that may be expensive to compute should override [`stats_options`]
104/// to declare what they require. The compressor merges all eligible schemes' options before
105/// generating stats, so each stat is always computed at most once for a given array.
106///
107/// A scheme implementation should be deterministic for a fixed input array and context. The
108/// compressor uses scheme order for deterministic tie-breaking, so non-deterministic estimates make
109/// compressed output harder to reproduce and compare.
110///
111/// [`scheme_name`]: Scheme::scheme_name
112/// [`matches`]: Scheme::matches
113/// [`compress`]: Scheme::compress
114/// [`expected_compression_ratio`]: Scheme::expected_compression_ratio
115/// [`stats_options`]: Scheme::stats_options
116/// [`num_children`]: Scheme::num_children
117/// [`descendant_exclusions`]: Scheme::descendant_exclusions
118/// [`ancestor_exclusions`]: Scheme::ancestor_exclusions
119pub trait Scheme: Debug + Send + Sync {
120    /// The globally unique name for this scheme (e.g. `"vortex.int.bitpacking"`).
121    fn scheme_name(&self) -> &'static str;
122
123    /// Whether this scheme can compress the given canonical array.
124    fn matches(&self, canonical: &Canonical) -> bool;
125
126    /// Returns the stats generation options this scheme requires. The compressor merges all
127    /// eligible schemes' options before generating stats so that a single stats pass satisfies
128    /// every scheme.
129    fn stats_options(&self) -> GenerateStatsOptions {
130        GenerateStatsOptions::default()
131    }
132
133    /// The number of child arrays this scheme produces when cascading. Returns 0 for leaf
134    /// schemes that produce a final encoded array.
135    fn num_children(&self) -> usize {
136        0
137    }
138
139    /// Schemes to exclude from specific children's subtrees (push direction).
140    ///
141    /// Each rule says: "when I cascade through child Y, do not use scheme X anywhere in that
142    /// subtree." Only meaningful when [`num_children`](Scheme::num_children) > 0.
143    fn descendant_exclusions(&self) -> Vec<DescendantExclusion> {
144        Vec::new()
145    }
146
147    /// Ancestors that make this scheme ineligible (pull direction).
148    ///
149    /// Each rule says: "if ancestor X cascaded through child Y somewhere above me in the chain, do
150    /// not try me."
151    fn ancestor_exclusions(&self) -> Vec<AncestorExclusion> {
152        Vec::new()
153    }
154
155    /// Cheaply estimate the compression ratio for this scheme on the given array.
156    ///
157    /// This method should be fast and infallible. Any expensive or fallible work should be
158    /// deferred to the compressor by returning
159    /// `CompressionEstimate::Deferred(DeferredEstimate::Sample)` or
160    /// `CompressionEstimate::Deferred(DeferredEstimate::Callback(...))`.
161    ///
162    /// The compressor will ask all schemes what their expected compression ratio is given the array
163    /// and statistics. The scheme with the highest estimated ratio will then be applied to the
164    /// entire array.
165    ///
166    /// [`CompressionEstimate::Verdict`] means the scheme already knows the terminal
167    /// [`crate::scheme::EstimateVerdict`]. `CompressionEstimate::Deferred(DeferredEstimate::Sample)`
168    /// asks the compressor to sample. `CompressionEstimate::Deferred(DeferredEstimate::Callback(...))`
169    /// asks the compressor to run custom deferred work. Deferred callbacks must return a
170    /// [`crate::scheme::EstimateVerdict`] directly, never another deferred request.
171    ///
172    /// Note that the compressor will also use this method when compressing samples, so some
173    /// statistics that might hold for the samples may not hold for the entire array (e.g.,
174    /// constancy). Implementations should check `ctx.is_sample` to make sure that they are
175    /// returning the correct information.
176    ///
177    /// The compressor guarantees that empty and all-null arrays are handled before this method is
178    /// called, so implementations may assume the array has at least one valid element. Outside of
179    /// sample compression, the compressor also encodes constant arrays itself before evaluating
180    /// schemes, so implementations only see constant arrays when `ctx.is_sample()` is `true`.
181    fn expected_compression_ratio(
182        &self,
183        _data: &ArrayAndStats,
184        _compress_ctx: CompressorContext,
185        _exec_ctx: &mut ExecutionCtx,
186    ) -> CompressionEstimate;
187
188    /// Compress the array using this scheme.
189    ///
190    /// # Errors
191    ///
192    /// Returns an error if compression fails.
193    fn compress(
194        &self,
195        compressor: &CascadingCompressor,
196        data: &ArrayAndStats,
197        compress_ctx: CompressorContext,
198        exec_ctx: &mut ExecutionCtx,
199    ) -> VortexResult<ArrayRef>;
200}
201
202impl PartialEq for dyn Scheme {
203    fn eq(&self, other: &Self) -> bool {
204        self.id() == other.id()
205    }
206}
207
208impl Eq for dyn Scheme {}
209
210impl Hash for dyn Scheme {
211    fn hash<H: Hasher>(&self, state: &mut H) {
212        self.id().hash(state);
213    }
214}
215
216/// Extension trait providing [`id`](SchemeExt::id) for all [`Scheme`] implementors.
217///
218/// This trait is automatically implemented for every type that implements [`Scheme`]. Because the
219/// blanket implementation covers all types, external crates cannot override `id()`.
220pub trait SchemeExt: Scheme {
221    /// Unique identifier derived from [`scheme_name`](Scheme::scheme_name).
222    fn id(&self) -> SchemeId {
223        SchemeId {
224            name: self.scheme_name(),
225        }
226    }
227}
228
229impl<T: Scheme + ?Sized> SchemeExt for T {}