Skip to main content

vortex_file/
strategy.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! This module defines the default layout strategy for a Vortex file.
5
6use std::sync::Arc;
7use std::sync::LazyLock;
8
9use vortex_alp::ALP;
10// Compressed encodings from encoding crates
11// Canonical array encodings from vortex-array
12use vortex_alp::ALPRD;
13use vortex_array::arrays::Bool;
14use vortex_array::arrays::Chunked;
15use vortex_array::arrays::Constant;
16use vortex_array::arrays::Decimal;
17use vortex_array::arrays::Dict;
18use vortex_array::arrays::Extension;
19use vortex_array::arrays::FixedSizeList;
20use vortex_array::arrays::List;
21use vortex_array::arrays::ListView;
22use vortex_array::arrays::Masked;
23use vortex_array::arrays::Null;
24use vortex_array::arrays::Primitive;
25use vortex_array::arrays::Struct;
26use vortex_array::arrays::VarBin;
27use vortex_array::arrays::VarBinView;
28use vortex_array::dtype::FieldPath;
29use vortex_array::session::ArrayRegistry;
30use vortex_array::session::ArraySession;
31#[cfg(feature = "zstd")]
32use vortex_btrblocks::BtrBlocksCompressorBuilder;
33#[cfg(feature = "zstd")]
34use vortex_btrblocks::FloatCode;
35#[cfg(feature = "zstd")]
36use vortex_btrblocks::IntCode;
37#[cfg(feature = "zstd")]
38use vortex_btrblocks::StringCode;
39use vortex_bytebool::ByteBool;
40use vortex_datetime_parts::DateTimeParts;
41use vortex_decimal_byte_parts::DecimalByteParts;
42use vortex_fastlanes::BitPacked;
43use vortex_fastlanes::Delta;
44use vortex_fastlanes::FoR;
45use vortex_fastlanes::RLE;
46use vortex_fsst::FSST;
47use vortex_layout::LayoutStrategy;
48use vortex_layout::layouts::buffered::BufferedStrategy;
49use vortex_layout::layouts::chunked::writer::ChunkedLayoutStrategy;
50use vortex_layout::layouts::collect::CollectStrategy;
51use vortex_layout::layouts::compressed::CompressingStrategy;
52use vortex_layout::layouts::compressed::CompressorPlugin;
53use vortex_layout::layouts::dict::writer::DictStrategy;
54use vortex_layout::layouts::flat::writer::FlatLayoutStrategy;
55use vortex_layout::layouts::repartition::RepartitionStrategy;
56use vortex_layout::layouts::repartition::RepartitionWriterOptions;
57use vortex_layout::layouts::table::TableStrategy;
58use vortex_layout::layouts::zoned::writer::ZonedLayoutOptions;
59use vortex_layout::layouts::zoned::writer::ZonedStrategy;
60use vortex_pco::Pco;
61use vortex_runend::RunEnd;
62use vortex_sequence::Sequence;
63use vortex_sparse::Sparse;
64use vortex_utils::aliases::hash_map::HashMap;
65use vortex_zigzag::ZigZag;
66#[cfg(feature = "zstd")]
67use vortex_zstd::Zstd;
68#[cfg(all(feature = "zstd", feature = "unstable_encodings"))]
69use vortex_zstd::ZstdBuffers;
70
71const ONE_MEG: u64 = 1 << 20;
72
73/// Static registry of all allowed array encodings for file writing.
74///
75/// This includes all canonical encodings from vortex-array plus all compressed
76/// encodings from the various encoding crates.
77pub static ALLOWED_ENCODINGS: LazyLock<ArrayRegistry> = LazyLock::new(|| {
78    let session = ArraySession::empty();
79
80    // Canonical encodings from vortex-array
81    session.register(Null);
82    session.register(Bool);
83    session.register(Primitive);
84    session.register(Decimal);
85    session.register(VarBin);
86    session.register(VarBinView);
87    session.register(List);
88    session.register(ListView);
89    session.register(FixedSizeList);
90    session.register(Struct);
91    session.register(Extension);
92    session.register(Chunked);
93    session.register(Constant);
94    session.register(Masked);
95    session.register(Dict);
96
97    // Compressed encodings from encoding crates
98    session.register(ALP);
99    session.register(ALPRD);
100    session.register(BitPacked);
101    session.register(ByteBool);
102    session.register(DateTimeParts);
103    session.register(DecimalByteParts);
104    session.register(Delta);
105    session.register(FoR);
106    session.register(FSST);
107    session.register(Pco);
108    session.register(RLE);
109    session.register(RunEnd);
110    session.register(Sequence);
111    session.register(Sparse);
112    session.register(ZigZag);
113
114    #[cfg(feature = "zstd")]
115    session.register(Zstd);
116    #[cfg(all(feature = "zstd", feature = "unstable_encodings"))]
117    session.register(ZstdBuffers);
118
119    session.registry().clone()
120});
121
122/// Build a new [writer strategy][LayoutStrategy] to compress and reorganize chunks of a Vortex file.
123///
124/// Vortex provides an out-of-the-box file writer that optimizes the layout of chunks on-disk,
125/// repartitioning and compressing them to strike a balance between size on-disk,
126/// bulk decoding performance, and IOPS required to perform an indexed read.
127pub struct WriteStrategyBuilder {
128    compressor: Option<Arc<dyn CompressorPlugin>>,
129    row_block_size: usize,
130    field_writers: HashMap<FieldPath, Arc<dyn LayoutStrategy>>,
131    allow_encodings: Option<ArrayRegistry>,
132    flat_strategy: Option<Arc<dyn LayoutStrategy>>,
133}
134
135impl Default for WriteStrategyBuilder {
136    /// Create a new empty builder. It can be further configured,
137    /// and then finally built yielding the [`LayoutStrategy`].
138    fn default() -> Self {
139        Self {
140            compressor: None,
141            row_block_size: 8192,
142            field_writers: HashMap::new(),
143            allow_encodings: Some(ALLOWED_ENCODINGS.clone()),
144            flat_strategy: None,
145        }
146    }
147}
148
149impl WriteStrategyBuilder {
150    /// Override the [compressor][CompressorPlugin] used for compressing chunks in the file.
151    ///
152    /// If not provided, this will use a BtrBlocks-style cascading compressor that tries to balance
153    /// total size with decoding performance.
154    pub fn with_compressor<C: CompressorPlugin>(mut self, compressor: C) -> Self {
155        self.compressor = Some(Arc::new(compressor));
156        self
157    }
158
159    /// Override the row block size used to determine the zone map sizes.
160    pub fn with_row_block_size(mut self, row_block_size: usize) -> Self {
161        self.row_block_size = row_block_size;
162        self
163    }
164
165    /// Override the default write layout for a specific field somewhere in the nested
166    /// schema tree.
167    pub fn with_field_writer(
168        mut self,
169        field: impl Into<FieldPath>,
170        writer: Arc<dyn LayoutStrategy>,
171    ) -> Self {
172        self.field_writers.insert(field.into(), writer);
173        self
174    }
175
176    /// Override the allowed array encodings for normalization.
177    pub fn with_allow_encodings(mut self, allow_encodings: ArrayRegistry) -> Self {
178        self.allow_encodings = Some(allow_encodings);
179        self
180    }
181
182    /// Override the flat layout strategy used for leaf chunks.
183    ///
184    /// By default, this uses [`FlatLayoutStrategy`]. This can be used to substitute a custom
185    /// layout strategy, e.g. one that inlines constant array buffers for GPU reads.
186    pub fn with_flat_strategy(mut self, flat: Arc<dyn LayoutStrategy>) -> Self {
187        self.flat_strategy = Some(flat);
188        self
189    }
190
191    /// Configure a write strategy that emits only CUDA-compatible encodings.
192    ///
193    /// This configures BtrBlocks to exclude schemes without CUDA kernel support.
194    /// With the `unstable_encodings` feature, strings use buffer-level Zstd compression
195    /// (`ZstdBuffersArray`) which preserves the array buffer layout for zero-conversion
196    /// GPU decompression. Without it, strings use interleaved Zstd compression.
197    #[cfg(feature = "zstd")]
198    pub fn with_cuda_compatible_encodings(mut self) -> Self {
199        let mut builder = BtrBlocksCompressorBuilder::default()
200            .exclude_int([IntCode::Sparse, IntCode::Rle])
201            .exclude_float([FloatCode::Rle, FloatCode::Sparse])
202            .exclude_string([StringCode::Dict, StringCode::Fsst]);
203
204        #[cfg(feature = "unstable_encodings")]
205        {
206            builder = builder.include_string([StringCode::ZstdBuffers]);
207        }
208        #[cfg(not(feature = "unstable_encodings"))]
209        {
210            builder = builder.include_string([StringCode::Zstd]);
211        }
212
213        self.compressor = Some(Arc::new(builder.build()));
214        self
215    }
216
217    /// Configure a write strategy that uses compact encodings (Pco for numerics, Zstd for
218    /// strings/binary).
219    ///
220    /// This provides better compression ratios than the default BtrBlocks strategy,
221    /// especially for floating-point heavy datasets.
222    #[cfg(feature = "zstd")]
223    pub fn with_compact_encodings(mut self) -> Self {
224        let btrblocks = BtrBlocksCompressorBuilder::default()
225            .include_string([StringCode::Zstd])
226            .include_int([IntCode::Pco])
227            .include_float([FloatCode::Pco])
228            .build();
229
230        self.compressor = Some(Arc::new(btrblocks));
231        self
232    }
233
234    /// Builds the canonical [`LayoutStrategy`] implementation, with the configured overrides
235    /// applied.
236    pub fn build(self) -> Arc<dyn LayoutStrategy> {
237        let flat: Arc<dyn LayoutStrategy> = if let Some(flat) = self.flat_strategy {
238            flat
239        } else if let Some(allow_encodings) = self.allow_encodings {
240            Arc::new(FlatLayoutStrategy::default().with_allow_encodings(allow_encodings))
241        } else {
242            Arc::new(FlatLayoutStrategy::default())
243        };
244
245        // 7. for each chunk create a flat layout
246        let chunked = ChunkedLayoutStrategy::new(flat.clone());
247        // 6. buffer chunks so they end up with closer segment ids physically
248        let buffered = BufferedStrategy::new(chunked, 2 * ONE_MEG); // 2MB
249        // 5. compress each chunk
250        let compressing = if let Some(ref compressor) = self.compressor {
251            CompressingStrategy::new_opaque(buffered, compressor.clone())
252        } else {
253            CompressingStrategy::new_btrblocks(buffered, true)
254        };
255
256        // 4. prior to compression, coalesce up to a minimum size
257        let coalescing = RepartitionStrategy::new(
258            compressing,
259            RepartitionWriterOptions {
260                // Write stream partitions roughly become segments. Because Vortex never reads less
261                // than one segment, the size of segments and, therefore, partitions, must be small
262                // enough to both (1) allow fine-grained random access reads and (2) allow
263                // sufficient read concurrency for the desired throughput. One megabyte is small
264                // enough to achieve this for S3 (Durner et al., "Exploiting Cloud Object Storage for
265                // High-Performance Analytics", VLDB Vol 16, Iss 11).
266                block_size_minimum: ONE_MEG,
267                block_len_multiple: self.row_block_size,
268                block_size_target: Some(ONE_MEG),
269                canonicalize: true,
270            },
271        );
272
273        // 2.1. | 3.1. compress stats tables and dict values.
274        let compress_then_flat = if let Some(ref compressor) = self.compressor {
275            CompressingStrategy::new_opaque(flat, compressor.clone())
276        } else {
277            CompressingStrategy::new_btrblocks(flat, false)
278        };
279
280        // 3. apply dict encoding or fallback
281        let dict = DictStrategy::new(
282            coalescing.clone(),
283            compress_then_flat.clone(),
284            coalescing,
285            Default::default(),
286        );
287
288        // 2. calculate stats for each row group
289        let stats = ZonedStrategy::new(
290            dict,
291            compress_then_flat.clone(),
292            ZonedLayoutOptions {
293                block_size: self.row_block_size,
294                ..Default::default()
295            },
296        );
297
298        // 1. repartition each column to fixed row counts
299        let repartition = RepartitionStrategy::new(
300            stats,
301            RepartitionWriterOptions {
302                // No minimum block size in bytes
303                block_size_minimum: 0,
304                // Always repartition into 8K row blocks
305                block_len_multiple: self.row_block_size,
306                block_size_target: None,
307                canonicalize: false,
308            },
309        );
310
311        // 0. start with splitting columns
312        let validity_strategy = CollectStrategy::new(compress_then_flat);
313
314        // Take any field overrides from the builder and apply them to the final strategy.
315        let table_strategy = TableStrategy::new(Arc::new(validity_strategy), Arc::new(repartition))
316            .with_field_writers(self.field_writers);
317
318        Arc::new(table_strategy)
319    }
320}