Skip to main content

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//! │      DType flatbuffer      │  optional; omitted via `exclude_dtype`
68//! ├────────────────────────────┤
69//! │      Layout flatbuffer     │  required; the root Layout tree
70//! ├────────────────────────────┤
71//! │    Statistics flatbuffer   │  optional; file-level per-field statistics
72//! ├────────────────────────────┤
73//! │      Footer flatbuffer     │  required; dictionary-encoded segment map
74//! │                            │  and array/layout/compression/encryption specs
75//! ├────────────────────────────┤
76//! │         Postscript         │  offsets of the four footer segments above;
77//! │                            │  at most 65528 bytes
78//! ├────────────────────────────┤
79//! │     8-byte End of File     │  u16 version, u16 postscript length,
80//! │                            │  4 magic bytes 'VTXF'
81//! └────────────────────────────┘
82//! ```
83//!
84//! The postscript records the offset, length, and alignment of the dtype, layout, statistics, and
85//! footer segments, so a single read of the file tail (defaulting to 64KiB) is enough to locate and
86//! parse the footer. The byte-level format is specified in full at
87//! <https://docs.vortex.dev/specs/file-format.html>.
88//!
89//! A Parquet-style file is realized by nesting a chunked layout of struct layouts of chunked layouts
90//! of flat layouts: the outer chunked layout models row groups and the inner one models pages.
91//! Layouts are adaptive, so the writer is free to build arbitrarily complex layouts to trade off
92//! locality and parallelism, or to elide statistics entirely when an external index supplies them.
93//!
94//! # Footer Deserialization
95//!
96//! [`FooterDeserializer`] supports incremental footer reads. It returns [`DeserializeStep`] values
97//! when it needs more bytes or a file size, and returns [`Footer`] once all required footer segments
98//! are available. [`VortexOpenOptions`] drives this state machine for ordinary file opens.
99
100mod counting;
101mod file;
102mod footer;
103pub mod multi;
104mod open;
105mod pruning;
106mod read;
107/// Segment sources, caches, and sinks used by file readers and writers.
108pub mod segments;
109mod strategy;
110#[cfg(test)]
111mod tests;
112/// Compatibility readers for newer file-statistics layout behavior.
113pub mod v2;
114mod writer;
115
116pub use counting::CountingVortexWrite;
117pub use file::*;
118pub use footer::*;
119pub use forever_constant::*;
120pub use open::*;
121pub use strategy::*;
122use vortex_array::arrays::Patched;
123use vortex_array::arrays::patched::use_experimental_patches;
124use vortex_array::session::ArraySessionExt;
125use vortex_pco::Pco;
126use vortex_session::VortexSession;
127pub use writer::*;
128
129/// The current version of the Vortex file format
130pub const VERSION: u16 = 1;
131/// The size of the footer in bytes in Vortex version 1
132pub const V1_FOOTER_FBS_SIZE: usize = 32;
133
134/// Constants that will never change (i.e., doing so would break backwards compatibility)
135mod forever_constant {
136    /// The extension for Vortex files
137    pub const VORTEX_FILE_EXTENSION: &str = "vortex";
138
139    /// The maximum length of a Vortex postscript in bytes
140    pub const MAX_POSTSCRIPT_SIZE: u16 = u16::MAX - 8;
141    /// The magic bytes for a Vortex file
142    pub const MAGIC_BYTES: [u8; 4] = *b"VTXF";
143    /// The size of the EOF marker in bytes
144    pub const EOF_SIZE: usize = 8;
145
146    #[cfg(test)]
147    mod test {
148        use super::*;
149        use crate::*;
150
151        #[test]
152        fn never_change_these_constants() {
153            assert_eq!(V1_FOOTER_FBS_SIZE, 32);
154            assert_eq!(MAX_POSTSCRIPT_SIZE, 65527);
155            assert_eq!(MAGIC_BYTES, *b"VTXF");
156            assert_eq!(EOF_SIZE, 8);
157        }
158    }
159}
160
161/// Register the default encodings use in Vortex files with the provided session.
162///
163/// NOTE: this function will be changed in the future to encapsulate logic for using different
164/// Vortex "Editions" that may support different sets of encodings.
165pub fn register_default_encodings(session: &VortexSession) {
166    vortex_bytebool::initialize(session);
167    vortex_fsst::initialize(session);
168    #[cfg(feature = "unstable_encodings")]
169    vortex_onpair::initialize(session);
170    vortex_zigzag::initialize(session);
171
172    {
173        let arrays = session.arrays();
174        arrays.register(Pco);
175        #[cfg(feature = "zstd")]
176        arrays.register(vortex_zstd::Zstd);
177        #[cfg(all(feature = "zstd", feature = "unstable_encodings"))]
178        arrays.register(vortex_zstd::ZstdBuffers);
179        if use_experimental_patches() {
180            arrays.register(Patched);
181        }
182    }
183
184    vortex_alp::initialize(session);
185    vortex_datetime_parts::initialize(session);
186    vortex_decimal_byte_parts::initialize(session);
187    vortex_fastlanes::initialize(session);
188    vortex_runend::initialize(session);
189    vortex_sequence::initialize(session);
190    vortex_sparse::initialize(session);
191
192    #[cfg(feature = "unstable_encodings")]
193    vortex_tensor::initialize(session);
194}
195
196#[cfg(test)]
197mod default_encoding_tests {
198    use vortex_array::VTable as _;
199    use vortex_array::array_session;
200    use vortex_array::arrays::Filter;
201    use vortex_array::optimizer::kernels::ArrayKernelsExt as _;
202    use vortex_array::session::ArraySessionExt as _;
203    use vortex_fsst::FSST;
204
205    use crate::register_default_encodings;
206
207    #[test]
208    fn register_default_encodings_registers_external_execute_parent_kernels() {
209        let session = array_session();
210
211        assert!(session.arrays().registry().find(&FSST.id()).is_none());
212        assert!(!session.kernels().has_execute_parent(Filter.id(), FSST.id()));
213
214        register_default_encodings(&session);
215
216        assert!(session.arrays().registry().find(&FSST.id()).is_some());
217        assert!(session.kernels().has_execute_parent(Filter.id(), FSST.id()));
218    }
219}