Skip to main content

vortex_layout/layouts/zoned/
writer.rs

1//! Write-time assembly for zoned layouts.
2
3// SPDX-License-Identifier: Apache-2.0
4// SPDX-FileCopyrightText: Copyright the Vortex contributors
5
6use std::num::NonZeroUsize;
7use std::sync::Arc;
8
9use async_trait::async_trait;
10use futures::StreamExt as _;
11use parking_lot::Mutex;
12use vortex_array::ArrayContext;
13use vortex_array::IntoArray;
14use vortex_array::VortexSessionExecute;
15use vortex_array::aggregate_fn::AggregateFnRef;
16use vortex_array::aggregate_fn::AggregateFnVTable;
17use vortex_array::aggregate_fn::AggregateFnVTableExt;
18use vortex_array::aggregate_fn::EmptyOptions;
19use vortex_array::aggregate_fn::NumericalAggregateOpts;
20use vortex_array::aggregate_fn::fns::bounded_max::BoundedMax;
21use vortex_array::aggregate_fn::fns::bounded_max::BoundedMaxOptions;
22use vortex_array::aggregate_fn::fns::bounded_min::BoundedMin;
23use vortex_array::aggregate_fn::fns::bounded_min::BoundedMinOptions;
24use vortex_array::aggregate_fn::fns::max::Max;
25use vortex_array::aggregate_fn::fns::min::Min;
26use vortex_array::aggregate_fn::fns::nan_count::NanCount;
27use vortex_array::aggregate_fn::fns::null_count::NullCount;
28use vortex_array::aggregate_fn::fns::sum::Sum;
29use vortex_array::dtype::DType;
30use vortex_error::VortexError;
31use vortex_error::VortexResult;
32use vortex_io::session::RuntimeSessionExt;
33use vortex_session::VortexSession;
34use vortex_utils::parallelism::get_available_parallelism;
35
36use crate::IntoLayout;
37use crate::LayoutRef;
38use crate::LayoutStrategy;
39use crate::layouts::zoned::AggregateStatsAccumulator;
40use crate::layouts::zoned::ZonedLayout;
41use crate::layouts::zoned::aggregate_partials;
42use crate::layouts::zoned::schema::default_bounded_stat_max_bytes;
43use crate::segments::SegmentSinkRef;
44use crate::sequence::SendableSequentialStream;
45use crate::sequence::SequencePointer;
46use crate::sequence::SequentialArrayStreamExt;
47use crate::sequence::SequentialStreamAdapter;
48use crate::sequence::SequentialStreamExt;
49
50/// Configuration for building zoned layouts.
51///
52/// The input stream is assumed to already be partitioned into one chunk per zone, except
53/// possibly the final partial zone.
54pub struct ZonedLayoutOptions {
55    /// The size of a statistics block
56    pub block_size: NonZeroUsize,
57    /// The aggregate partials to collect for each block.
58    ///
59    /// If unset, the writer chooses pruning aggregates from the input dtype.
60    pub aggregate_fns: Option<Arc<[AggregateFnRef]>>,
61    /// Number of chunks to compute aggregate partials in parallel.
62    pub concurrency: NonZeroUsize,
63}
64
65impl Default for ZonedLayoutOptions {
66    fn default() -> Self {
67        Self {
68            block_size: unsafe { NonZeroUsize::new_unchecked(8192) },
69            aggregate_fns: None,
70            concurrency: unsafe {
71                NonZeroUsize::new_unchecked(get_available_parallelism().unwrap_or(1))
72            },
73        }
74    }
75}
76
77pub struct ZonedStrategy {
78    child: Arc<dyn LayoutStrategy>,
79    stats: Arc<dyn LayoutStrategy>,
80    options: ZonedLayoutOptions,
81}
82
83impl ZonedStrategy {
84    /// Create a writer that emits a data child plus an auxiliary per-zone stats child.
85    pub fn new<Child: LayoutStrategy, Stats: LayoutStrategy>(
86        child: Child,
87        stats: Stats,
88        options: ZonedLayoutOptions,
89    ) -> Self {
90        Self {
91            child: Arc::new(child),
92            stats: Arc::new(stats),
93            options,
94        }
95    }
96}
97
98#[async_trait]
99impl LayoutStrategy for ZonedStrategy {
100    async fn write_stream(
101        &self,
102        ctx: ArrayContext,
103        segment_sink: SegmentSinkRef,
104        stream: SendableSequentialStream,
105        mut eof: SequencePointer,
106        session: &VortexSession,
107    ) -> VortexResult<LayoutRef> {
108        let aggregate_fns = self
109            .options
110            .aggregate_fns
111            .clone()
112            .unwrap_or_else(|| default_zoned_aggregate_fns(stream.dtype()));
113        let compute_session = session.clone();
114
115        let stats_accumulator = Arc::new(Mutex::new(AggregateStatsAccumulator::new(
116            stream.dtype(),
117            &aggregate_fns,
118        )));
119        let aggregate_fns = stats_accumulator.lock().aggregate_fns();
120
121        let stream_dtype = stream.dtype().clone();
122        let concurrency = self.options.concurrency.get();
123        let stream = stream
124            .map(move |item| {
125                let aggregate_fns = Arc::clone(&aggregate_fns);
126                let session = compute_session.clone();
127                session.handle().spawn_cpu(move || {
128                    let (sequence_id, chunk) = item?;
129                    let partials = aggregate_partials(
130                        &chunk,
131                        &aggregate_fns,
132                        &mut session.create_execution_ctx(),
133                    )?;
134                    Ok::<_, VortexError>((sequence_id, chunk, partials))
135                })
136            })
137            .buffered(concurrency);
138
139        // Accumulate zone stats in stream order so the auxiliary table stays aligned with the
140        // data child.
141        let stats_accumulator2 = Arc::clone(&stats_accumulator);
142        let stream = SequentialStreamAdapter::new(
143            stream_dtype,
144            stream.map(move |item| {
145                let (sequence_id, chunk, partials) = item?;
146                stats_accumulator2.lock().push_partials(partials)?;
147                Ok((sequence_id, chunk))
148            }),
149        )
150        .sendable();
151
152        let block_size = self.options.block_size;
153
154        // The eof used for the data child should appear _before_ our own stats tables.
155        let data_eof = eof.split_off();
156        let data_layout = self
157            .child
158            .write_stream(
159                ctx.clone(),
160                Arc::clone(&segment_sink),
161                stream,
162                data_eof,
163                session,
164            )
165            .await?;
166
167        let Some((stats_array, aggregate_fns)) = stats_accumulator.lock().as_array()? else {
168            // If we have no stats (e.g. the DType doesn't support them), then we just return the
169            // child layout.
170            return Ok(data_layout);
171        };
172
173        // We must defer creating the stats table LayoutWriter until now, because the DType of
174        // the table depends on which stats were successfully computed.
175        let stats_stream = stats_array
176            .into_array()
177            .to_array_stream()
178            .sequenced(eof.split_off());
179        let zones_layout = self
180            .stats
181            .write_stream(ctx, Arc::clone(&segment_sink), stats_stream, eof, session)
182            .await?;
183
184        Ok(
185            ZonedLayout::try_new(data_layout, zones_layout, block_size, aggregate_fns)?
186                .into_layout(),
187        )
188    }
189
190    fn buffered_bytes(&self) -> u64 {
191        self.child.buffered_bytes() + self.stats.buffered_bytes()
192    }
193}
194
195fn default_zoned_aggregate_fns(dtype: &DType) -> Arc<[AggregateFnRef]> {
196    let (max, min) = match dtype {
197        DType::Utf8(_) | DType::Binary(_) => (
198            BoundedMax.bind(BoundedMaxOptions {
199                max_bytes: default_bounded_stat_max_bytes(),
200            }),
201            BoundedMin.bind(BoundedMinOptions {
202                max_bytes: default_bounded_stat_max_bytes(),
203            }),
204        ),
205        _ => (
206            Max.bind(NumericalAggregateOpts::skip_nans()),
207            Min.bind(NumericalAggregateOpts::skip_nans()),
208        ),
209    };
210
211    let mut aggregate_fns = vec![max, min];
212    if Sum
213        .return_dtype(&NumericalAggregateOpts::skip_nans(), dtype)
214        .is_some()
215    {
216        aggregate_fns.push(Sum.bind(NumericalAggregateOpts::skip_nans()));
217    }
218    aggregate_fns.push(NanCount.bind(EmptyOptions));
219    aggregate_fns.push(NullCount.bind(EmptyOptions));
220
221    aggregate_fns.into()
222}
223
224#[cfg(test)]
225mod tests {
226    use vortex_array::aggregate_fn::fns::bounded_max::BoundedMax;
227    use vortex_array::aggregate_fn::fns::bounded_min::BoundedMin;
228    use vortex_array::aggregate_fn::fns::max::Max;
229    use vortex_array::aggregate_fn::fns::min::Min;
230    use vortex_array::aggregate_fn::fns::sum::Sum;
231    use vortex_array::dtype::Nullability;
232    use vortex_array::dtype::PType;
233    use vortex_array::extension::datetime::TimeUnit;
234    use vortex_array::extension::datetime::Timestamp;
235
236    use super::*;
237
238    #[test]
239    fn default_aggregates_bound_variable_length_min_max() {
240        let aggregate_fns = default_zoned_aggregate_fns(&DType::Utf8(Nullability::NonNullable));
241
242        assert_eq!(
243            aggregate_fns[0].as_::<BoundedMax>().max_bytes,
244            default_bounded_stat_max_bytes()
245        );
246        assert_eq!(
247            aggregate_fns[1].as_::<BoundedMin>().max_bytes,
248            default_bounded_stat_max_bytes()
249        );
250    }
251
252    #[test]
253    fn default_aggregates_keep_fixed_width_min_max_exact() {
254        let aggregate_fns = default_zoned_aggregate_fns(&PType::I32.into());
255
256        assert!(aggregate_fns[0].is::<Max>());
257        assert!(aggregate_fns[1].is::<Min>());
258        assert!(aggregate_fns[2].is::<Sum>());
259    }
260
261    #[test]
262    fn default_aggregates_skip_sum_for_non_summable_dtype() {
263        let dtype = DType::Extension(
264            Timestamp::new(TimeUnit::Microseconds, Nullability::Nullable).erased(),
265        );
266        let aggregate_fns = default_zoned_aggregate_fns(&dtype);
267
268        assert!(
269            aggregate_fns
270                .iter()
271                .all(|aggregate_fn| !aggregate_fn.is::<Sum>())
272        );
273    }
274}