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::IntoArray;
13use vortex_array::VortexSessionExecute;
14use vortex_array::aggregate_fn::AggregateFnRef;
15use vortex_array::aggregate_fn::AggregateFnVTable;
16use vortex_array::aggregate_fn::AggregateFnVTableExt;
17use vortex_array::aggregate_fn::EmptyOptions;
18use vortex_array::aggregate_fn::NumericalAggregateOpts;
19use vortex_array::aggregate_fn::fns::bounded_max::BoundedMax;
20use vortex_array::aggregate_fn::fns::bounded_max::BoundedMaxOptions;
21use vortex_array::aggregate_fn::fns::bounded_min::BoundedMin;
22use vortex_array::aggregate_fn::fns::bounded_min::BoundedMinOptions;
23use vortex_array::aggregate_fn::fns::max::Max;
24use vortex_array::aggregate_fn::fns::min::Min;
25use vortex_array::aggregate_fn::fns::nan_count::NanCount;
26use vortex_array::aggregate_fn::fns::null_count::NullCount;
27use vortex_array::aggregate_fn::fns::sum::Sum;
28use vortex_array::aggregate_fn::session::AggregateFnSessionExt;
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::LayoutRef;
37use crate::LayoutStrategy;
38use crate::LayoutWriterContext;
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: LayoutWriterContext,
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(), session));
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 mut exec_ctx = session.create_execution_ctx();
168 let Some((stats_array, aggregate_fns)) =
169 stats_accumulator.lock().as_array(&mut exec_ctx)?
170 else {
171 return Ok(data_layout);
174 };
175
176 let stats_stream = stats_array
179 .into_array()
180 .to_array_stream()
181 .sequenced(eof.split_off());
182 let zones_layout = self
183 .stats
184 .write_stream(ctx, Arc::clone(&segment_sink), stats_stream, eof, session)
185 .await?;
186
187 Ok(
188 ZonedLayout::try_new(data_layout, zones_layout, block_size, aggregate_fns)?
189 .into_layout(),
190 )
191 }
192}
193
194fn default_zoned_aggregate_fns(dtype: &DType, session: &VortexSession) -> Arc<[AggregateFnRef]> {
195 let (max, min) = match dtype {
196 DType::Utf8(_) | DType::Binary(_) => (
197 BoundedMax.bind(BoundedMaxOptions {
198 max_bytes: default_bounded_stat_max_bytes(),
199 }),
200 BoundedMin.bind(BoundedMinOptions {
201 max_bytes: default_bounded_stat_max_bytes(),
202 }),
203 ),
204 _ => (
205 Max.bind(NumericalAggregateOpts::skip_nans()),
206 Min.bind(NumericalAggregateOpts::skip_nans()),
207 ),
208 };
209
210 let mut aggregate_fns = vec![max, min];
211 if Sum
212 .return_dtype(&NumericalAggregateOpts::skip_nans(), dtype)
213 .is_some()
214 {
215 aggregate_fns.push(Sum.bind(NumericalAggregateOpts::skip_nans()));
216 }
217 aggregate_fns.push(NanCount.bind(EmptyOptions));
218 aggregate_fns.push(NullCount.bind(EmptyOptions));
219
220 aggregate_fns.extend(session.aggregate_fns().zone_stat_defaults(dtype));
222
223 aggregate_fns.into()
224}
225
226#[cfg(test)]
227mod tests {
228 use vortex_array::aggregate_fn::fns::bounded_max::BoundedMax;
229 use vortex_array::aggregate_fn::fns::bounded_min::BoundedMin;
230 use vortex_array::aggregate_fn::fns::max::Max;
231 use vortex_array::aggregate_fn::fns::min::Min;
232 use vortex_array::aggregate_fn::fns::sum::Sum;
233 use vortex_array::dtype::Nullability;
234 use vortex_array::dtype::PType;
235 use vortex_array::extension::datetime::TimeUnit;
236 use vortex_array::extension::datetime::Timestamp;
237
238 use super::*;
239
240 #[test]
241 fn default_aggregates_bound_variable_length_min_max() {
242 let aggregate_fns = default_zoned_aggregate_fns(
243 &DType::Utf8(Nullability::NonNullable),
244 &vortex_array::array_session(),
245 );
246
247 assert_eq!(
248 aggregate_fns[0].as_::<BoundedMax>().max_bytes,
249 default_bounded_stat_max_bytes()
250 );
251 assert_eq!(
252 aggregate_fns[1].as_::<BoundedMin>().max_bytes,
253 default_bounded_stat_max_bytes()
254 );
255 }
256
257 #[test]
258 fn default_aggregates_keep_fixed_width_min_max_exact() {
259 let aggregate_fns =
260 default_zoned_aggregate_fns(&PType::I32.into(), &vortex_array::array_session());
261
262 assert!(aggregate_fns[0].is::<Max>());
263 assert!(aggregate_fns[1].is::<Min>());
264 assert!(aggregate_fns[2].is::<Sum>());
265 }
266
267 #[test]
268 fn default_aggregates_skip_sum_for_non_summable_dtype() {
269 let dtype = DType::Extension(
270 Timestamp::new(TimeUnit::Microseconds, Nullability::Nullable).erased(),
271 );
272 let aggregate_fns = default_zoned_aggregate_fns(&dtype, &vortex_array::array_session());
273
274 assert!(
275 aggregate_fns
276 .iter()
277 .all(|aggregate_fn| !aggregate_fn.is::<Sum>())
278 );
279 }
280}