1use std::sync::Arc;
16
17use async_trait::async_trait;
18use vortex_array::dtype::Field;
19use vortex_array::dtype::FieldName;
20use vortex_array::dtype::FieldPath;
21use vortex_error::VortexResult;
22use vortex_session::VortexSession;
23use vortex_utils::aliases::hash_map::HashMap;
24use vortex_utils::aliases::hash_set::HashSet;
25
26use crate::LayoutRef;
27use crate::LayoutStrategy;
28use crate::LayoutWriterContext;
29use crate::layouts::list::writer::ListLayoutStrategy;
30use crate::layouts::struct_::StructStrategy;
31use crate::segments::SegmentSinkRef;
32use crate::sequence::SendableSequentialStream;
33use crate::sequence::SequencePointer;
34
35type ListLayoutFactory = Arc<dyn Fn(ListLayoutStrategy) -> Arc<dyn LayoutStrategy> + Send + Sync>;
36
37pub struct TableStrategy {
52 leaf_writers: HashMap<FieldPath, Arc<dyn LayoutStrategy>>,
55 validity: Arc<dyn LayoutStrategy>,
57 leaf: Arc<dyn LayoutStrategy>,
59 list_layout_factory: Option<ListLayoutFactory>,
64}
65
66impl TableStrategy {
67 pub fn new(validity: Arc<dyn LayoutStrategy>, fallback: Arc<dyn LayoutStrategy>) -> Self {
86 Self {
87 leaf_writers: Default::default(),
88 validity,
89 leaf: fallback,
90 list_layout_factory: None,
91 }
92 }
93
94 pub fn with_field_writer(
123 mut self,
124 field_path: impl Into<FieldPath>,
125 writer: Arc<dyn LayoutStrategy>,
126 ) -> Self {
127 self.leaf_writers
128 .insert(self.validate_path(field_path.into()), writer);
129 self
130 }
131
132 pub fn with_field_writers(
136 mut self,
137 writers: impl IntoIterator<Item = (FieldPath, Arc<dyn LayoutStrategy>)>,
138 ) -> Self {
139 for (field_path, strategy) in writers {
140 self.leaf_writers
141 .insert(self.validate_path(field_path), strategy);
142 }
143 self
144 }
145
146 pub fn with_default_strategy(mut self, default: Arc<dyn LayoutStrategy>) -> Self {
148 self.leaf = default;
149 self
150 }
151
152 pub fn with_validity_strategy(mut self, validity: Arc<dyn LayoutStrategy>) -> Self {
154 self.validity = validity;
155 self
156 }
157
158 pub fn with_list_layout(self) -> Self {
163 self.with_list_layout_factory(|strategy| Arc::new(strategy))
164 }
165
166 pub fn with_list_layout_factory(
172 mut self,
173 factory: impl Fn(ListLayoutStrategy) -> Arc<dyn LayoutStrategy> + Send + Sync + 'static,
174 ) -> Self {
175 self.list_layout_factory = Some(Arc::new(factory));
176 self
177 }
178}
179
180impl TableStrategy {
181 fn struct_strategy(&self) -> StructStrategy {
186 let mut field_writers: HashMap<FieldName, Arc<dyn LayoutStrategy>> = HashMap::default();
187
188 let mut named_first: HashSet<FieldName> = HashSet::default();
191 for path in self.leaf_writers.keys() {
192 if let Some(Field::Name(name)) = path.parts().first() {
193 named_first.insert(name.clone());
194 }
195 }
196
197 for name in named_first {
198 let writer = match self.leaf_writers.get(&FieldPath::from_name(name.clone())) {
201 Some(exact) => Arc::clone(exact),
202 None => {
203 Arc::new(self.descend(&Field::Name(name.clone()))) as Arc<dyn LayoutStrategy>
204 }
205 };
206 field_writers.insert(name, writer);
207 }
208
209 StructStrategy::new(Arc::clone(&self.validity), Arc::new(self.descend_clean()))
210 .with_field_writers(field_writers)
211 }
212
213 fn list_strategy(&self) -> Option<Arc<dyn LayoutStrategy>> {
219 let factory = self.list_layout_factory.as_ref()?;
220 let list_layout = ListLayoutStrategy::default()
221 .with_elements(Arc::new(self.descend_clean()))
222 .with_offsets(Arc::clone(&self.leaf))
223 .with_validity(Arc::clone(&self.validity))
224 .with_fallback(Arc::clone(&self.leaf));
225 Some(factory(list_layout))
226 }
227
228 fn descend(&self, field: &Field) -> Self {
231 let mut new_writers = HashMap::with_capacity(self.leaf_writers.len());
232
233 for (field_path, strategy) in &self.leaf_writers {
234 if field_path.parts().first() == Some(field)
235 && let Some(subpath) = field_path.clone().step_into()
236 && !subpath.is_root()
237 {
238 new_writers.insert(subpath, Arc::clone(strategy));
239 }
240 }
241
242 Self {
243 leaf_writers: new_writers,
244 validity: Arc::clone(&self.validity),
245 leaf: Arc::clone(&self.leaf),
246 list_layout_factory: self.list_layout_factory.clone(),
247 }
248 }
249
250 fn descend_clean(&self) -> Self {
253 Self {
254 leaf_writers: HashMap::default(),
255 validity: Arc::clone(&self.validity),
256 leaf: Arc::clone(&self.leaf),
257 list_layout_factory: self.list_layout_factory.clone(),
258 }
259 }
260
261 fn validate_path(&self, path: FieldPath) -> FieldPath {
262 assert!(
263 !path.is_root(),
264 "Do not set override as a root strategy, instead set the default strategy"
265 );
266
267 for field_path in self.leaf_writers.keys() {
270 assert!(
271 !path.overlap(field_path),
272 "Override for field_path {path} conflicts with existing override for {field_path}"
273 );
274 }
275
276 path
277 }
278}
279
280#[async_trait]
282impl LayoutStrategy for TableStrategy {
283 async fn write_stream(
284 &self,
285 ctx: LayoutWriterContext,
286 segment_sink: SegmentSinkRef,
287 stream: SendableSequentialStream,
288 eof: SequencePointer,
289 session: &VortexSession,
290 ) -> VortexResult<LayoutRef> {
291 let dtype = stream.dtype().clone();
292
293 if dtype.is_struct() {
294 return self
295 .struct_strategy()
296 .write_stream(ctx, segment_sink, stream, eof, session)
297 .await;
298 }
299
300 if dtype.is_list()
301 && let Some(list_strategy) = self.list_strategy()
302 {
303 return list_strategy
304 .write_stream(ctx, segment_sink, stream, eof, session)
305 .await;
306 }
307
308 self.leaf
310 .write_stream(ctx, segment_sink, stream, eof, session)
311 .await
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use std::num::NonZeroUsize;
318 use std::sync::Arc;
319 use std::task::Poll;
320
321 use vortex_array::ArrayContext;
322 use vortex_array::ArrayRef;
323 use vortex_array::IntoArray;
324 use vortex_array::arrays::BoolArray;
325 use vortex_array::arrays::ChunkedArray;
326 use vortex_array::arrays::ListArray;
327 use vortex_array::arrays::PrimitiveArray;
328 use vortex_array::arrays::StructArray;
329 use vortex_array::dtype::DType;
330 use vortex_array::dtype::FieldPath;
331 use vortex_array::dtype::Nullability;
332 use vortex_array::dtype::PType;
333 use vortex_array::dtype::StructFields;
334 use vortex_array::field_path;
335 use vortex_array::validity::Validity;
336 use vortex_buffer::buffer;
337 use vortex_error::VortexExpect;
338 use vortex_error::VortexResult;
339 use vortex_io::session::RuntimeSessionExt;
340
341 use crate::LayoutRef;
342 use crate::LayoutStrategy;
343 use crate::layouts::chunked::Chunked;
344 use crate::layouts::chunked::writer::ChunkedLayoutStrategy;
345 use crate::layouts::flat::Flat;
346 use crate::layouts::flat::writer::FlatLayoutStrategy;
347 use crate::layouts::list::List;
348 use crate::layouts::repartition::RepartitionStrategy;
349 use crate::layouts::repartition::RepartitionWriterOptions;
350 use crate::layouts::table::TableStrategy;
351 use crate::layouts::zoned::Zoned;
352 use crate::layouts::zoned::writer::ZonedLayoutOptions;
353 use crate::layouts::zoned::writer::ZonedStrategy;
354 use crate::segments::TestSegments;
355 use crate::sequence::SequenceId;
356 use crate::sequence::SequentialArrayStreamExt;
357 use crate::sequence::SequentialStreamAdapter;
358 use crate::sequence::SequentialStreamExt;
359 use crate::test::SESSION;
360 use crate::test::new_session;
361
362 async fn write<S: LayoutStrategy>(strategy: &S, array: ArrayRef) -> VortexResult<LayoutRef> {
363 let segments = Arc::new(TestSegments::default());
364 let (ptr, eof) = SequenceId::root().split();
365 let stream = array.to_array_stream().sequenced(ptr);
366 let session = new_session().with_tokio();
367 strategy
368 .write_stream(
369 ArrayContext::empty().into(),
370 segments,
371 stream,
372 eof,
373 &session,
374 )
375 .await
376 }
377
378 fn flat_table() -> TableStrategy {
381 let flat: Arc<dyn LayoutStrategy> = Arc::new(FlatLayoutStrategy::default());
382 TableStrategy::new(Arc::clone(&flat), flat)
383 }
384
385 #[tokio::test]
387 async fn dispatches_struct() -> VortexResult<()> {
388 let struct_array = StructArray::from_fields(
389 [
390 ("a", buffer![1i32, 2, 3].into_array()),
391 ("b", buffer![10i32, 20, 30].into_array()),
392 ]
393 .as_slice(),
394 )?
395 .into_array();
396
397 let layout = write(&flat_table(), struct_array).await?;
398 insta::assert_snapshot!(layout.display_tree(), @r"
399 vortex.struct, dtype: {a=i32, b=i32}, children: 2
400 ├── a: vortex.flat, dtype: i32, segment: 0
401 └── b: vortex.flat, dtype: i32, segment: 1
402 ");
403 Ok(())
404 }
405
406 #[tokio::test]
409 async fn dispatches_nested_list() -> VortexResult<()> {
410 let inner = ListArray::try_new(
411 buffer![1i32, 2, 3, 4, 5, 6].into_array(),
412 buffer![0u32, 2, 5, 5, 6].into_array(),
413 Validity::NonNullable,
414 )?
415 .into_array();
416 let outer = ListArray::try_new(
417 inner,
418 buffer![0u32, 2, 4].into_array(),
419 Validity::NonNullable,
420 )?
421 .into_array();
422
423 let layout = write(&flat_table().with_list_layout(), outer).await?;
424 insta::assert_snapshot!(layout.display_tree(), @r"
425 vortex.list, dtype: list(list(i32)), children: 2
426 ├── elements: vortex.list, dtype: list(i32), children: 2
427 │ ├── elements: vortex.flat, dtype: i32, segment: 1
428 │ └── offsets: vortex.flat, dtype: u64, segment: 2
429 └── offsets: vortex.flat, dtype: u64, segment: 0
430 ");
431 Ok(())
432 }
433
434 #[tokio::test]
437 async fn dispatches_struct_list_struct() -> VortexResult<()> {
438 let inner_struct = StructArray::from_fields(
439 [
440 ("a", buffer![1i32, 2, 3, 4, 5].into_array()),
441 ("b", buffer![10i32, 20, 30, 40, 50].into_array()),
442 ]
443 .as_slice(),
444 )?
445 .into_array();
446 let items = ListArray::try_new(
447 inner_struct,
448 buffer![0u32, 2, 5, 5].into_array(),
449 Validity::Array(BoolArray::from_iter([true, false, true]).into_array()),
450 )?
451 .into_array();
452 let st = StructArray::from_fields([("items", items)].as_slice())?.into_array();
453
454 let layout = write(&flat_table().with_list_layout(), st).await?;
455 insta::assert_snapshot!(layout.display_tree(), @r"
456 vortex.struct, dtype: {items=list({a=i32, b=i32})?}, children: 1
457 └── items: vortex.list, dtype: list({a=i32, b=i32})?, children: 3
458 ├── elements: vortex.struct, dtype: {a=i32, b=i32}, children: 2
459 │ ├── a: vortex.flat, dtype: i32, segment: 2
460 │ └── b: vortex.flat, dtype: i32, segment: 3
461 ├── offsets: vortex.flat, dtype: u64, segment: 0
462 └── validity: vortex.flat, dtype: bool, segment: 1
463 ");
464 Ok(())
465 }
466
467 #[tokio::test]
471 async fn dispatches_chunked_list() -> VortexResult<()> {
472 let chunk0 = ListArray::try_new(
473 buffer![1i32, 2, 3].into_array(),
474 buffer![0u32, 2, 3].into_array(),
475 Validity::NonNullable,
476 )?
477 .into_array();
478 let chunk1 = ListArray::try_new(
479 buffer![4i32, 5, 6, 7].into_array(),
480 buffer![0u32, 1, 4].into_array(),
481 Validity::NonNullable,
482 )?
483 .into_array();
484 let dtype = chunk0.dtype().clone();
485 let chunked = ChunkedArray::try_new(vec![chunk0, chunk1], dtype)?.into_array();
486
487 let flat: Arc<dyn LayoutStrategy> = Arc::new(FlatLayoutStrategy::default());
488 let dispatcher = TableStrategy::new(
489 Arc::clone(&flat),
490 Arc::new(ChunkedLayoutStrategy::new(FlatLayoutStrategy::default())),
491 )
492 .with_list_layout();
493 let layout = write(&dispatcher, chunked).await?;
494 insta::assert_snapshot!(layout.display_tree(), @r"
495 vortex.list, dtype: list(i32), children: 2
496 ├── elements: vortex.chunked, dtype: i32, children: 2
497 │ ├── [0]: vortex.flat, dtype: i32, segment: 0
498 │ └── [1]: vortex.flat, dtype: i32, segment: 1
499 └── offsets: vortex.chunked, dtype: u64, children: 2
500 ├── [0]: vortex.flat, dtype: u64, segment: 2
501 └── [1]: vortex.flat, dtype: u64, segment: 3
502 ");
503 Ok(())
504 }
505
506 #[tokio::test]
508 async fn wraps_list_strategy_before_decomposition() -> VortexResult<()> {
509 let list = ListArray::try_new(
510 PrimitiveArray::from_iter(0..9_i32).into_array(),
511 PrimitiveArray::from_iter(0..=9_u32).into_array(),
512 Validity::NonNullable,
513 )?
514 .into_array();
515
516 let row_block_size = NonZeroUsize::new(4).vortex_expect("4 is non-zero");
517 let flat: Arc<dyn LayoutStrategy> = Arc::new(FlatLayoutStrategy::default());
518 let stats = Arc::clone(&flat);
519 let chunked: Arc<dyn LayoutStrategy> =
520 Arc::new(ChunkedLayoutStrategy::new(FlatLayoutStrategy::default()));
521 let dispatcher = TableStrategy::new(Arc::clone(&flat), chunked).with_list_layout_factory(
522 move |list_layout| {
523 let zoned = ZonedStrategy::new(
524 list_layout,
525 Arc::clone(&stats),
526 ZonedLayoutOptions {
527 block_size: row_block_size,
528 ..Default::default()
529 },
530 );
531 Arc::new(RepartitionStrategy::new(
532 zoned,
533 RepartitionWriterOptions {
534 block_size_minimum: 0,
535 block_len_multiple: row_block_size.get(),
536 block_size_target: None,
537 canonicalize: false,
538 },
539 )) as Arc<dyn LayoutStrategy>
540 },
541 );
542
543 let layout = write(&dispatcher, list).await?;
544 let zoned = layout.as_::<Zoned>();
545 assert_eq!(zoned.zone_len(), 4);
546 assert_eq!(zoned.nzones(), 3);
547
548 let data = layout
549 .slot(0)?
550 .vortex_expect("ZonedLayout always has a data child");
551 assert!(data.is::<List>());
552 assert_eq!(data.row_count(), 9);
553 Ok(())
554 }
555
556 #[tokio::test]
558 async fn non_struct_input_uses_leaf() -> VortexResult<()> {
559 let primitive = PrimitiveArray::from_iter([1i32, 2, 3]).into_array();
560 let layout = write(&flat_table(), primitive).await?;
561 insta::assert_snapshot!(layout.display_tree(), @"vortex.flat, dtype: i32, segment: 0");
562 Ok(())
563 }
564
565 #[tokio::test]
567 async fn chunked_struct() -> VortexResult<()> {
568 let validity: Arc<dyn LayoutStrategy> = Arc::new(FlatLayoutStrategy::default());
569 let chunked_flat: Arc<dyn LayoutStrategy> =
570 Arc::new(ChunkedLayoutStrategy::new(FlatLayoutStrategy::default()));
571 let dispatcher = TableStrategy::new(validity, chunked_flat);
572
573 let c0 = StructArray::from_fields(
574 [
575 ("a", buffer![1i32, 2].into_array()),
576 ("b", buffer![10i32, 20].into_array()),
577 ]
578 .as_slice(),
579 )?
580 .into_array();
581 let c1 = StructArray::from_fields(
582 [
583 ("a", buffer![3i32].into_array()),
584 ("b", buffer![30i32].into_array()),
585 ]
586 .as_slice(),
587 )?
588 .into_array();
589 let dtype = c0.dtype().clone();
590 let chunked = ChunkedArray::try_new(vec![c0, c1], dtype)?.into_array();
591
592 let layout = write(&dispatcher, chunked).await?;
593 insta::assert_snapshot!(layout.display_tree(), @r"
594 vortex.struct, dtype: {a=i32, b=i32}, children: 2
595 ├── a: vortex.chunked, dtype: i32, children: 2
596 │ ├── [0]: vortex.flat, dtype: i32, segment: 0
597 │ └── [1]: vortex.flat, dtype: i32, segment: 1
598 └── b: vortex.chunked, dtype: i32, children: 2
599 ├── [0]: vortex.flat, dtype: i32, segment: 2
600 └── [1]: vortex.flat, dtype: i32, segment: 3
601 ");
602 Ok(())
603 }
604
605 #[tokio::test]
607 async fn field_override_is_used() -> VortexResult<()> {
608 let struct_array = StructArray::from_fields(
609 [
610 ("a", buffer![1i32, 2, 3].into_array()),
611 ("b", buffer![10i32, 20, 30].into_array()),
612 ]
613 .as_slice(),
614 )?
615 .into_array();
616
617 let strategy = flat_table().with_field_writer(
618 field_path!(a),
619 Arc::new(RepartitionStrategy::new(
620 ChunkedLayoutStrategy::new(FlatLayoutStrategy::default()),
621 RepartitionWriterOptions {
622 block_size_minimum: 0,
623 block_len_multiple: 2,
624 block_size_target: None,
625 canonicalize: false,
626 },
627 )),
628 );
629 let layout = write(&strategy, struct_array).await?;
630 assert_eq!(layout.nchildren(), 2);
631 let overridden = layout.slot(1)?.vortex_expect("field a");
632 assert!(overridden.is::<Chunked>());
633 assert_eq!(overridden.nchildren(), 2);
634 assert_eq!(
635 overridden.slot(0)?.vortex_expect("first chunk").row_count(),
636 2
637 );
638 assert_eq!(
639 overridden
640 .slot(1)?
641 .vortex_expect("second chunk")
642 .row_count(),
643 1
644 );
645 assert!(layout.slot(2)?.vortex_expect("field b").is::<Flat>());
646 Ok(())
647 }
648
649 #[test]
650 #[should_panic(
651 expected = "Override for field_path $a.$b conflicts with existing override for $a.$b.$c"
652 )]
653 fn test_overlapping_paths_fail() {
654 let flat = Arc::new(FlatLayoutStrategy::default());
655
656 let path = TableStrategy::new(
658 Arc::<FlatLayoutStrategy>::clone(&flat),
659 Arc::<FlatLayoutStrategy>::clone(&flat),
660 )
661 .with_field_writer(field_path!(a.b.c), Arc::<FlatLayoutStrategy>::clone(&flat));
662
663 let _path = path.with_field_writer(field_path!(a.b), flat);
665 }
666
667 #[test]
668 #[should_panic(
669 expected = "Do not set override as a root strategy, instead set the default strategy"
670 )]
671 fn test_root_override() {
672 let flat = Arc::new(FlatLayoutStrategy::default());
673 let _strategy = TableStrategy::new(
674 Arc::<FlatLayoutStrategy>::clone(&flat),
675 Arc::<FlatLayoutStrategy>::clone(&flat),
676 )
677 .with_field_writer(FieldPath::root(), flat);
678 }
679
680 #[tokio::test]
681 #[should_panic(expected = "panic while transposing table stream")]
682 async fn table_fanout_panic_propagates() {
683 let ctx = ArrayContext::empty();
684 let segments = Arc::new(TestSegments::default());
685 let (_, eof) = SequenceId::root().split();
686 let dtype = DType::Struct(
687 StructFields::from_iter([(
688 "a",
689 DType::Primitive(PType::I32, Nullability::NonNullable),
690 )]),
691 Nullability::NonNullable,
692 );
693 let stream =
694 futures::stream::poll_fn(|_| -> Poll<Option<VortexResult<(SequenceId, ArrayRef)>>> {
695 panic!("panic while transposing table stream");
696 });
697 let strategy = TableStrategy::new(
698 Arc::new(FlatLayoutStrategy::default()),
699 Arc::new(FlatLayoutStrategy::default()),
700 );
701
702 strategy
703 .write_stream(
704 ctx.into(),
705 segments,
706 SequentialStreamAdapter::new(dtype, stream).sendable(),
707 eof,
708 &SESSION,
709 )
710 .await
711 .unwrap();
712 }
713}