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