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