Skip to main content

vortex_file/
file.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! This module defines the [`VortexFile`] struct, which represents a Vortex file on disk or in memory.
5//!
6//! The `VortexFile` provides methods for accessing file metadata, creating segment sources for reading
7//! data from the file, and initiating scans to read the file's contents into memory as Vortex arrays.
8
9use std::ops::Range;
10use std::sync::Arc;
11use std::sync::OnceLock;
12
13use itertools::Itertools;
14use vortex_array::ArrayRef;
15use vortex_array::dtype::DType;
16use vortex_array::dtype::FieldMask;
17use vortex_array::expr::Expression;
18use vortex_buffer::ByteBuffer;
19use vortex_error::VortexResult;
20use vortex_layout::LayoutReader;
21use vortex_layout::scan::layout::LayoutReaderDataSource;
22use vortex_layout::scan::scan_builder::ScanBuilder;
23use vortex_layout::scan::split_by::SplitBy;
24use vortex_layout::scan::v2::ScanFile;
25use vortex_layout::scan::v2::SegmentLocation;
26use vortex_layout::segments::SegmentSource;
27use vortex_scan::DataSourceRef;
28use vortex_session::VortexSession;
29use vortex_utils::aliases::hash_map::HashMap;
30
31use crate::FileStatistics;
32use crate::footer::Footer;
33use crate::pruning::can_prune_file_stats;
34use crate::v2::FileStatsLayoutReader;
35
36/// Represents a Vortex file, providing access to its metadata and content.
37///
38/// A `VortexFile` is created by opening a Vortex file using [`VortexOpenOptions`](crate::VortexOpenOptions).
39/// It provides methods for accessing file metadata (such as row count, data type, and statistics)
40/// and for initiating scans to read the file's contents.
41#[derive(Clone)]
42pub struct VortexFile {
43    /// The footer of the Vortex file, containing metadata and layout information.
44    footer: Footer,
45    /// The segment source used to read segments from this file.
46    segment_source: Arc<dyn SegmentSource>,
47    /// The Vortex session used to open this file.
48    session: VortexSession,
49    /// User-defined metadata values resolved for this file open.
50    metadata: Arc<HashMap<String, ByteBuffer>>,
51    /// None id LayoutReader caching is turned off
52    layout_reader_cache: Option<OnceLock<Arc<dyn LayoutReader>>>,
53}
54
55fn layout_reader(
56    segment_source: Arc<dyn SegmentSource>,
57    footer: &Footer,
58    session: &VortexSession,
59) -> VortexResult<Arc<dyn LayoutReader>> {
60    let root_reader = footer
61        .layout()
62        // TODO(ngates): we may want to allow the user pass in a name here?
63        .new_reader("".into(), segment_source, session, &Default::default())?;
64
65    Ok(if let Some(stats) = footer.statistics().cloned() {
66        Arc::new(FileStatsLayoutReader::new(
67            root_reader,
68            stats,
69            session.clone(),
70        ))
71    } else {
72        root_reader
73    })
74}
75
76impl VortexFile {
77    /// Creates a new `VortexFile` from the given footer, segment source, and session.
78    pub fn new(
79        footer: Footer,
80        segment_source: Arc<dyn SegmentSource>,
81        session: VortexSession,
82    ) -> Self {
83        Self {
84            footer,
85            segment_source,
86            session,
87            metadata: Arc::new(HashMap::new()),
88            layout_reader_cache: None,
89        }
90    }
91
92    pub(crate) fn with_metadata(mut self, metadata: Arc<HashMap<String, ByteBuffer>>) -> Self {
93        self.metadata = metadata;
94        self
95    }
96
97    /// Enable layout reader caching.
98    ///
99    /// Repeated calls to [`layout_reader`](Self::layout_reader), [`scan`](Self::scan), and
100    /// [`data_source`](Self::data_source) will share the same reader tree.
101    pub fn with_caching(self) -> Self {
102        Self {
103            footer: self.footer,
104            segment_source: self.segment_source,
105            session: self.session,
106            metadata: self.metadata,
107            layout_reader_cache: Some(OnceLock::new()),
108        }
109    }
110
111    /// Returns a reference to the file's footer, which contains metadata and layout information.
112    pub fn footer(&self) -> &Footer {
113        &self.footer
114    }
115
116    /// Returns the number of rows in the file.
117    pub fn row_count(&self) -> u64 {
118        self.footer.row_count()
119    }
120
121    /// Returns the data type of the file's contents.
122    pub fn dtype(&self) -> &DType {
123        self.footer.dtype()
124    }
125
126    /// Returns the file's statistics, if available.
127    ///
128    /// Statistics can be used for query optimization and data exploration.
129    pub fn file_stats(&self) -> Option<&FileStatistics> {
130        self.footer.statistics()
131    }
132
133    /// Returns the user-defined metadata segments loaded for this file.
134    ///
135    /// Metadata is only loaded when requested during open. Iteration order is unspecified.
136    pub fn metadata_segments(&self) -> impl Iterator<Item = (&str, &ByteBuffer)> {
137        self.metadata
138            .iter()
139            .map(|(key, metadata)| (key.as_str(), metadata))
140    }
141
142    /// Returns the loaded user-defined metadata segment for the given key.
143    ///
144    /// Returns `None` when the key is absent or metadata was not loaded.
145    pub fn metadata_segment(&self, key: &str) -> Option<&ByteBuffer> {
146        self.metadata.get(key)
147    }
148
149    /// Create a new segment source for reading from the file.
150    ///
151    /// This may spawn a background I/O driver that will exit when the returned segment source
152    /// is dropped.
153    pub fn segment_source(&self) -> Arc<dyn SegmentSource> {
154        Arc::clone(&self.segment_source)
155    }
156
157    /// Replace the segment source used by this file.
158    ///
159    /// Any cached layout reader is cleared so that subsequent scans construct readers over the
160    /// replacement source.
161    pub fn with_segment_source(mut self, segment_source: Arc<dyn SegmentSource>) -> Self {
162        self.segment_source = segment_source;
163        if self.layout_reader_cache.is_some() {
164            self.layout_reader_cache = Some(OnceLock::new());
165        }
166        self
167    }
168
169    /// Returns a reference to the Vortex session used to open this file.
170    pub fn session(&self) -> &VortexSession {
171        &self.session
172    }
173
174    /// Create a new layout reader for the file.
175    ///
176    /// Wraps the root layout in a [`FileStatsLayoutReader`] if file stats are available.
177    pub fn layout_reader(&self) -> VortexResult<Arc<dyn LayoutReader>> {
178        match &self.layout_reader_cache {
179            None => layout_reader(
180                Arc::clone(&self.segment_source),
181                &self.footer,
182                &self.session,
183            ),
184            Some(reader) => {
185                // get_or_try_init is unstable
186                if let Some(val) = reader.get() {
187                    Ok(Arc::clone(val))
188                } else {
189                    let inner = layout_reader(
190                        Arc::clone(&self.segment_source),
191                        &self.footer,
192                        &self.session,
193                    )?;
194                    Ok(if let Err(val) = reader.set(Arc::clone(&inner)) {
195                        val
196                    } else {
197                        inner
198                    })
199                }
200            }
201        }
202    }
203
204    /// Create a [`DataSource`](vortex_scan::DataSource) from this file for scanning.
205    ///
206    /// Wraps the file's layout reader with [`FileStatsLayoutReader`] (when file-level
207    /// statistics are available) and [`LayoutReaderDataSource`].
208    pub fn data_source(&self) -> VortexResult<DataSourceRef> {
209        let reader = self.layout_reader()?;
210
211        Ok(Arc::new(LayoutReaderDataSource::new(
212            reader,
213            self.session.clone(),
214        )))
215    }
216
217    /// Initiate a scan of the file, returning a builder for projection, filtering, selection, and
218    /// execution options.
219    pub fn scan(&self) -> VortexResult<ScanBuilder<ArrayRef>> {
220        Ok(ScanBuilder::new(
221            self.session.clone(),
222            self.layout_reader()?,
223        ))
224    }
225
226    /// The parts of this file the planner-driven scan executor reads through: its root layout,
227    /// where each of its segments lives, and its segment source.
228    ///
229    /// Pass this to [`vortex_layout::scan::v2::ScanBuilder::new`] to build a V2 scan explicitly.
230    pub fn scan_file(&self) -> ScanFile {
231        let locations = self
232            .footer
233            .segment_specs_with_metadata()
234            .iter()
235            .map(|spec| SegmentLocation {
236                offset: spec.offset,
237                length: spec.length,
238                alignment: spec.alignment,
239            })
240            .collect();
241        ScanFile {
242            layout: Arc::clone(self.footer.layout()),
243            locations,
244            segments: Arc::clone(&self.segment_source),
245        }
246    }
247
248    /// Returns `true` if file-level statistics prove the expression cannot
249    /// match any rows in this file.
250    ///
251    /// Row-count-aware pruning predicates are evaluated with the file's total
252    /// row count as their scope.
253    pub fn can_prune(&self, filter: &Expression) -> VortexResult<bool> {
254        let Some((stats, fields)) = self
255            .footer
256            .statistics()
257            .zip(self.footer.dtype().as_struct_fields_opt())
258        else {
259            return Ok(false);
260        };
261
262        can_prune_file_stats(
263            &filter.bind(self.footer.dtype())?,
264            self.footer.row_count(),
265            stats,
266            fields,
267            &self.session,
268        )
269    }
270
271    /// Return the file's natural row splits as root-coordinate ranges.
272    ///
273    /// These are the ranges that the default [`SplitBy`] would use for an all-fields scan.
274    pub fn splits(&self) -> VortexResult<Vec<Range<u64>>> {
275        let reader = self.layout_reader()?;
276        Ok(SplitBy::default()
277            .splits(reader.as_ref(), &(0..reader.row_count()), &[FieldMask::All])?
278            .into_iter()
279            .tuple_windows()
280            .map(|(start, end)| start..end)
281            .collect())
282    }
283}