vortex_file/lib.rs
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4#![expect(clippy::cast_possible_truncation)]
5#![doc(html_logo_url = "/vortex/docs/_static/vortex_spiral_logo.svg")]
6//! Read and write Vortex layouts, a serialization of Vortex arrays.
7//!
8//! A Vortex file stores a root [`Layout`](vortex_layout::Layout), the byte segments referenced by
9//! that layout, an optional file-level [`DType`](vortex_array::dtype::DType) and statistics, and
10//! enough footer metadata to deserialize the tree. Layouts are recursive, so a file may organize
11//! data by row groups, columns, dictionaries, statistics, or other writer-chosen structures without
12//! changing the logical dtype seen by readers. The built-in layouts are:
13//!
14//! - [`FlatLayout`](vortex_layout::layouts::flat::FlatLayout): a single contiguously serialized
15//! array of buffers with a specific in-memory [`Alignment`](vortex_buffer::Alignment).
16//! - [`StructLayout`](vortex_layout::layouts::struct_::StructLayout): each column laid out at known
17//! offsets, permitting a subset of columns to be read in linear time with constant-time random
18//! access to any column.
19//! - [`ChunkedLayout`](vortex_layout::layouts::chunked::ChunkedLayout): each chunk laid out at known
20//! offsets; locating the chunks covering a row range is an `N log(N)` search of the offsets.
21//! - [`DictLayout`](vortex_layout::layouts::dict::DictLayout): a shared dictionary of values with a
22//! child layout holding indices.
23//! - [`ZonedLayout`](vortex_layout::layouts::zoned::ZonedLayout): a zone-map of statistics used for
24//! filter pruning.
25//!
26//! A layout alone is _not_ a standalone Vortex file: it is not self-describing, so the file pairs it
27//! with the dtype and footer metadata needed to deserialize it. This crate owns the file
28//! reader/writer APIs; the byte-level format is described under [File Format](#file-format) below
29//! and specified in full in the docs: <https://docs.vortex.dev/specs/file-format.html>.
30//!
31//! # Reading
32//!
33//! Use [`OpenOptionsSessionExt::open_options`] to create [`VortexOpenOptions`] from a session.
34//! Opening reads or accepts a footer, builds a segment source, and returns [`VortexFile`]. Scans are
35//! configured from [`VortexFile::scan`] with projection/filter expressions, row ranges,
36//! [`Selection`](vortex_scan::selection::Selection), split strategy, and concurrency settings from
37//! `vortex-layout`.
38//!
39//! Supplying known metadata can reduce open-time IO:
40//!
41//! - [`VortexOpenOptions::with_file_size`] avoids a size request.
42//! - [`VortexOpenOptions::with_dtype`] is required for files written without an embedded dtype.
43//! - [`VortexOpenOptions::with_footer`] can open a file without reading footer bytes.
44//! - [`VortexOpenOptions::with_segment_cache`] reuses segment buffers across scans.
45//!
46//! # Writing
47//!
48//! Use [`WriteOptionsSessionExt::write_options`] or [`VortexWriteOptions::new`] to write an
49//! [`ArrayStream`](vortex_array::stream::ArrayStream). The default [`WriteStrategyBuilder`]
50//! repartitions rows, builds statistics layouts, dictionary-encodes suitable columns, compresses
51//! chunks with the BtrBlocks-style compressor, and writes flat leaf layouts. Advanced users can
52//! replace the whole strategy or override individual fields.
53//!
54//! # File Format
55//!
56//! A Vortex file begins and ends with the 4-byte magic `VTXF`. Data segments are written first, in
57//! writer-chosen order, followed by the footer flatbuffers, a postscript, and an 8-byte
58//! end-of-file marker:
59//!
60//! ```text
61//! ┌────────────────────────────┐
62//! │ Magic bytes 'VTXF' │ 4 bytes
63//! ├────────────────────────────┤
64//! │ Segments │ serialized array chunks and per-column
65//! │ (data & statistics) │ statistics, in writer-chosen order
66//! ├────────────────────────────┤
67//! │ User-metadata segments │ optional; opaque values keyed by the postscript
68//! ├────────────────────────────┤
69//! │ DType flatbuffer │ optional; omitted via `exclude_dtype`
70//! ├────────────────────────────┤
71//! │ Layout flatbuffer │ required; the root Layout tree
72//! ├────────────────────────────┤
73//! │ Statistics flatbuffer │ optional; file-level per-field statistics
74//! ├────────────────────────────┤
75//! │ Footer flatbuffer │ required; dictionary-encoded segment map
76//! │ │ and array/layout/compression/encryption specs
77//! ├────────────────────────────┤
78//! │ Postscript │ locators for the footer and user-metadata segments;
79//! │ │ at most 65528 bytes
80//! ├────────────────────────────┤
81//! │ 8-byte End of File │ u16 version, u16 postscript length,
82//! │ │ 4 magic bytes 'VTXF'
83//! └────────────────────────────┘
84//! ```
85//!
86//! The postscript records the offset, length, and alignment of the dtype, layout, statistics, and
87//! footer segments, plus any user-defined metadata segments keyed by string, so a single read of the
88//! file tail (defaulting to 64KiB) is enough to locate and parse the footer and metadata locators.
89//! User-metadata values live in their own segments; opening a file reads none of them by default.
90//! [`VortexOpenOptions::include_metadata`] eagerly resolves every locator through the cache-backed
91//! segment source, issuing targeted reads only for values not already covered by the initial read.
92//! The byte-level format is specified in full at
93//! <https://docs.vortex.dev/specs/file-format.html>.
94//!
95//! A Parquet-style file is realized by nesting a chunked layout of struct layouts of chunked layouts
96//! of flat layouts: the outer chunked layout models row groups and the inner one models pages.
97//! Layouts are adaptive, so the writer is free to build arbitrarily complex layouts to trade off
98//! locality and parallelism, or to elide statistics entirely when an external index supplies them.
99//!
100//! # Footer Deserialization
101//!
102//! [`FooterDeserializer`] supports incremental footer reads. It returns [`DeserializeStep`] values
103//! when it needs more bytes or a file size, and returns [`Footer`] once all required footer segments
104//! are available. [`VortexOpenOptions`] drives this state machine for ordinary file opens.
105
106mod counting;
107mod file;
108pub mod flatbuffers;
109mod footer;
110pub mod multi;
111mod open;
112mod pruning;
113mod read;
114/// Segment sources, caches, and sinks used by file readers and writers.
115pub mod segments;
116mod strategy;
117#[cfg(test)]
118mod tests;
119/// Compatibility readers for newer file-statistics layout behavior.
120pub mod v2;
121mod writer;
122
123pub use counting::CountingVortexWrite;
124pub use file::*;
125pub use footer::*;
126pub use forever_constant::*;
127pub use open::*;
128pub use strategy::*;
129use vortex_array::arrays::Patched;
130use vortex_array::arrays::patched::use_experimental_patches;
131use vortex_array::session::ArraySessionExt;
132use vortex_pco::Pco;
133use vortex_session::VortexSession;
134pub use writer::*;
135
136/// The current version of the Vortex file format
137pub const VERSION: u16 = 1;
138/// The size of the footer in bytes in Vortex version 1
139pub const V1_FOOTER_FBS_SIZE: usize = 32;
140
141/// Constants that will never change (i.e., doing so would break backwards compatibility)
142mod forever_constant {
143 /// The extension for Vortex files
144 pub const VORTEX_FILE_EXTENSION: &str = "vortex";
145
146 /// The maximum length of a Vortex postscript in bytes
147 pub const MAX_POSTSCRIPT_SIZE: u16 = u16::MAX - 8;
148 /// The magic bytes for a Vortex file
149 pub const MAGIC_BYTES: [u8; 4] = *b"VTXF";
150 /// The size of the EOF marker in bytes
151 pub const EOF_SIZE: usize = 8;
152
153 #[cfg(test)]
154 mod test {
155 use crate::*;
156
157 #[test]
158 fn never_change_these_constants() {
159 assert_eq!(V1_FOOTER_FBS_SIZE, 32);
160 assert_eq!(MAX_POSTSCRIPT_SIZE, 65527);
161 assert_eq!(MAGIC_BYTES, *b"VTXF");
162 assert_eq!(EOF_SIZE, 8);
163 }
164 }
165}
166
167/// Register the default encodings use in Vortex files with the provided session.
168///
169/// Registration covers reading: a session can decode every encoding registered here. The
170/// writer is gated separately by the editions enabled on its session.
171pub fn register_default_encodings(session: &VortexSession) {
172 vortex_bytebool::initialize(session);
173 vortex_fsst::initialize(session);
174 vortex_onpair::initialize(session);
175 vortex_zigzag::initialize(session);
176 #[cfg(feature = "zstd")]
177 vortex_zstd::initialize(session);
178
179 {
180 let arrays = session.arrays();
181 arrays.register(Pco);
182 if use_experimental_patches() {
183 arrays.register(Patched);
184 }
185 }
186
187 vortex_alp::initialize(session);
188 vortex_datetime_parts::initialize(session);
189 vortex_decimal_byte_parts::initialize(session);
190 vortex_fastlanes::initialize(session);
191 vortex_runend::initialize(session);
192 vortex_sequence::initialize(session);
193 vortex_sparse::initialize(session);
194
195 #[cfg(feature = "tensor")]
196 vortex_tensor::initialize(session);
197}
198
199#[cfg(test)]
200pub(crate) fn enable_all_registered_array_encodings(session: &VortexSession) {
201 use vortex_array::dtype::session::DTypeSessionExt;
202 use vortex_edition::ComponentKind;
203 use vortex_edition::Edition;
204 use vortex_edition::EditionId;
205 use vortex_edition::EditionInclusion;
206 use vortex_edition::EditionSessionExt;
207 use vortex_error::VortexExpect;
208 use vortex_error::vortex_err;
209 use vortex_layout::session::LayoutSessionExt;
210
211 const TEST_EDITION: EditionId = EditionId::new("test", 2026, 7, 0);
212
213 let editions = session.editions();
214 editions
215 .declare_edition(Edition {
216 id: TEST_EDITION,
217 min_library_version: None,
218 })
219 .map_err(|error| vortex_err!("{error}"))
220 .vortex_expect("test edition is valid");
221 let component_ids = [
222 (
223 ComponentKind::Array,
224 session
225 .arrays()
226 .registry()
227 .read(|map| map.keys().copied().collect::<Vec<_>>()),
228 ),
229 (
230 ComponentKind::Layout,
231 session
232 .layouts()
233 .registry()
234 .read(|map| map.keys().copied().collect::<Vec<_>>()),
235 ),
236 (
237 ComponentKind::DType,
238 session
239 .dtypes()
240 .registry()
241 .read(|map| map.keys().copied().collect::<Vec<_>>()),
242 ),
243 ];
244 for (kind, ids) in component_ids {
245 for id in ids {
246 editions
247 .declare_inclusion(EditionInclusion::new(kind, &id, TEST_EDITION))
248 .map_err(|error| vortex_err!("{error}"))
249 .vortex_expect("registered component has one test-edition inclusion");
250 }
251 }
252 for id in [
253 "vortex.bounded_max",
254 "vortex.bounded_min",
255 "vortex.max",
256 "vortex.min",
257 "vortex.nan_count",
258 "vortex.null_count",
259 ] {
260 editions
261 .declare_inclusion(EditionInclusion::new(
262 ComponentKind::Aggregate,
263 id,
264 TEST_EDITION,
265 ))
266 .map_err(|error| vortex_err!("{error}"))
267 .vortex_expect("default aggregate has one test-edition inclusion");
268 }
269 session
270 .enable_edition(TEST_EDITION)
271 .map_err(|error| vortex_err!("{error}"))
272 .vortex_expect("test edition is registered");
273}
274
275#[cfg(test)]
276mod default_encoding_tests {
277 use vortex_array::VTable as _;
278 use vortex_array::array_session;
279 use vortex_array::arrays::Filter;
280 use vortex_array::optimizer::kernels::ArrayKernelsExt as _;
281 use vortex_array::session::ArraySessionExt as _;
282 use vortex_fsst::FSST;
283 use vortex_onpair::OnPair;
284
285 use crate::register_default_encodings;
286
287 #[test]
288 fn register_default_encodings_registers_external_execute_parent_kernels() {
289 let session = array_session();
290
291 assert!(!session.arrays().registry().contains_key(&FSST.id()));
292 assert!(!session.kernels().has_execute_parent(Filter.id(), FSST.id()));
293
294 register_default_encodings(&session);
295
296 assert!(session.arrays().registry().contains_key(&FSST.id()));
297 assert!(session.kernels().has_execute_parent(Filter.id(), FSST.id()));
298 assert!(session.arrays().registry().contains_key(&OnPair.id()));
299 assert!(
300 session
301 .kernels()
302 .has_execute_parent(Filter.id(), OnPair.id())
303 );
304 }
305}