Skip to main content

vortex_btrblocks/
builder.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Builder for configuring `BtrBlocksCompressor` instances.
5
6use vortex_array::ArrayId;
7use vortex_decimal_byte_parts::decimal_byte_parts_v2_id;
8use vortex_fastlanes::for_v2_id;
9use vortex_session::VortexSession;
10use vortex_utils::aliases::hash_set::HashSet;
11
12use crate::BtrBlocksCompressor;
13use crate::CascadingCompressor;
14use crate::CompressionSessionExt;
15use crate::Scheme;
16use crate::SchemeExt;
17use crate::SchemeId;
18use crate::allowed_ids::AllowedIds;
19use crate::schemes::binary;
20use crate::schemes::float;
21use crate::schemes::integer;
22use crate::schemes::string;
23
24/// The preset a [`BtrBlocksCompressorBuilder`] builds with.
25///
26/// Every mode except [`All`](Self::All) excludes some schemes in
27/// [`build`](BtrBlocksCompressorBuilder::build).
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub(crate) enum CompressionMode {
30    /// Excludes nothing. Set by [`empty`](BtrBlocksCompressorBuilder::empty).
31    All,
32    /// Excludes Zstd and Pco. Set by [`from_session`](BtrBlocksCompressorBuilder::from_session).
33    Default,
34    /// Excludes buffer-level Zstd, keeping Zstd for strings and binary and Pco for numerics.
35    /// Set by [`with_compact`](BtrBlocksCompressorBuilder::with_compact).
36    Compact,
37    /// Excludes schemes without CUDA kernel support, keeping FSST for strings and both Zstd
38    /// schemes for binary. Set by
39    /// [`only_cuda_compatible`](BtrBlocksCompressorBuilder::only_cuda_compatible).
40    Cuda,
41}
42
43impl CompressionMode {
44    /// Returns the schemes [`build`](BtrBlocksCompressorBuilder::build) drops in this mode.
45    fn excluded_schemes(self) -> Vec<SchemeId> {
46        let mut excluded = Vec::new();
47        match self {
48            Self::All => {}
49            Self::Default => {
50                #[cfg(feature = "zstd")]
51                excluded.extend([
52                    string::ZstdScheme.id(),
53                    binary::ZstdScheme.id(),
54                    binary::ZstdBuffersScheme.id(),
55                ]);
56                #[cfg(feature = "pco")]
57                excluded.extend([integer::PcoScheme.id(), float::PcoScheme.id()]);
58            }
59            Self::Compact => {
60                #[cfg(feature = "zstd")]
61                excluded.push(binary::ZstdBuffersScheme.id());
62            }
63            Self::Cuda => {
64                // Keep FSST, which has a CUDA decoder and direct Arrow offset-based export. Other
65                // string fragmentation and dictionary schemes still require unsupported decode
66                // paths. Delta has a CUDA decode kernel, but stays excluded until GPU delta
67                // decode is benchmarked against the schemes it would displace.
68                excluded.extend([
69                    integer::DeltaScheme::default().id(),
70                    integer::SparseScheme.id(),
71                    integer::IntRLEScheme.id(),
72                    float::ALPRDScheme.id(),
73                    float::FloatRLEScheme.id(),
74                    float::NullDominatedSparseScheme.id(),
75                    string::NullDominatedSparseScheme.id(),
76                    string::StringDictScheme.id(),
77                    binary::BinaryDictScheme.id(),
78                ]);
79                // Both binary Zstd schemes are kept. Buffer-level compression preserves binary
80                // arrays' buffer layout for zero-conversion GPU decompression, but belongs to the
81                // opt-in `zstd` edition, so the session's enabled editions decide which of the
82                // two is kept.
83                #[cfg(feature = "zstd")]
84                excluded.push(string::ZstdScheme.id());
85                #[cfg(feature = "pco")]
86                excluded.extend([integer::PcoScheme.id(), float::PcoScheme.id()]);
87            }
88        }
89        excluded
90    }
91
92    /// Returns the serialized IDs [`build`](BtrBlocksCompressorBuilder::build) disallows in this
93    /// mode, on top of the session's restrictions.
94    fn excluded_encodings(self) -> Vec<ArrayId> {
95        match self {
96            Self::All | Self::Default | Self::Compact => Vec::new(),
97            // Multi-part DecimalByteParts arrays and FoR arrays with per-chunk references have no
98            // CUDA decode kernel.
99            Self::Cuda => vec![decimal_byte_parts_v2_id(), for_v2_id()],
100        }
101    }
102}
103
104/// Builder for creating configured [`BtrBlocksCompressor`] instances.
105///
106/// [`from_session`](Self::from_session) starts from the schemes registered in the session's
107/// [`CompressionSession`](crate::CompressionSession), in registration order, and
108/// [`build`](Self::build) excludes some of them: by default Zstd and Pco.
109/// [`with_compact`](Self::with_compact) and [`only_cuda_compatible`](Self::only_cuda_compatible)
110/// switch to a different preset.
111///
112/// The builder also tracks which serialized array IDs its schemes may produce, taken from the
113/// session's registered arrays and enabled editions. [`build`](Self::build) drops every scheme
114/// that produces a disallowed ID.
115///
116/// # Examples
117///
118/// ```rust
119/// use vortex_btrblocks::{BtrBlocksCompressorBuilder, CompressionSession, Scheme, SchemeExt};
120/// use vortex_btrblocks::schemes::integer::IntDictScheme;
121/// use vortex_session::VortexSession;
122///
123/// let session = VortexSession::empty().with::<CompressionSession>();
124///
125/// // Compressor with the session's schemes, restricted to the encodings it allows. This session
126/// // enables no editions, so every scheme is dropped.
127/// let compressor = BtrBlocksCompressorBuilder::from_session(&session).build();
128///
129/// // Remove specific schemes.
130/// let compressor = BtrBlocksCompressorBuilder::from_session(&session)
131///     .exclude_schemes([IntDictScheme.id()])
132///     .build();
133/// ```
134#[derive(Debug, Clone)]
135pub struct BtrBlocksCompressorBuilder {
136    schemes: Vec<&'static dyn Scheme>,
137    allowed: AllowedIds,
138    mode: CompressionMode,
139}
140
141impl BtrBlocksCompressorBuilder {
142    /// Creates a builder with every scheme registered in the session's
143    /// [`CompressionSession`](crate::CompressionSession), allowing the serialized IDs registered
144    /// in the session and permitted by its enabled editions.
145    pub fn from_session(session: &VortexSession) -> Self {
146        Self {
147            schemes: session.compression().schemes().to_vec(),
148            allowed: AllowedIds::from_session(session),
149            mode: CompressionMode::Default,
150        }
151    }
152
153    /// Creates a builder with no schemes registered.
154    ///
155    /// Useful when the caller wants explicit, scheme-by-scheme control over the compressor.
156    /// Every added scheme and every serialized ID is allowed.
157    pub fn empty() -> Self {
158        Self {
159            schemes: Vec::new(),
160            allowed: AllowedIds::all(),
161            mode: CompressionMode::All,
162        }
163    }
164
165    /// Adds a compression scheme not registered on the session.
166    ///
167    /// This allows encoding crates outside of `vortex-btrblocks` to register their own schemes
168    /// with the compressor.
169    ///
170    /// # Panics
171    ///
172    /// Panics if a scheme with the same [`SchemeId`] is already present.
173    pub fn with_new_scheme(mut self, scheme: &'static dyn Scheme) -> Self {
174        assert!(
175            !self.schemes.iter().any(|s| s.id() == scheme.id()),
176            "scheme {:?} is already present in the builder",
177            scheme.id(),
178        );
179
180        self.schemes.push(scheme);
181        self
182    }
183
184    /// Switches to the compact preset, keeping Zstd for strings and binary and Pco for numerics.
185    ///
186    /// This provides better compression ratios than the default, especially for floating-point
187    /// heavy datasets. The Zstd and Pco schemes are only registered with the `zstd` and `pco`
188    /// features.
189    pub fn with_compact(mut self) -> Self {
190        self.mode = CompressionMode::Compact;
191        self
192    }
193
194    /// Switches to the CUDA preset, excluding schemes without CUDA kernel support, keeping FSST
195    /// for string compression and Zstd for binary compression.
196    ///
197    /// This preset is intended for files that will be decoded by CUDA kernels. It may choose a
198    /// larger encoded representation than the default compressor.
199    pub fn only_cuda_compatible(mut self) -> Self {
200        self.mode = CompressionMode::Cuda;
201        self
202    }
203
204    /// Removes the specified compression schemes by their [`SchemeId`].
205    pub fn exclude_schemes(mut self, ids: impl IntoIterator<Item = SchemeId>) -> Self {
206        let ids: HashSet<_> = ids.into_iter().collect();
207        self.schemes.retain(|s| !ids.contains(&s.id()));
208        self
209    }
210
211    /// Allows serialized IDs the session's enabled editions do not permit, still requiring them
212    /// to be registered in the session.
213    pub fn disable_editions(mut self) -> Self {
214        self.allowed.editions = None;
215        self
216    }
217
218    /// Allows serialized IDs regardless of the session's registered arrays and enabled editions.
219    ///
220    /// Serialized IDs excluded by a preset stay excluded.
221    pub fn unrestricted(mut self) -> Self {
222        self.allowed.registered = None;
223        self.allowed.editions = None;
224        self
225    }
226
227    /// Builds the configured [`BtrBlocksCompressor`] from the schemes that its preset does not
228    /// exclude and whose produced serialized IDs are all allowed.
229    pub fn build(self) -> BtrBlocksCompressor {
230        BtrBlocksCompressor(CascadingCompressor::new(self.allowed_schemes()))
231    }
232
233    fn allowed_schemes(&self) -> Vec<&'static dyn Scheme> {
234        let excluded: HashSet<SchemeId> = self.mode.excluded_schemes().into_iter().collect();
235        let excluded_ids: HashSet<ArrayId> = self.mode.excluded_encodings().into_iter().collect();
236        let allowed = |id: &ArrayId| self.allowed.is_allowed(id) && !excluded_ids.contains(id);
237        self.schemes
238            .iter()
239            .copied()
240            .filter(|s| !excluded.contains(&s.id()))
241            .map(|s| s.refine(&allowed))
242            .filter(|s| s.produced_encodings().iter().all(&allowed))
243            .collect()
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use rstest::rstest;
250    use vortex_array::ArrayId;
251    use vortex_array::VTable;
252    use vortex_array::arrays::VarBin;
253    use vortex_decimal_byte_parts::DecimalByteParts;
254    use vortex_decimal_byte_parts::decimal_byte_parts_v1_id;
255    use vortex_fsst::FSST;
256
257    use super::*;
258    use crate::CompressionSession;
259    use crate::schemes::decimal::DECIMAL_V1;
260    use crate::schemes::decimal::DECIMAL_V2;
261
262    #[rstest]
263    #[case::fsst_missing_codes(&string::FSSTScheme, vec![FSST.id()], false)]
264    #[case::fsst_with_codes(&string::FSSTScheme, vec![FSST.id(), VarBin.id()], true)]
265    #[case::decimal_v1_id(&DECIMAL_V1, vec![decimal_byte_parts_v1_id()], true)]
266    #[case::decimal_both_ids(&DECIMAL_V2, vec![decimal_byte_parts_v1_id(), DecimalByteParts.id()], true)]
267    fn test_allowed_schemes(
268        #[case] scheme: &'static dyn Scheme,
269        #[case] allowed: Vec<ArrayId>,
270        #[case] retained: bool,
271    ) {
272        let mut builder = BtrBlocksCompressorBuilder::empty().with_new_scheme(scheme);
273        builder.allowed.editions = Some(allowed.into_iter().collect());
274
275        assert_eq!(!builder.allowed_schemes().is_empty(), retained);
276    }
277
278    #[test]
279    fn delta_is_excluded_only_by_cuda() {
280        let session = VortexSession::empty().with::<CompressionSession>();
281        let builder = BtrBlocksCompressorBuilder::from_session(&session).unrestricted();
282        let has_delta = |builder: BtrBlocksCompressorBuilder| {
283            builder
284                .allowed_schemes()
285                .iter()
286                .any(|s| s.id() == integer::DeltaScheme::default().id())
287        };
288        assert!(has_delta(builder.clone()));
289        assert!(has_delta(builder.clone().with_compact()));
290        assert!(!has_delta(builder.only_cuda_compatible()));
291    }
292}