1use std::env;
16use std::sync::Arc;
17use std::sync::LazyLock;
18
19use async_trait::async_trait;
20use vortex_array::ArrayContext;
21use vortex_array::dtype::Field;
22use vortex_array::dtype::FieldName;
23use vortex_array::dtype::FieldPath;
24use vortex_error::VortexResult;
25use vortex_session::VortexSession;
26use vortex_utils::aliases::hash_map::HashMap;
27use vortex_utils::aliases::hash_set::HashSet;
28
29use crate::LayoutRef;
30use crate::LayoutStrategy;
31use crate::layouts::list::writer::ListLayoutStrategy;
32use crate::layouts::struct_::StructStrategy;
33use crate::segments::SegmentSinkRef;
34use crate::sequence::SendableSequentialStream;
35use crate::sequence::SequencePointer;
36
37pub fn use_experimental_list_layout() -> bool {
43 static USE_EXPERIMENTAL_LIST_LAYOUT: LazyLock<bool> =
44 LazyLock::new(|| env::var("VORTEX_EXPERIMENTAL_LIST_LAYOUT").is_ok_and(|v| v == "1"));
45 *USE_EXPERIMENTAL_LIST_LAYOUT
46}
47
48type ListLayoutFactory = Arc<dyn Fn(ListLayoutStrategy) -> Arc<dyn LayoutStrategy> + Send + Sync>;
49
50pub struct TableStrategy {
65 leaf_writers: HashMap<FieldPath, Arc<dyn LayoutStrategy>>,
68 validity: Arc<dyn LayoutStrategy>,
70 leaf: Arc<dyn LayoutStrategy>,
72 list_layout_factory: Option<ListLayoutFactory>,
77}
78
79impl TableStrategy {
80 pub fn new(validity: Arc<dyn LayoutStrategy>, fallback: Arc<dyn LayoutStrategy>) -> Self {
99 Self {
100 leaf_writers: Default::default(),
101 validity,
102 leaf: fallback,
103 list_layout_factory: None,
104 }
105 }
106
107 pub fn with_field_writer(
136 mut self,
137 field_path: impl Into<FieldPath>,
138 writer: Arc<dyn LayoutStrategy>,
139 ) -> Self {
140 self.leaf_writers
141 .insert(self.validate_path(field_path.into()), writer);
142 self
143 }
144
145 pub fn with_field_writers(
149 mut self,
150 writers: impl IntoIterator<Item = (FieldPath, Arc<dyn LayoutStrategy>)>,
151 ) -> Self {
152 for (field_path, strategy) in writers {
153 self.leaf_writers
154 .insert(self.validate_path(field_path), strategy);
155 }
156 self
157 }
158
159 pub fn with_default_strategy(mut self, default: Arc<dyn LayoutStrategy>) -> Self {
161 self.leaf = default;
162 self
163 }
164
165 pub fn with_validity_strategy(mut self, validity: Arc<dyn LayoutStrategy>) -> Self {
167 self.validity = validity;
168 self
169 }
170
171 pub fn with_list_layout(self) -> Self {
176 self.with_list_layout_factory(|strategy| Arc::new(strategy))
177 }
178
179 pub fn with_list_layout_factory(
185 mut self,
186 factory: impl Fn(ListLayoutStrategy) -> Arc<dyn LayoutStrategy> + Send + Sync + 'static,
187 ) -> Self {
188 self.list_layout_factory = Some(Arc::new(factory));
189 self
190 }
191}
192
193impl TableStrategy {
194 fn struct_strategy(&self) -> StructStrategy {
199 let mut field_writers: HashMap<FieldName, Arc<dyn LayoutStrategy>> = HashMap::default();
200
201 let mut named_first: HashSet<FieldName> = HashSet::default();
204 for path in self.leaf_writers.keys() {
205 if let Some(Field::Name(name)) = path.parts().first() {
206 named_first.insert(name.clone());
207 }
208 }
209
210 for name in named_first {
211 let writer = match self.leaf_writers.get(&FieldPath::from_name(name.clone())) {
214 Some(exact) => Arc::clone(exact),
215 None => {
216 Arc::new(self.descend(&Field::Name(name.clone()))) as Arc<dyn LayoutStrategy>
217 }
218 };
219 field_writers.insert(name, writer);
220 }
221
222 StructStrategy::new(Arc::clone(&self.validity), Arc::new(self.descend_clean()))
223 .with_field_writers(field_writers)
224 }
225
226 fn list_strategy(&self) -> Option<Arc<dyn LayoutStrategy>> {
232 let factory = self.list_layout_factory.as_ref()?;
233 let list_layout = ListLayoutStrategy::default()
234 .with_elements(Arc::new(self.descend_clean()))
235 .with_offsets(Arc::clone(&self.leaf))
236 .with_validity(Arc::clone(&self.validity))
237 .with_fallback(Arc::clone(&self.leaf));
238 Some(factory(list_layout))
239 }
240
241 fn descend(&self, field: &Field) -> Self {
244 let mut new_writers = HashMap::with_capacity(self.leaf_writers.len());
245
246 for (field_path, strategy) in &self.leaf_writers {
247 if field_path.parts().first() == Some(field)
248 && let Some(subpath) = field_path.clone().step_into()
249 && !subpath.is_root()
250 {
251 new_writers.insert(subpath, Arc::clone(strategy));
252 }
253 }
254
255 Self {
256 leaf_writers: new_writers,
257 validity: Arc::clone(&self.validity),
258 leaf: Arc::clone(&self.leaf),
259 list_layout_factory: self.list_layout_factory.clone(),
260 }
261 }
262
263 fn descend_clean(&self) -> Self {
266 Self {
267 leaf_writers: HashMap::default(),
268 validity: Arc::clone(&self.validity),
269 leaf: Arc::clone(&self.leaf),
270 list_layout_factory: self.list_layout_factory.clone(),
271 }
272 }
273
274 fn validate_path(&self, path: FieldPath) -> FieldPath {
275 assert!(
276 !path.is_root(),
277 "Do not set override as a root strategy, instead set the default strategy"
278 );
279
280 for field_path in self.leaf_writers.keys() {
283 assert!(
284 !path.overlap(field_path),
285 "Override for field_path {path} conflicts with existing override for {field_path}"
286 );
287 }
288
289 path
290 }
291}
292
293#[async_trait]
295impl LayoutStrategy for TableStrategy {
296 async fn write_stream(
297 &self,
298 ctx: ArrayContext,
299 segment_sink: SegmentSinkRef,
300 stream: SendableSequentialStream,
301 eof: SequencePointer,
302 session: &VortexSession,
303 ) -> VortexResult<LayoutRef> {
304 let dtype = stream.dtype().clone();
305
306 if dtype.is_struct() {
307 return self
308 .struct_strategy()
309 .write_stream(ctx, segment_sink, stream, eof, session)
310 .await;
311 }
312
313 if dtype.is_list()
314 && let Some(list_strategy) = self.list_strategy()
315 {
316 return list_strategy
317 .write_stream(ctx, segment_sink, stream, eof, session)
318 .await;
319 }
320
321 self.leaf
323 .write_stream(ctx, segment_sink, stream, eof, session)
324 .await
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use std::num::NonZeroUsize;
331 use std::sync::Arc;
332 use std::task::Poll;
333
334 use vortex_array::ArrayContext;
335 use vortex_array::ArrayRef;
336 use vortex_array::IntoArray;
337 use vortex_array::arrays::BoolArray;
338 use vortex_array::arrays::ChunkedArray;
339 use vortex_array::arrays::ListArray;
340 use vortex_array::arrays::PrimitiveArray;
341 use vortex_array::arrays::StructArray;
342 use vortex_array::dtype::DType;
343 use vortex_array::dtype::FieldPath;
344 use vortex_array::dtype::Nullability;
345 use vortex_array::dtype::PType;
346 use vortex_array::dtype::StructFields;
347 use vortex_array::field_path;
348 use vortex_array::validity::Validity;
349 use vortex_buffer::buffer;
350 use vortex_error::VortexExpect;
351 use vortex_error::VortexResult;
352
353 use crate::LayoutRef;
354 use crate::LayoutStrategy;
355 use crate::layouts::chunked::writer::ChunkedLayoutStrategy;
356 use crate::layouts::flat::writer::FlatLayoutStrategy;
357 use crate::layouts::list::List;
358 use crate::layouts::repartition::RepartitionStrategy;
359 use crate::layouts::repartition::RepartitionWriterOptions;
360 use crate::layouts::table::TableStrategy;
361 use crate::layouts::zoned::Zoned;
362 use crate::layouts::zoned::writer::ZonedLayoutOptions;
363 use crate::layouts::zoned::writer::ZonedStrategy;
364 use crate::segments::TestSegments;
365 use crate::sequence::SequenceId;
366 use crate::sequence::SequentialArrayStreamExt;
367 use crate::sequence::SequentialStreamAdapter;
368 use crate::sequence::SequentialStreamExt;
369 use crate::test::SESSION;
370
371 async fn write<S: LayoutStrategy>(strategy: &S, array: ArrayRef) -> VortexResult<LayoutRef> {
372 let segments = Arc::new(TestSegments::default());
373 let (ptr, eof) = SequenceId::root().split();
374 let stream = array.to_array_stream().sequenced(ptr);
375 strategy
376 .write_stream(ArrayContext::empty(), segments, stream, eof, &SESSION)
377 .await
378 }
379
380 fn flat_table() -> TableStrategy {
383 let flat: Arc<dyn LayoutStrategy> = Arc::new(FlatLayoutStrategy::default());
384 TableStrategy::new(Arc::clone(&flat), flat)
385 }
386
387 #[tokio::test]
389 async fn dispatches_struct() -> VortexResult<()> {
390 let struct_array = StructArray::from_fields(
391 [
392 ("a", buffer![1i32, 2, 3].into_array()),
393 ("b", buffer![10i32, 20, 30].into_array()),
394 ]
395 .as_slice(),
396 )?
397 .into_array();
398
399 let layout = write(&flat_table(), struct_array).await?;
400 insta::assert_snapshot!(layout.display_tree(), @r"
401 vortex.struct, dtype: {a=i32, b=i32}, children: 2
402 ├── a: vortex.flat, dtype: i32, segment: 0
403 └── b: vortex.flat, dtype: i32, segment: 1
404 ");
405 Ok(())
406 }
407
408 #[tokio::test]
411 async fn dispatches_nested_list() -> VortexResult<()> {
412 let inner = ListArray::try_new(
413 buffer![1i32, 2, 3, 4, 5, 6].into_array(),
414 buffer![0u32, 2, 5, 5, 6].into_array(),
415 Validity::NonNullable,
416 )?
417 .into_array();
418 let outer = ListArray::try_new(
419 inner,
420 buffer![0u32, 2, 4].into_array(),
421 Validity::NonNullable,
422 )?
423 .into_array();
424
425 let layout = write(&flat_table().with_list_layout(), outer).await?;
426 insta::assert_snapshot!(layout.display_tree(), @r"
427 vortex.list, dtype: list(list(i32)), children: 2
428 ├── elements: vortex.list, dtype: list(i32), children: 2
429 │ ├── elements: vortex.flat, dtype: i32, segment: 1
430 │ └── offsets: vortex.flat, dtype: u64, segment: 2
431 └── offsets: vortex.flat, dtype: u64, segment: 0
432 ");
433 Ok(())
434 }
435
436 #[tokio::test]
439 async fn dispatches_struct_list_struct() -> VortexResult<()> {
440 let inner_struct = StructArray::from_fields(
441 [
442 ("a", buffer![1i32, 2, 3, 4, 5].into_array()),
443 ("b", buffer![10i32, 20, 30, 40, 50].into_array()),
444 ]
445 .as_slice(),
446 )?
447 .into_array();
448 let items = ListArray::try_new(
449 inner_struct,
450 buffer![0u32, 2, 5, 5].into_array(),
451 Validity::Array(BoolArray::from_iter([true, false, true]).into_array()),
452 )?
453 .into_array();
454 let st = StructArray::from_fields([("items", items)].as_slice())?.into_array();
455
456 let layout = write(&flat_table().with_list_layout(), st).await?;
457 insta::assert_snapshot!(layout.display_tree(), @r"
458 vortex.struct, dtype: {items=list({a=i32, b=i32})?}, children: 1
459 └── items: vortex.list, dtype: list({a=i32, b=i32})?, children: 3
460 ├── elements: vortex.struct, dtype: {a=i32, b=i32}, children: 2
461 │ ├── a: vortex.flat, dtype: i32, segment: 2
462 │ └── b: vortex.flat, dtype: i32, segment: 3
463 ├── offsets: vortex.flat, dtype: u64, segment: 0
464 └── validity: vortex.flat, dtype: bool, segment: 1
465 ");
466 Ok(())
467 }
468
469 #[tokio::test]
473 async fn dispatches_chunked_list() -> VortexResult<()> {
474 let chunk0 = ListArray::try_new(
475 buffer![1i32, 2, 3].into_array(),
476 buffer![0u32, 2, 3].into_array(),
477 Validity::NonNullable,
478 )?
479 .into_array();
480 let chunk1 = ListArray::try_new(
481 buffer![4i32, 5, 6, 7].into_array(),
482 buffer![0u32, 1, 4].into_array(),
483 Validity::NonNullable,
484 )?
485 .into_array();
486 let dtype = chunk0.dtype().clone();
487 let chunked = ChunkedArray::try_new(vec![chunk0, chunk1], dtype)?.into_array();
488
489 let flat: Arc<dyn LayoutStrategy> = Arc::new(FlatLayoutStrategy::default());
490 let dispatcher = TableStrategy::new(
491 Arc::clone(&flat),
492 Arc::new(ChunkedLayoutStrategy::new(FlatLayoutStrategy::default())),
493 )
494 .with_list_layout();
495 let layout = write(&dispatcher, chunked).await?;
496 insta::assert_snapshot!(layout.display_tree(), @r"
497 vortex.list, dtype: list(i32), children: 2
498 ├── elements: vortex.chunked, dtype: i32, children: 2
499 │ ├── [0]: vortex.flat, dtype: i32, segment: 0
500 │ └── [1]: vortex.flat, dtype: i32, segment: 1
501 └── offsets: vortex.chunked, dtype: u64, children: 2
502 ├── [0]: vortex.flat, dtype: u64, segment: 2
503 └── [1]: vortex.flat, dtype: u64, segment: 3
504 ");
505 Ok(())
506 }
507
508 #[tokio::test]
510 async fn wraps_list_strategy_before_decomposition() -> VortexResult<()> {
511 let list = ListArray::try_new(
512 PrimitiveArray::from_iter(0..9_i32).into_array(),
513 PrimitiveArray::from_iter(0..=9_u32).into_array(),
514 Validity::NonNullable,
515 )?
516 .into_array();
517
518 let row_block_size = NonZeroUsize::new(4).vortex_expect("4 is non-zero");
519 let flat: Arc<dyn LayoutStrategy> = Arc::new(FlatLayoutStrategy::default());
520 let stats = Arc::clone(&flat);
521 let chunked: Arc<dyn LayoutStrategy> =
522 Arc::new(ChunkedLayoutStrategy::new(FlatLayoutStrategy::default()));
523 let dispatcher = TableStrategy::new(Arc::clone(&flat), chunked).with_list_layout_factory(
524 move |list_layout| {
525 let zoned = ZonedStrategy::new(
526 list_layout,
527 Arc::clone(&stats),
528 ZonedLayoutOptions {
529 block_size: row_block_size,
530 ..Default::default()
531 },
532 );
533 Arc::new(RepartitionStrategy::new(
534 zoned,
535 RepartitionWriterOptions {
536 block_size_minimum: 0,
537 block_len_multiple: row_block_size.get(),
538 block_size_target: None,
539 canonicalize: false,
540 },
541 )) as Arc<dyn LayoutStrategy>
542 },
543 );
544
545 let layout = write(&dispatcher, list).await?;
546 let zoned = layout.as_::<Zoned>();
547 assert_eq!(zoned.zone_len(), 4);
548 assert_eq!(zoned.nzones(), 3);
549
550 let data = layout.child(0)?;
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 =
618 flat_table().with_field_writer(field_path!(a), Arc::new(FlatLayoutStrategy::default()));
619 let layout = write(&strategy, struct_array).await?;
620 insta::assert_snapshot!(layout.display_tree(), @r"
621 vortex.struct, dtype: {a=i32, b=i32}, children: 2
622 ├── a: vortex.flat, dtype: i32, segment: 0
623 └── b: vortex.flat, dtype: i32, segment: 1
624 ");
625 Ok(())
626 }
627
628 #[test]
629 #[should_panic(
630 expected = "Override for field_path $a.$b conflicts with existing override for $a.$b.$c"
631 )]
632 fn test_overlapping_paths_fail() {
633 let flat = Arc::new(FlatLayoutStrategy::default());
634
635 let path = TableStrategy::new(
637 Arc::<FlatLayoutStrategy>::clone(&flat),
638 Arc::<FlatLayoutStrategy>::clone(&flat),
639 )
640 .with_field_writer(field_path!(a.b.c), Arc::<FlatLayoutStrategy>::clone(&flat));
641
642 let _path = path.with_field_writer(field_path!(a.b), flat);
644 }
645
646 #[test]
647 #[should_panic(
648 expected = "Do not set override as a root strategy, instead set the default strategy"
649 )]
650 fn test_root_override() {
651 let flat = Arc::new(FlatLayoutStrategy::default());
652 let _strategy = TableStrategy::new(
653 Arc::<FlatLayoutStrategy>::clone(&flat),
654 Arc::<FlatLayoutStrategy>::clone(&flat),
655 )
656 .with_field_writer(FieldPath::root(), flat);
657 }
658
659 #[tokio::test]
660 #[should_panic(expected = "panic while transposing table stream")]
661 async fn table_fanout_panic_propagates() {
662 let ctx = ArrayContext::empty();
663 let segments = Arc::new(TestSegments::default());
664 let (_, eof) = SequenceId::root().split();
665 let dtype = DType::Struct(
666 StructFields::from_iter([(
667 "a",
668 DType::Primitive(PType::I32, Nullability::NonNullable),
669 )]),
670 Nullability::NonNullable,
671 );
672 let stream =
673 futures::stream::poll_fn(|_| -> Poll<Option<VortexResult<(SequenceId, ArrayRef)>>> {
674 panic!("panic while transposing table stream");
675 });
676 let strategy = TableStrategy::new(
677 Arc::new(FlatLayoutStrategy::default()),
678 Arc::new(FlatLayoutStrategy::default()),
679 );
680
681 strategy
682 .write_stream(
683 ctx,
684 segments,
685 SequentialStreamAdapter::new(dtype, stream).sendable(),
686 eof,
687 &SESSION,
688 )
689 .await
690 .unwrap();
691 }
692}