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 if ctx.has_nullable_ancestor() {
122 return self
123 .child
124 .write_stream(ctx, segment_sink, stream, eof, session)
125 .await;
126 }
127 let compute_session = session.clone();
128
129 let stats_accumulator = Arc::new(Mutex::new(AggregateStatsAccumulator::new(
130 stream.dtype(),
131 &aggregate_fns,
132 )));
133 let aggregate_fns = stats_accumulator.lock().aggregate_fns();
138 for aggregate_fn in aggregate_fns.iter() {
139 if !ctx.allows_aggregate(&aggregate_fn.id()) {
140 vortex_bail!("Aggregate {} not permitted by ctx", aggregate_fn.id());
141 }
142 }
143
144 let stream_dtype = stream.dtype().clone();
145 let concurrency = self.options.concurrency.get();
146 let stream = stream
147 .map(move |item| {
148 let aggregate_fns = Arc::clone(&aggregate_fns);
149 let session = compute_session.clone();
150 session.handle().spawn_cpu(move || {
151 let (sequence_id, chunk) = item?;
152 let partials = aggregate_partials(
153 &chunk,
154 &aggregate_fns,
155 &mut session.create_execution_ctx(),
156 )?;
157 Ok::<_, VortexError>((sequence_id, chunk, partials))
158 })
159 })
160 .buffered(concurrency);
161
162 let stats_accumulator2 = Arc::clone(&stats_accumulator);
165 let stream = SequentialStreamAdapter::new(
166 stream_dtype,
167 stream.map(move |item| {
168 let (sequence_id, chunk, partials) = item?;
169 stats_accumulator2.lock().push_partials(partials)?;
170 Ok((sequence_id, chunk))
171 }),
172 )
173 .sendable();
174
175 let block_size = self.options.block_size;
176
177 let data_eof = eof.split_off();
179 let data_layout = self
180 .child
181 .write_stream(
182 ctx.clone(),
183 Arc::clone(&segment_sink),
184 stream,
185 data_eof,
186 session,
187 )
188 .await?;
189
190 let mut exec_ctx = session.create_execution_ctx();
191 let Some((stats_array, aggregate_fns)) =
192 stats_accumulator.lock().as_array(&mut exec_ctx)?
193 else {
194 return Ok(data_layout);
197 };
198
199 let stats_stream = stats_array
202 .into_array()
203 .to_array_stream()
204 .sequenced(eof.split_off());
205 let zones_layout = self
206 .stats
207 .write_stream(ctx, Arc::clone(&segment_sink), stats_stream, eof, session)
208 .await?;
209
210 Ok(
211 ZonedLayout::try_new(data_layout, zones_layout, block_size, aggregate_fns)?
212 .into_layout(),
213 )
214 }
215}
216
217fn default_zoned_aggregate_fns(dtype: &DType, session: &VortexSession) -> Arc<[AggregateFnRef]> {
218 let (max, min) = match dtype {
219 DType::Utf8(_) | DType::Binary(_) => (
220 BoundedMax.bind(BoundedMaxOptions {
221 max_bytes: default_bounded_stat_max_bytes(),
222 }),
223 BoundedMin.bind(BoundedMinOptions {
224 max_bytes: default_bounded_stat_max_bytes(),
225 }),
226 ),
227 _ => (
228 Max.bind(NumericalAggregateOpts::skip_nans()),
229 Min.bind(NumericalAggregateOpts::skip_nans()),
230 ),
231 };
232
233 let mut aggregate_fns = vec![
238 max,
239 min,
240 NanCount.bind(EmptyOptions),
241 NullCount.bind(EmptyOptions),
242 ];
243
244 aggregate_fns.extend(session.aggregate_fns().zone_stat_defaults(dtype));
246
247 aggregate_fns.into()
248}
249
250#[cfg(test)]
251mod tests {
252 use rstest::rstest;
253 use vortex_array::ArrayContext;
254 use vortex_array::IntoArray;
255 use vortex_array::aggregate_fn::AggregateFnVTable;
256 use vortex_array::aggregate_fn::fns::bounded_max::BoundedMax;
257 use vortex_array::aggregate_fn::fns::bounded_min::BoundedMin;
258 use vortex_array::aggregate_fn::fns::max::Max;
259 use vortex_array::aggregate_fn::fns::min::Min;
260 use vortex_array::aggregate_fn::fns::sum::Sum;
261 use vortex_array::arrays::ChunkedArray;
262 use vortex_array::dtype::Nullability;
263 use vortex_array::dtype::PType;
264 use vortex_array::extension::datetime::TimeUnit;
265 use vortex_array::extension::datetime::Timestamp;
266 use vortex_buffer::buffer;
267 use vortex_error::VortexExpect;
268 use vortex_io::runtime::Handle;
269 use vortex_io::runtime::single::block_on;
270 use vortex_io::session::RuntimeSession;
271 use vortex_io::session::RuntimeSessionExt;
272 use vortex_utils::aliases::hash_set::HashSet;
273
274 use super::*;
275 use crate::layouts::chunked::writer::ChunkedLayoutStrategy;
276 use crate::layouts::flat::writer::FlatLayoutStrategy;
277 use crate::layouts::zoned::Zoned;
278 use crate::segments::TestSegments;
279 use crate::sequence::SequenceId;
280 use crate::sequence::SequentialArrayStreamExt;
281 use crate::session::LayoutSession;
282
283 fn write_zones(ctx: LayoutWriterContext) -> VortexResult<Vec<String>> {
286 Ok(write_layout(ctx)?
287 .as_::<Zoned>()
288 .aggregate_fns()
289 .iter()
290 .map(|aggregate_fn| aggregate_fn.id().to_string())
291 .collect())
292 }
293
294 fn write_layout(ctx: LayoutWriterContext) -> VortexResult<LayoutRef> {
296 let strategy = ZonedStrategy::new(
297 ChunkedLayoutStrategy::new(FlatLayoutStrategy::default()),
298 FlatLayoutStrategy::default(),
299 ZonedLayoutOptions {
300 block_size: NonZeroUsize::new(3).vortex_expect("non zero"),
301 ..Default::default()
302 },
303 );
304 let (ptr, eof) = SequenceId::root().split();
305 let stream = ChunkedArray::from_iter([
306 buffer![1, 2, 3].into_array(),
307 buffer![4, 5, 6].into_array(),
308 buffer![7, 8, 9].into_array(),
309 ])
310 .into_array()
311 .to_array_stream()
312 .sequenced(ptr);
313
314 block_on(|handle: Handle| async move {
315 let session = vortex_array::array_session()
316 .with::<LayoutSession>()
317 .with::<RuntimeSession>()
318 .with_handle(handle);
319 strategy
320 .write_stream(
321 ctx,
322 Arc::new(TestSegments::default()),
323 stream,
324 eof,
325 &session,
326 )
327 .await
328 })
329 }
330
331 #[test]
332 fn unrestricted_context_writes_the_default_aggregates() -> VortexResult<()> {
333 let written = write_zones(LayoutWriterContext::new(ArrayContext::empty()))?;
334 assert!(written.contains(&Min.id().to_string()));
335 assert!(written.contains(&Max.id().to_string()));
336 assert!(
337 !written.contains(&Sum.id().to_string()),
338 "wrote {written:?}"
339 );
340 Ok(())
341 }
342
343 #[test]
346 fn a_nullable_ancestor_writes_no_zone_map() -> VortexResult<()> {
347 let layout =
348 write_layout(LayoutWriterContext::new(ArrayContext::empty()).with_nullable_ancestor())?;
349 assert!(!layout.is::<Zoned>());
350 Ok(())
351 }
352
353 #[test]
354 fn a_permitted_set_covering_the_defaults_writes_them() -> VortexResult<()> {
355 let ctx = LayoutWriterContext::new(ArrayContext::empty()).with_allowed_aggregates(
356 HashSet::from_iter([Min.id(), Max.id(), NanCount.id(), NullCount.id()]),
357 );
358 assert!(write_zones(ctx)?.contains(&Max.id().to_string()));
359 Ok(())
360 }
361
362 #[test]
363 fn a_forbidden_aggregate_fails_the_write() {
364 let ctx = LayoutWriterContext::new(ArrayContext::empty())
365 .with_allowed_aggregates(HashSet::from_iter([Min.id()]));
366 let error = write_zones(ctx).expect_err("the default aggregates are not all permitted");
367 assert!(
368 error.to_string().contains("not permitted by ctx"),
369 "unexpected error: {error}"
370 );
371 }
372
373 #[test]
374 fn default_aggregates_bound_variable_length_min_max() {
375 let aggregate_fns = default_zoned_aggregate_fns(
376 &DType::Utf8(Nullability::NonNullable),
377 &vortex_array::array_session(),
378 );
379
380 assert_eq!(
381 aggregate_fns[0].as_::<BoundedMax>().max_bytes,
382 default_bounded_stat_max_bytes()
383 );
384 assert_eq!(
385 aggregate_fns[1].as_::<BoundedMin>().max_bytes,
386 default_bounded_stat_max_bytes()
387 );
388 }
389
390 #[test]
391 fn default_aggregates_keep_fixed_width_min_max_exact() {
392 let aggregate_fns =
393 default_zoned_aggregate_fns(&PType::I32.into(), &vortex_array::array_session());
394
395 assert!(aggregate_fns[0].is::<Max>());
396 assert!(aggregate_fns[1].is::<Min>());
397 assert!(aggregate_fns[2].is::<NanCount>());
398 }
399
400 #[rstest]
402 #[case::summable(PType::I32.into())]
403 #[case::not_summable(DType::Extension(
404 Timestamp::new(TimeUnit::Microseconds, Nullability::Nullable).erased(),
405 ))]
406 fn default_aggregates_never_record_sum(#[case] dtype: DType) {
407 let aggregate_fns = default_zoned_aggregate_fns(&dtype, &vortex_array::array_session());
408
409 assert!(
410 aggregate_fns
411 .iter()
412 .all(|aggregate_fn| !aggregate_fn.is::<Sum>())
413 );
414 }
415}