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