vortex_layout/layouts/zoned/
writer.rs1use 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
50pub struct ZonedLayoutOptions {
55 pub block_size: NonZeroUsize,
57 pub aggregate_fns: Option<Arc<[AggregateFnRef]>>,
61 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 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 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 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 return Ok(data_layout);
171 };
172
173 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}