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::AggregateFnVTableExt;
16use vortex_array::aggregate_fn::EmptyOptions;
17use vortex_array::aggregate_fn::NumericalAggregateOpts;
18use vortex_array::aggregate_fn::fns::bounded_max::BoundedMax;
19use vortex_array::aggregate_fn::fns::bounded_max::BoundedMaxOptions;
20use vortex_array::aggregate_fn::fns::bounded_min::BoundedMin;
21use vortex_array::aggregate_fn::fns::bounded_min::BoundedMinOptions;
22use vortex_array::aggregate_fn::fns::max::Max;
23use vortex_array::aggregate_fn::fns::min::Min;
24use vortex_array::aggregate_fn::fns::nan_count::NanCount;
25use vortex_array::aggregate_fn::fns::null_count::NullCount;
26use vortex_array::aggregate_fn::session::AggregateFnSessionExt;
27use vortex_array::dtype::DType;
28use vortex_error::VortexError;
29use vortex_error::VortexResult;
30use vortex_error::vortex_bail;
31use vortex_io::session::RuntimeSessionExt;
32use vortex_session::VortexSession;
33use vortex_utils::parallelism::get_available_parallelism;
34
35use crate::LayoutRef;
36use crate::LayoutStrategy;
37use crate::LayoutWriterContext;
38use crate::layouts::zoned::AggregateStatsAccumulator;
39use crate::layouts::zoned::ZonedLayout;
40use crate::layouts::zoned::aggregate_partials;
41use crate::layouts::zoned::schema::default_bounded_stat_max_bytes;
42use crate::segments::SegmentSinkRef;
43use crate::sequence::SendableSequentialStream;
44use crate::sequence::SequencePointer;
45use crate::sequence::SequentialArrayStreamExt;
46use crate::sequence::SequentialStreamAdapter;
47use crate::sequence::SequentialStreamExt;
48
49pub struct ZonedLayoutOptions {
54 pub block_size: NonZeroUsize,
56 pub aggregate_fns: Option<Arc<[AggregateFnRef]>>,
60 pub concurrency: NonZeroUsize,
62}
63
64impl Default for ZonedLayoutOptions {
65 fn default() -> Self {
66 Self {
67 block_size: unsafe { NonZeroUsize::new_unchecked(8192) },
68 aggregate_fns: None,
69 concurrency: unsafe {
70 NonZeroUsize::new_unchecked(get_available_parallelism().unwrap_or(1))
71 },
72 }
73 }
74}
75
76pub struct ZonedStrategy {
77 child: Arc<dyn LayoutStrategy>,
78 stats: Arc<dyn LayoutStrategy>,
79 options: ZonedLayoutOptions,
80}
81
82impl ZonedStrategy {
83 pub fn new<Child: LayoutStrategy, Stats: LayoutStrategy>(
85 child: Child,
86 stats: Stats,
87 options: ZonedLayoutOptions,
88 ) -> Self {
89 Self {
90 child: Arc::new(child),
91 stats: Arc::new(stats),
92 options,
93 }
94 }
95}
96
97#[async_trait]
98impl LayoutStrategy for ZonedStrategy {
99 async fn write_stream(
100 &self,
101 ctx: LayoutWriterContext,
102 segment_sink: SegmentSinkRef,
103 stream: SendableSequentialStream,
104 mut eof: SequencePointer,
105 session: &VortexSession,
106 ) -> VortexResult<LayoutRef> {
107 let aggregate_fns = self
108 .options
109 .aggregate_fns
110 .clone()
111 .unwrap_or_else(|| default_zoned_aggregate_fns(stream.dtype(), session));
112 let compute_session = session.clone();
113
114 let stats_accumulator = Arc::new(Mutex::new(AggregateStatsAccumulator::new(
115 stream.dtype(),
116 &aggregate_fns,
117 )));
118 let aggregate_fns = stats_accumulator.lock().aggregate_fns();
123 for aggregate_fn in aggregate_fns.iter() {
124 if !ctx.allows_aggregate(&aggregate_fn.id()) {
125 vortex_bail!("Aggregate {} not permitted by ctx", aggregate_fn.id());
126 }
127 }
128
129 let stream_dtype = stream.dtype().clone();
130 let concurrency = self.options.concurrency.get();
131 let stream = stream
132 .map(move |item| {
133 let aggregate_fns = Arc::clone(&aggregate_fns);
134 let session = compute_session.clone();
135 session.handle().spawn_cpu(move || {
136 let (sequence_id, chunk) = item?;
137 let partials = aggregate_partials(
138 &chunk,
139 &aggregate_fns,
140 &mut session.create_execution_ctx(),
141 )?;
142 Ok::<_, VortexError>((sequence_id, chunk, partials))
143 })
144 })
145 .buffered(concurrency);
146
147 let stats_accumulator2 = Arc::clone(&stats_accumulator);
150 let stream = SequentialStreamAdapter::new(
151 stream_dtype,
152 stream.map(move |item| {
153 let (sequence_id, chunk, partials) = item?;
154 stats_accumulator2.lock().push_partials(partials)?;
155 Ok((sequence_id, chunk))
156 }),
157 )
158 .sendable();
159
160 let block_size = self.options.block_size;
161
162 let data_eof = eof.split_off();
164 let data_layout = self
165 .child
166 .write_stream(
167 ctx.clone(),
168 Arc::clone(&segment_sink),
169 stream,
170 data_eof,
171 session,
172 )
173 .await?;
174
175 let mut exec_ctx = session.create_execution_ctx();
176 let Some((stats_array, aggregate_fns)) =
177 stats_accumulator.lock().as_array(&mut exec_ctx)?
178 else {
179 return Ok(data_layout);
182 };
183
184 let stats_stream = stats_array
187 .into_array()
188 .to_array_stream()
189 .sequenced(eof.split_off());
190 let zones_layout = self
191 .stats
192 .write_stream(ctx, Arc::clone(&segment_sink), stats_stream, eof, session)
193 .await?;
194
195 Ok(
196 ZonedLayout::try_new(data_layout, zones_layout, block_size, aggregate_fns)?
197 .into_layout(),
198 )
199 }
200}
201
202fn default_zoned_aggregate_fns(dtype: &DType, session: &VortexSession) -> Arc<[AggregateFnRef]> {
203 let (max, min) = match dtype {
204 DType::Utf8(_) | DType::Binary(_) => (
205 BoundedMax.bind(BoundedMaxOptions {
206 max_bytes: default_bounded_stat_max_bytes(),
207 }),
208 BoundedMin.bind(BoundedMinOptions {
209 max_bytes: default_bounded_stat_max_bytes(),
210 }),
211 ),
212 _ => (
213 Max.bind(NumericalAggregateOpts::skip_nans()),
214 Min.bind(NumericalAggregateOpts::skip_nans()),
215 ),
216 };
217
218 let mut aggregate_fns = vec![
223 max,
224 min,
225 NanCount.bind(EmptyOptions),
226 NullCount.bind(EmptyOptions),
227 ];
228
229 aggregate_fns.extend(session.aggregate_fns().zone_stat_defaults(dtype));
231
232 aggregate_fns.into()
233}
234
235#[cfg(test)]
236mod tests {
237 use rstest::rstest;
238 use vortex_array::ArrayContext;
239 use vortex_array::IntoArray;
240 use vortex_array::aggregate_fn::AggregateFnVTable;
241 use vortex_array::aggregate_fn::fns::bounded_max::BoundedMax;
242 use vortex_array::aggregate_fn::fns::bounded_min::BoundedMin;
243 use vortex_array::aggregate_fn::fns::max::Max;
244 use vortex_array::aggregate_fn::fns::min::Min;
245 use vortex_array::aggregate_fn::fns::sum::Sum;
246 use vortex_array::arrays::ChunkedArray;
247 use vortex_array::dtype::Nullability;
248 use vortex_array::dtype::PType;
249 use vortex_array::extension::datetime::TimeUnit;
250 use vortex_array::extension::datetime::Timestamp;
251 use vortex_buffer::buffer;
252 use vortex_error::VortexExpect;
253 use vortex_io::runtime::Handle;
254 use vortex_io::runtime::single::block_on;
255 use vortex_io::session::RuntimeSession;
256 use vortex_io::session::RuntimeSessionExt;
257 use vortex_utils::aliases::hash_set::HashSet;
258
259 use super::*;
260 use crate::layouts::chunked::writer::ChunkedLayoutStrategy;
261 use crate::layouts::flat::writer::FlatLayoutStrategy;
262 use crate::layouts::zoned::Zoned;
263 use crate::segments::TestSegments;
264 use crate::sequence::SequenceId;
265 use crate::sequence::SequentialArrayStreamExt;
266 use crate::session::LayoutSession;
267
268 fn write_zones(ctx: LayoutWriterContext) -> VortexResult<Vec<String>> {
271 let strategy = ZonedStrategy::new(
272 ChunkedLayoutStrategy::new(FlatLayoutStrategy::default()),
273 FlatLayoutStrategy::default(),
274 ZonedLayoutOptions {
275 block_size: NonZeroUsize::new(3).vortex_expect("non zero"),
276 ..Default::default()
277 },
278 );
279 let (ptr, eof) = SequenceId::root().split();
280 let stream = ChunkedArray::from_iter([
281 buffer![1, 2, 3].into_array(),
282 buffer![4, 5, 6].into_array(),
283 buffer![7, 8, 9].into_array(),
284 ])
285 .into_array()
286 .to_array_stream()
287 .sequenced(ptr);
288
289 let layout = block_on(|handle: Handle| async move {
290 let session = vortex_array::array_session()
291 .with::<LayoutSession>()
292 .with::<RuntimeSession>()
293 .with_handle(handle);
294 strategy
295 .write_stream(
296 ctx,
297 Arc::new(TestSegments::default()),
298 stream,
299 eof,
300 &session,
301 )
302 .await
303 })?;
304
305 Ok(layout
306 .as_::<Zoned>()
307 .aggregate_fns()
308 .iter()
309 .map(|aggregate_fn| aggregate_fn.id().to_string())
310 .collect())
311 }
312
313 #[test]
314 fn unrestricted_context_writes_the_default_aggregates() -> VortexResult<()> {
315 let written = write_zones(LayoutWriterContext::new(ArrayContext::empty()))?;
316 assert!(written.contains(&Min.id().to_string()));
317 assert!(written.contains(&Max.id().to_string()));
318 assert!(
319 !written.contains(&Sum.id().to_string()),
320 "wrote {written:?}"
321 );
322 Ok(())
323 }
324
325 #[test]
326 fn a_permitted_set_covering_the_defaults_writes_them() -> VortexResult<()> {
327 let ctx = LayoutWriterContext::new(ArrayContext::empty()).with_allowed_aggregates(
328 HashSet::from_iter([Min.id(), Max.id(), NanCount.id(), NullCount.id()]),
329 );
330 assert!(write_zones(ctx)?.contains(&Max.id().to_string()));
331 Ok(())
332 }
333
334 #[test]
335 fn a_forbidden_aggregate_fails_the_write() {
336 let ctx = LayoutWriterContext::new(ArrayContext::empty())
337 .with_allowed_aggregates(HashSet::from_iter([Min.id()]));
338 let error = write_zones(ctx).expect_err("the default aggregates are not all permitted");
339 assert!(
340 error.to_string().contains("not permitted by ctx"),
341 "unexpected error: {error}"
342 );
343 }
344
345 #[test]
346 fn default_aggregates_bound_variable_length_min_max() {
347 let aggregate_fns = default_zoned_aggregate_fns(
348 &DType::Utf8(Nullability::NonNullable),
349 &vortex_array::array_session(),
350 );
351
352 assert_eq!(
353 aggregate_fns[0].as_::<BoundedMax>().max_bytes,
354 default_bounded_stat_max_bytes()
355 );
356 assert_eq!(
357 aggregate_fns[1].as_::<BoundedMin>().max_bytes,
358 default_bounded_stat_max_bytes()
359 );
360 }
361
362 #[test]
363 fn default_aggregates_keep_fixed_width_min_max_exact() {
364 let aggregate_fns =
365 default_zoned_aggregate_fns(&PType::I32.into(), &vortex_array::array_session());
366
367 assert!(aggregate_fns[0].is::<Max>());
368 assert!(aggregate_fns[1].is::<Min>());
369 assert!(aggregate_fns[2].is::<NanCount>());
370 }
371
372 #[rstest]
374 #[case::summable(PType::I32.into())]
375 #[case::not_summable(DType::Extension(
376 Timestamp::new(TimeUnit::Microseconds, Nullability::Nullable).erased(),
377 ))]
378 fn default_aggregates_never_record_sum(#[case] dtype: DType) {
379 let aggregate_fns = default_zoned_aggregate_fns(&dtype, &vortex_array::array_session());
380
381 assert!(
382 aggregate_fns
383 .iter()
384 .all(|aggregate_fn| !aggregate_fn.is::<Sum>())
385 );
386 }
387}