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