Skip to main content

vortex_file/
writer.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::io;
5use std::io::Write;
6use std::sync::Arc;
7use std::sync::atomic::AtomicU64;
8use std::sync::atomic::Ordering;
9
10use futures::FutureExt;
11use futures::StreamExt;
12use futures::TryStreamExt;
13use futures::future::Fuse;
14use futures::future::LocalBoxFuture;
15use futures::future::ready;
16use futures::pin_mut;
17use futures::select;
18use itertools::Itertools;
19use vortex_array::ArrayContext;
20use vortex_array::ArrayRef;
21use vortex_array::dtype::DType;
22use vortex_array::dtype::FieldPath;
23use vortex_array::expr::stats::Stat;
24use vortex_array::iter::ArrayIterator;
25use vortex_array::iter::ArrayIteratorExt;
26use vortex_array::session::ArraySessionExt;
27use vortex_array::stats::PRUNING_STATS;
28use vortex_array::stream::ArrayStream;
29use vortex_array::stream::ArrayStreamAdapter;
30use vortex_array::stream::ArrayStreamExt;
31use vortex_array::stream::SendableArrayStream;
32use vortex_buffer::ByteBuffer;
33use vortex_error::VortexError;
34use vortex_error::VortexExpect;
35use vortex_error::VortexResult;
36use vortex_error::vortex_bail;
37use vortex_error::vortex_err;
38use vortex_io::IoBuf;
39use vortex_io::VortexWrite;
40use vortex_io::kanal_ext::KanalExt;
41use vortex_io::runtime::BlockingRuntime;
42use vortex_io::session::RuntimeSessionExt;
43use vortex_layout::LayoutStrategy;
44use vortex_layout::layouts::file_stats::accumulate_stats;
45use vortex_layout::sequence::SequenceId;
46use vortex_layout::sequence::SequentialStreamAdapter;
47use vortex_layout::sequence::SequentialStreamExt;
48use vortex_session::SessionExt;
49use vortex_session::VortexSession;
50use vortex_session::registry::ReadContext;
51
52use crate::ALLOWED_ENCODINGS;
53use crate::Footer;
54use crate::MAGIC_BYTES;
55use crate::WriteStrategyBuilder;
56use crate::counting::CountingVortexWrite;
57use crate::footer::FileStatistics;
58use crate::segments::writer::BufferedSegmentSink;
59
60/// Configure a new writer, which can eventually be used to write an [`ArrayStream`] into a sink
61/// that implements [`VortexWrite`].
62///
63/// Unless overridden, the default [write strategy][crate::WriteStrategyBuilder] will be used with no
64/// additional configuration.
65///
66/// Construct with [`WriteOptionsSessionExt::write_options`] for normal use so the writer inherits
67/// the session's runtime, array registry, and memory configuration.
68pub struct VortexWriteOptions {
69    session: VortexSession,
70    strategy: Arc<dyn LayoutStrategy>,
71    exclude_dtype: bool,
72    max_variable_length_statistics_size: usize,
73    file_statistics: Vec<Stat>,
74}
75
76/// Extension trait for constructing [`VortexWriteOptions`] from a session.
77pub trait WriteOptionsSessionExt: SessionExt {
78    /// Create [`VortexWriteOptions`] for writing to a Vortex file.
79    fn write_options(&self) -> VortexWriteOptions {
80        let session = self.session();
81        VortexWriteOptions {
82            strategy: WriteStrategyBuilder::default().build(),
83            session,
84            exclude_dtype: false,
85            file_statistics: PRUNING_STATS.to_vec(),
86            max_variable_length_statistics_size: 64,
87        }
88    }
89}
90impl<S: SessionExt> WriteOptionsSessionExt for S {}
91
92impl VortexWriteOptions {
93    /// Create a new [`VortexWriteOptions`] with the given session.
94    pub fn new(session: VortexSession) -> Self {
95        VortexWriteOptions {
96            strategy: WriteStrategyBuilder::default().build(),
97            session,
98            exclude_dtype: false,
99            file_statistics: PRUNING_STATS.to_vec(),
100            max_variable_length_statistics_size: 64,
101        }
102    }
103
104    /// Replace the default layout strategy with the provided one.
105    ///
106    /// The strategy controls repartitioning, statistics layout, compression, and leaf segment
107    /// emission. Use [`WriteStrategyBuilder`] when only a small part of the default strategy needs
108    /// customization.
109    pub fn with_strategy(mut self, strategy: Arc<dyn LayoutStrategy>) -> Self {
110        self.strategy = strategy;
111        self
112    }
113
114    /// Exclude the DType from the Vortex file. You must provide the DType to the reader.
115    // TODO(ngates): Should we store some sort of DType checksum to make sure the one passed at
116    //  read-time is sane? I guess most layouts will have some reasonable validation.
117    pub fn exclude_dtype(mut self) -> Self {
118        self.exclude_dtype = true;
119        self
120    }
121
122    /// Configure which statistics to compute at the file level.
123    ///
124    /// Pass an empty vector to omit file-level statistics.
125    pub fn with_file_statistics(mut self, file_statistics: Vec<Stat>) -> Self {
126        self.file_statistics = file_statistics;
127        self
128    }
129}
130
131impl VortexWriteOptions {
132    /// Drop into the blocking writer API using the given runtime.
133    ///
134    /// The returned adapter drives async writer internals on `runtime` while accepting ordinary
135    /// [`std::io::Write`] sinks and [`ArrayIterator`] inputs.
136    pub fn blocking<B: BlockingRuntime>(self, runtime: &B) -> BlockingWrite<'_, B> {
137        BlockingWrite {
138            options: self,
139            runtime,
140        }
141    }
142
143    /// Write an [`ArrayStream`] as a Vortex file.
144    ///
145    /// Note that buffers are flushed as soon as they are available with no buffering, the caller
146    /// is responsible for deciding how to configure buffering on the underlying `Write` sink.
147    pub async fn write<W: VortexWrite + Unpin, S: ArrayStream + Send + 'static>(
148        self,
149        write: W,
150        stream: S,
151    ) -> VortexResult<WriteSummary> {
152        self.write_internal(write, ArrayStreamExt::boxed(stream))
153            .await
154    }
155
156    async fn write_internal<W: VortexWrite + Unpin>(
157        self,
158        mut write: W,
159        stream: SendableArrayStream,
160    ) -> VortexResult<WriteSummary> {
161        // NOTE(os): Setup an array context that already has all known encodings pre-populated.
162        // This is preferred for now over having an empty context here, because only the
163        // serialised array order is deterministic. The serialisation of arrays are done
164        // parallel and with an empty context they can register their encodings to the context
165        // in different order, changing the written bytes from run to run.
166        let ctx = ArrayContext::new(ALLOWED_ENCODINGS.iter().cloned().sorted().collect())
167            // Configure a registry just to ensure only known encodings are interned.
168            .with_registry(self.session.arrays().registry().clone());
169        let dtype = stream.dtype().clone();
170
171        let (mut ptr, eof) = SequenceId::root().split();
172
173        let stream = SequentialStreamAdapter::new(
174            dtype.clone(),
175            stream
176                .try_filter(|chunk| ready(!chunk.is_empty()))
177                .map(move |result| result.map(|chunk| (ptr.advance(), chunk))),
178        )
179        .sendable();
180        let (file_stats, stream) = accumulate_stats(
181            stream,
182            self.file_statistics.clone().into(),
183            self.max_variable_length_statistics_size,
184            &self.session,
185        );
186
187        // First, write the magic bytes.
188        write.write_all(ByteBuffer::copy_from(MAGIC_BYTES)).await?;
189        let mut position = MAGIC_BYTES.len() as u64;
190
191        // Create a channel to send buffers from the segment sink to the output stream.
192        let (send, recv) = kanal::bounded_async(1);
193
194        let segments = Arc::new(BufferedSegmentSink::new(send, position));
195
196        // We spawn the layout future so it is driven in the background while we write the
197        // buffer stream, so we don't need to poll it until all buffers have been drained.
198        let ctx2 = ctx.clone();
199        let session = self.session.clone();
200        let layout_fut = self.session.handle().spawn_nested(move |h| async move {
201            let session = session.with_handle(h);
202            let layout = self
203                .strategy
204                .write_stream(
205                    ctx2,
206                    Arc::<BufferedSegmentSink>::clone(&segments),
207                    stream,
208                    eof,
209                    &session,
210                )
211                .await?;
212            Ok::<_, VortexError>((layout, segments.segment_specs()))
213        });
214
215        // Flush buffers as they arrive
216        let recv_stream = recv.into_stream();
217        pin_mut!(recv_stream);
218        while let Some(buffer) = recv_stream.next().await {
219            if buffer.is_empty() {
220                continue;
221            }
222            position += buffer.len() as u64;
223            write.write_all(buffer).await?;
224        }
225
226        let (layout, segment_specs) = layout_fut.await?;
227
228        // Assemble the Footer object now that we have all the segments.
229        let mut footer = Footer::new(
230            Arc::clone(&layout),
231            segment_specs,
232            if self.file_statistics.is_empty() {
233                None
234            } else {
235                Some(FileStatistics::new_with_dtype(
236                    file_stats.stats_sets().into(),
237                    &dtype,
238                ))
239            },
240            ReadContext::new(ctx.to_ids()),
241        );
242
243        // Emit the footer buffers and EOF.
244        let footer_buffers = footer
245            .clone()
246            .into_serializer()
247            .with_offset(position)
248            .with_exclude_dtype(self.exclude_dtype)
249            .serialize()?;
250
251        // Update the approx footer size in the footer object, so it can be used for caching and
252        // memory management in the future.
253        footer = footer.with_approx_byte_size(footer_buffers.iter().map(|b| b.len()).sum());
254
255        for buffer in footer_buffers {
256            position += buffer.len() as u64;
257            write.write_all(buffer).await?;
258        }
259
260        write.flush().await?;
261
262        Ok(WriteSummary {
263            footer,
264            size: position,
265        })
266    }
267
268    /// Create a push-based [`Writer`] that can be used to incrementally write arrays to the file.
269    ///
270    /// Each pushed chunk must have dtype `dtype`. Call [`Writer::finish`] to close the input stream,
271    /// flush remaining buffers, and receive the [`WriteSummary`].
272    pub fn writer<'w, W: VortexWrite + Unpin + 'w>(self, write: W, dtype: DType) -> Writer<'w> {
273        // Create a channel for sending arrays to the layout task.
274        let (arrays_send, arrays_recv) = kanal::bounded_async(1);
275
276        let arrays =
277            ArrayStreamExt::boxed(ArrayStreamAdapter::new(dtype, arrays_recv.into_stream()));
278
279        let write = CountingVortexWrite::new(write);
280        let bytes_written = write.counter();
281        let strategy = Arc::clone(&self.strategy);
282        let future = self.write(write, arrays).boxed_local().fuse();
283
284        Writer {
285            arrays: Some(arrays_send),
286            future,
287            bytes_written,
288            strategy,
289        }
290    }
291}
292
293/// An async API for writing Vortex files.
294pub struct Writer<'w> {
295    // The input channel for sending arrays to the writer.
296    arrays: Option<kanal::AsyncSender<VortexResult<ArrayRef>>>,
297    // The writer task that ultimately produces the footer.
298    future: Fuse<LocalBoxFuture<'w, VortexResult<WriteSummary>>>,
299    // The bytes written so far.
300    bytes_written: Arc<AtomicU64>,
301    // The layout strategy that is being used for the write.
302    strategy: Arc<dyn LayoutStrategy>,
303}
304
305impl Writer<'_> {
306    /// Push a new chunk into the writer.
307    pub async fn push(&mut self, chunk: ArrayRef) -> VortexResult<()> {
308        let arrays = self.arrays.clone().vortex_expect("missing arrays sender");
309        let send_fut = async move { arrays.send(Ok(chunk)).await }.fuse();
310        pin_mut!(send_fut);
311
312        // We poll the writer future to continue writing bytes to the output, while waiting for
313        // enough room to push the next chunk into the channel.
314        select! {
315            result = send_fut => {
316                // If the send future failed, the writer has failed or panicked.
317                if result.is_err() {
318                    return Err(self.handle_failed_task().await);
319                }
320            },
321            result = &mut self.future => {
322                // Under normal operation, the writer future should never complete until
323                // finish() is called. Therefore, we can assume the writer has failed.
324                // The writer future has failed, we need to propagate the error.
325                match result {
326                    Ok(_) => vortex_bail!("Internal error: writer future completed early"),
327                    Err(e) => return Err(e),
328                }
329            }
330        }
331
332        Ok(())
333    }
334
335    /// Push an entire [`ArrayStream`] into the writer, consuming it.
336    ///
337    /// A task is spawned to consume the stream and push it into the writer, with the current
338    /// thread being used to write buffers to the output.
339    pub async fn push_stream(&mut self, mut stream: SendableArrayStream) -> VortexResult<()> {
340        let arrays = self.arrays.clone().vortex_expect("missing arrays sender");
341        let stream_fut = async move {
342            while let Some(chunk) = stream.next().await {
343                arrays.send(chunk).await?;
344            }
345            Ok::<_, kanal::SendError>(())
346        }
347        .fuse();
348        pin_mut!(stream_fut);
349
350        // We poll the writer future to continue writing bytes to the output, while waiting for
351        // enough room to push the stream into the channel.
352        select! {
353            result = stream_fut => {
354                if let Err(_send_err) = result {
355                    // If the send future failed, the writer has failed or panicked.
356                    return Err(self.handle_failed_task().await);
357                }
358            }
359
360            result = &mut self.future => {
361                // Under normal operation, the writer future should never complete until
362                // finish() is called. Therefore, we can assume the writer has failed.
363                // The writer future has failed, we need to propagate the error.
364                match result {
365                    Ok(_) => vortex_bail!("Internal error: writer future completed early"),
366                    Err(e) => return Err(e),
367                }
368            }
369        }
370
371        Ok(())
372    }
373
374    /// Returns the number of bytes written to the file so far.
375    pub fn bytes_written(&self) -> u64 {
376        self.bytes_written.load(Ordering::Relaxed)
377    }
378
379    /// Returns the number of bytes currently buffered by the layout writers.
380    pub fn buffered_bytes(&self) -> u64 {
381        self.strategy.buffered_bytes()
382    }
383
384    /// Finish writing the Vortex file, flushing any remaining buffers and returning the
385    /// new file's footer.
386    pub async fn finish(mut self) -> VortexResult<WriteSummary> {
387        // Drop the input channel to signal EOF.
388        drop(self.arrays.take());
389
390        // Await the future task.
391        self.future.await
392    }
393
394    /// Assuming the writer task has failed, await it to get the error.
395    async fn handle_failed_task(&mut self) -> VortexError {
396        match (&mut self.future).await {
397            Ok(_) => vortex_err!(
398                "Internal error: writer task completed successfully but write future finished early"
399            ),
400            Err(e) => e,
401        }
402    }
403}
404
405/// Blocking adapter for [`VortexWriteOptions`].
406pub struct BlockingWrite<'rt, B: BlockingRuntime> {
407    options: VortexWriteOptions,
408    runtime: &'rt B,
409}
410
411impl<'rt, B: BlockingRuntime> BlockingWrite<'rt, B> {
412    /// Write a Vortex file into the given `Write` sink.
413    ///
414    /// The iterator is converted to an [`ArrayStream`] and driven to completion on
415    /// the configured blocking runtime.
416    pub fn write<W: Write + Unpin>(
417        self,
418        write: W,
419        iter: impl ArrayIterator + Send + 'static,
420    ) -> VortexResult<WriteSummary> {
421        self.runtime.block_on(async move {
422            self.options
423                .write(BlockingWriteAdapter(write), iter.into_array_stream())
424                .await
425        })
426    }
427
428    /// Create a blocking push-based writer for chunks with dtype `dtype`.
429    pub fn writer<'w, W: Write + Unpin + 'w>(
430        self,
431        write: W,
432        dtype: DType,
433    ) -> BlockingWriter<'rt, 'w, B> {
434        BlockingWriter {
435            writer: self.options.writer(BlockingWriteAdapter(write), dtype),
436            runtime: self.runtime,
437        }
438    }
439}
440
441/// A blocking adapter around a [`Writer`], allowing incremental writing of arrays to a Vortex file.
442pub struct BlockingWriter<'rt, 'w, B: BlockingRuntime> {
443    runtime: &'rt B,
444    writer: Writer<'w>,
445}
446
447impl<B: BlockingRuntime> BlockingWriter<'_, '_, B> {
448    /// Push one array chunk into the file.
449    pub fn push(&mut self, chunk: ArrayRef) -> VortexResult<()> {
450        self.runtime.block_on(self.writer.push(chunk))
451    }
452
453    /// Returns the number of bytes written to the sink so far.
454    pub fn bytes_written(&self) -> u64 {
455        self.writer.bytes_written()
456    }
457
458    /// Returns the number of bytes currently buffered by layout strategies.
459    pub fn buffered_bytes(&self) -> u64 {
460        self.writer.buffered_bytes()
461    }
462
463    /// Finish writing and return the written file summary.
464    pub fn finish(self) -> VortexResult<WriteSummary> {
465        self.runtime.block_on(self.writer.finish())
466    }
467}
468
469// TODO(ngates): this blocking API may change, for now we just run blocking I/O inline.
470struct BlockingWriteAdapter<W>(W);
471
472impl<W: Write + Unpin> VortexWrite for BlockingWriteAdapter<W> {
473    async fn write_all<B: IoBuf>(&mut self, buffer: B) -> io::Result<B> {
474        self.0.write_all(buffer.as_slice())?;
475        Ok(buffer)
476    }
477
478    fn flush(&mut self) -> impl Future<Output = io::Result<()>> {
479        ready(self.0.flush())
480    }
481
482    fn shutdown(&mut self) -> impl Future<Output = io::Result<()>> {
483        ready(Ok(()))
484    }
485}
486
487/// Summary returned after a Vortex file is written.
488pub struct WriteSummary {
489    footer: Footer,
490    size: u64,
491    // TODO(ngates): add a checksum
492}
493
494impl WriteSummary {
495    /// The footer of the written Vortex file.
496    pub fn footer(&self) -> &Footer {
497        &self.footer
498    }
499
500    /// The total size of the written Vortex file in bytes.
501    pub fn size(&self) -> u64 {
502        self.size
503    }
504
505    /// The total number of rows in the written Vortex file.
506    pub fn row_count(&self) -> u64 {
507        self.footer.row_count()
508    }
509
510    /// Returns the compressed size in bytes of each top-level column in schema order.
511    ///
512    /// A column's size includes every physical segment attributed to its layout subtree,
513    /// including auxiliary segments such as zone maps and dictionaries; see
514    /// [`Footer::compressed_field_sizes`] for the exact attribution semantics and for sizes of
515    /// nested fields. Bytes not attributable to a specific column (e.g. top-level struct
516    /// validity) are not included in any column's size.
517    ///
518    /// For a non-struct file, the returned vector contains a single entry for the root column.
519    pub fn compressed_column_sizes(&self) -> VortexResult<Vec<u64>> {
520        let sizes = self.footer.compressed_field_sizes()?;
521        let Some(fields) = self.footer.dtype().as_struct_fields_opt() else {
522            return Ok(vec![sizes.total()]);
523        };
524        Ok(fields
525            .names()
526            .iter()
527            .map(|name| {
528                sizes
529                    .get(&FieldPath::from_name(name.clone()))
530                    .unwrap_or_default()
531            })
532            .collect())
533    }
534}