use std::sync::Arc;
use async_trait::async_trait;
use futures::StreamExt;
use futures::future::try_join;
use futures::future::try_join_all;
use vortex_array::ArrayContext;
use vortex_array::ArrayRef;
use vortex_array::ExecutionCtx;
use vortex_array::IntoArray;
use vortex_array::VortexSessionExecute;
use vortex_array::arrays::ConstantArray;
use vortex_array::arrays::List;
use vortex_array::arrays::ListView;
use vortex_array::arrays::PrimitiveArray;
use vortex_array::arrays::list::ListDataParts;
use vortex_array::arrays::listview::list_from_list_view;
use vortex_array::builtins::ArrayBuiltins;
use vortex_array::dtype::DType;
use vortex_array::dtype::Nullability;
use vortex_array::dtype::PType;
use vortex_array::matcher::Matcher;
use vortex_array::scalar_fn::fns::operators::Operator;
use vortex_error::VortexExpect;
use vortex_error::VortexResult;
use vortex_error::vortex_bail;
use vortex_io::kanal_ext::KanalExt;
use vortex_io::session::RuntimeSessionExt;
use vortex_session::VortexSession;
use crate::IntoLayout;
use crate::LayoutRef;
use crate::LayoutStrategy;
use crate::layouts::flat::writer::FlatLayoutStrategy;
use crate::layouts::list::ListLayout;
use crate::segments::SegmentSinkRef;
use crate::sequence::SendableSequentialStream;
use crate::sequence::SequenceId;
use crate::sequence::SequencePointer;
use crate::sequence::SequentialStream;
use crate::sequence::SequentialStreamAdapter;
use crate::sequence::SequentialStreamExt;
type ChildChunk = VortexResult<(SequenceId, ArrayRef)>;
#[derive(Clone)]
pub struct ListLayoutStrategy {
elements: Arc<dyn LayoutStrategy>,
offsets: Arc<dyn LayoutStrategy>,
validity: Arc<dyn LayoutStrategy>,
fallback: Arc<dyn LayoutStrategy>,
}
impl Default for ListLayoutStrategy {
fn default() -> Self {
let flat: Arc<dyn LayoutStrategy> = Arc::new(FlatLayoutStrategy::default());
Self {
elements: Arc::clone(&flat),
offsets: Arc::clone(&flat),
validity: Arc::clone(&flat),
fallback: flat,
}
}
}
impl ListLayoutStrategy {
pub fn with_elements(mut self, elements: Arc<dyn LayoutStrategy>) -> Self {
self.elements = elements;
self
}
pub fn with_offsets(mut self, offsets: Arc<dyn LayoutStrategy>) -> Self {
self.offsets = offsets;
self
}
pub fn with_validity(mut self, validity: Arc<dyn LayoutStrategy>) -> Self {
self.validity = validity;
self
}
pub fn with_fallback(mut self, fallback: Arc<dyn LayoutStrategy>) -> Self {
self.fallback = fallback;
self
}
}
#[async_trait]
impl LayoutStrategy for ListLayoutStrategy {
async fn write_stream(
&self,
ctx: ArrayContext,
segment_sink: SegmentSinkRef,
stream: SendableSequentialStream,
mut eof: SequencePointer,
session: &VortexSession,
) -> VortexResult<LayoutRef> {
let dtype = stream.dtype().clone();
if !dtype.is_list() {
return self
.fallback
.write_stream(ctx, segment_sink, stream, eof, session)
.await;
}
let is_nullable = dtype.is_nullable();
let element_dtype = dtype
.as_list_element_opt()
.vortex_expect("DType is List")
.as_ref()
.clone();
let offsets_dtype = DType::Primitive(PType::U64, Nullability::NonNullable);
let (elements_tx, elements_rx) = kanal::bounded_async::<ChildChunk>(1);
let (offsets_tx, offsets_rx) = kanal::bounded_async::<ChildChunk>(1);
let (validity_tx, validity_rx) = if is_nullable {
let (tx, rx) = kanal::bounded_async::<ChildChunk>(1);
(Some(tx), Some(rx))
} else {
(None, None)
};
let fanout_fut = transpose_list_column(
stream,
session.clone(),
elements_tx,
offsets_tx,
validity_tx,
);
let handle = session.handle();
let mut child_specs: Vec<(
DType,
Arc<dyn LayoutStrategy>,
kanal::AsyncReceiver<ChildChunk>,
)> = vec![
(element_dtype, Arc::clone(&self.elements), elements_rx),
(offsets_dtype, Arc::clone(&self.offsets), offsets_rx),
];
if let Some(validity_rx) = validity_rx {
child_specs.push((
DType::Bool(Nullability::NonNullable),
Arc::clone(&self.validity),
validity_rx,
));
}
let layout_futures: Vec<_> = child_specs
.into_iter()
.map(|(child_dtype, strategy, rx)| {
let child_stream =
SequentialStreamAdapter::new(child_dtype, rx.into_stream().boxed()).sendable();
let child_eof = eof.split_off();
let ctx = ctx.clone();
let segment_sink = Arc::clone(&segment_sink);
let session = session.clone();
handle.spawn_nested(move |h| async move {
let session = session.with_handle(h);
strategy
.write_stream(ctx, segment_sink, child_stream, child_eof, &session)
.await
})
})
.collect();
let (_, layouts) = try_join(fanout_fut, try_join_all(layout_futures)).await?;
let mut layouts = layouts.into_iter();
let elements_layout = layouts.next().vortex_expect("elements layout present");
let offsets_layout = layouts.next().vortex_expect("offsets layout present");
let validity_layout =
is_nullable.then(|| layouts.next().vortex_expect("validity layout present"));
Ok(ListLayout::new(dtype, elements_layout, offsets_layout, validity_layout).into_layout())
}
fn buffered_bytes(&self) -> u64 {
let list_bytes = self.elements.buffered_bytes()
+ self.offsets.buffered_bytes()
+ self.validity.buffered_bytes();
list_bytes.max(self.fallback.buffered_bytes())
}
}
async fn transpose_list_column(
mut stream: SendableSequentialStream,
session: VortexSession,
elements_tx: kanal::AsyncSender<ChildChunk>,
offsets_tx: kanal::AsyncSender<ChildChunk>,
validity_tx: Option<kanal::AsyncSender<ChildChunk>>,
) -> VortexResult<()> {
let mut exec_ctx = session.create_execution_ctx();
let mut element_base: u64 = 0;
let mut first = true;
let mut saw_chunk = false;
while let Some(chunk) = stream.next().await {
let (sequence_id, array) = chunk?;
saw_chunk = true;
let mut sp = sequence_id.descend();
let ListDataParts {
elements,
offsets,
validity,
..
} = canonicalize_to_list_parts(array, &mut exec_ctx)?;
let n_elements = elements.len() as u64;
let row_count = offsets.len().saturating_sub(1);
let offsets = global_offsets(offsets, element_base, first, &mut exec_ctx)?;
element_base += n_elements;
first = false;
if elements_tx
.send(Ok((sp.advance(), elements)))
.await
.is_err()
|| offsets_tx.send(Ok((sp.advance(), offsets))).await.is_err()
{
vortex_bail!("list child writer finished before all chunks were sent");
}
if let Some(validity_tx) = &validity_tx {
let validity = validity
.execute_mask(row_count, &mut exec_ctx)?
.into_array();
if validity_tx
.send(Ok((sp.advance(), validity)))
.await
.is_err()
{
vortex_bail!("list validity writer finished before all chunks were sent");
}
}
}
if !saw_chunk {
vortex_bail!("ListLayoutStrategy needs at least one chunk");
}
Ok(())
}
fn canonicalize_to_list_parts(
array: ArrayRef,
exec_ctx: &mut ExecutionCtx,
) -> VortexResult<ListDataParts> {
let canonical = array.execute_until::<AnyList>(exec_ctx)?;
if let Some(list) = canonical.as_opt::<List>() {
Ok(list.into_owned().into_data_parts())
} else if let Some(view) = canonical.as_opt::<ListView>() {
Ok(list_from_list_view(view.into_owned(), exec_ctx)?.into_data_parts())
} else {
unreachable!("AnyList matcher guarantees List or ListView")
}
}
fn global_offsets(
offsets: ArrayRef,
element_base: u64,
first: bool,
exec_ctx: &mut ExecutionCtx,
) -> VortexResult<ArrayRef> {
let widened = offsets.cast(DType::Primitive(PType::U64, Nullability::NonNullable))?;
let based = if element_base == 0 {
widened
} else {
let base = ConstantArray::new(element_base, widened.len()).into_array();
widened.binary(base, Operator::Add)?
};
let based = if first {
based
} else {
based.slice(1..based.len())?
};
Ok(based.execute::<PrimitiveArray>(exec_ctx)?.into_array())
}
struct AnyList;
impl Matcher for AnyList {
type Match<'a> = ();
fn try_match(array: &ArrayRef) -> Option<Self::Match<'_>> {
(array.as_opt::<List>().is_some() || array.as_opt::<ListView>().is_some()).then_some(())
}
}
#[cfg(test)]
mod tests {
use futures::stream;
use vortex_array::arrays::BoolArray;
use vortex_array::arrays::ChunkedArray;
use vortex_array::arrays::ListArray;
use vortex_array::arrays::StructArray;
use vortex_array::dtype::Nullability;
use vortex_array::dtype::PType;
use vortex_array::validity::Validity;
use vortex_buffer::buffer;
use vortex_io::session::RuntimeSession;
use super::*;
use crate::layouts::chunked::writer::ChunkedLayoutStrategy;
use crate::layouts::flat::writer::FlatLayoutStrategy;
use crate::layouts::table::TableStrategy;
use crate::segments::TestSegments;
use crate::sequence::SequentialArrayStreamExt;
use crate::session::LayoutSession;
fn layout_test_session() -> VortexSession {
vortex_array::array_session()
.with::<LayoutSession>()
.with::<RuntimeSession>()
.with_tokio()
}
fn flat_list_strategy() -> ListLayoutStrategy {
ListLayoutStrategy::default()
}
async fn write<S: LayoutStrategy>(strategy: &S, array: ArrayRef) -> VortexResult<LayoutRef> {
let session = layout_test_session();
let segments = Arc::new(TestSegments::default());
let (ptr, eof) = SequenceId::root().split();
let stream = array.to_array_stream().sequenced(ptr);
strategy
.write_stream(ArrayContext::empty(), segments, stream, eof, &session)
.await
}
fn i32_list_dtype(nullable: bool) -> DType {
DType::List(
Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
if nullable {
Nullability::Nullable
} else {
Nullability::NonNullable
},
)
}
fn create_basic_list(validity: Validity) -> ArrayRef {
ListArray::try_new(
buffer![1i32, 2, 3, 4, 5].into_array(),
buffer![0u32, 2, 5, 5].into_array(),
validity,
)
.unwrap()
.into_array()
}
#[tokio::test]
async fn basic_non_nullable_input() -> VortexResult<()> {
let list = create_basic_list(Validity::NonNullable);
let layout = write(&flat_list_strategy(), list).await?;
assert_eq!(layout.row_count(), 3);
insta::assert_snapshot!(layout.display_tree(), @"
vortex.list, dtype: list(i32), children: 2
├── elements: vortex.flat, dtype: i32, segment: 0
└── offsets: vortex.flat, dtype: u64, segment: 1
");
Ok(())
}
#[tokio::test]
async fn basic_nullable_input() -> VortexResult<()> {
let list = create_basic_list(Validity::Array(
BoolArray::from_iter([true, false, true]).into_array(),
));
let layout = write(&flat_list_strategy(), list).await?;
assert_eq!(layout.row_count(), 3);
insta::assert_snapshot!(layout.display_tree(), @"
vortex.list, dtype: list(i32)?, children: 3
├── elements: vortex.flat, dtype: i32, segment: 0
├── offsets: vortex.flat, dtype: u64, segment: 1
└── validity: vortex.flat, dtype: bool, segment: 2
");
Ok(())
}
#[tokio::test]
async fn non_list_input_routes_to_fallback() -> VortexResult<()> {
let primitive = buffer![1i32, 2, 3].into_array();
let layout = write(&flat_list_strategy(), primitive).await?;
insta::assert_snapshot!(layout.display_tree(), @"vortex.flat, dtype: i32, segment: 0");
Ok(())
}
#[tokio::test]
async fn empty_stream_errors() {
let segments = Arc::new(TestSegments::default());
let (_, eof) = SequenceId::root().split();
let empty = stream::empty::<VortexResult<(SequenceId, ArrayRef)>>().boxed();
let stream = SequentialStreamAdapter::new(i32_list_dtype(false), empty).sendable();
let session = layout_test_session();
let res = flat_list_strategy()
.write_stream(ArrayContext::empty(), segments, stream, eof, &session)
.await;
assert!(res.is_err())
}
#[tokio::test]
async fn list_of_struct_tree() -> VortexResult<()> {
let struct_array = StructArray::from_fields(
[
("a", buffer![1i32, 2, 3, 4, 5].into_array()),
("b", buffer![10i32, 20, 30, 40, 50].into_array()),
]
.as_slice(),
)?
.into_array();
let list = ListArray::try_new(
struct_array,
buffer![0u32, 2, 5, 5].into_array(),
Validity::NonNullable,
)?
.into_array();
let flat: Arc<dyn LayoutStrategy> = Arc::new(FlatLayoutStrategy::default());
let table_strategy: Arc<dyn LayoutStrategy> =
Arc::new(TableStrategy::new(Arc::clone(&flat), Arc::clone(&flat)));
let writer = ListLayoutStrategy::default().with_elements(table_strategy);
let layout = write(&writer, list).await?;
insta::assert_snapshot!(layout.display_tree(), @"
vortex.list, dtype: list({a=i32, b=i32}), children: 2
├── elements: vortex.struct, dtype: {a=i32, b=i32}, children: 2
│ ├── a: vortex.flat, dtype: i32, segment: 1
│ └── b: vortex.flat, dtype: i32, segment: 2
└── offsets: vortex.flat, dtype: u64, segment: 0
");
Ok(())
}
#[tokio::test]
async fn list_of_list_tree() -> VortexResult<()> {
let inner_list = ListArray::try_new(
buffer![1i32, 2, 3, 4, 5, 6].into_array(),
buffer![0u32, 2, 5, 5, 6].into_array(),
Validity::NonNullable,
)?
.into_array();
let list = ListArray::try_new(
inner_list,
buffer![0u32, 2, 4].into_array(),
Validity::NonNullable,
)?
.into_array();
let writer =
ListLayoutStrategy::default().with_elements(Arc::new(ListLayoutStrategy::default()));
let layout = write(&writer, list).await?;
insta::assert_snapshot!(layout.display_tree(), @"
vortex.list, dtype: list(list(i32)), children: 2
├── elements: vortex.list, dtype: list(i32), children: 2
│ ├── elements: vortex.flat, dtype: i32, segment: 1
│ └── offsets: vortex.flat, dtype: u64, segment: 2
└── offsets: vortex.flat, dtype: u64, segment: 0
");
Ok(())
}
#[tokio::test]
async fn list_of_list_of_list_tree() -> VortexResult<()> {
let innermost = ListArray::try_new(
buffer![1i32, 2, 3, 4].into_array(),
buffer![0u32, 2, 4].into_array(),
Validity::NonNullable,
)?
.into_array();
let middle = ListArray::try_new(
innermost,
buffer![0u32, 2].into_array(),
Validity::NonNullable,
)?
.into_array();
let outer =
ListArray::try_new(middle, buffer![0u32, 1].into_array(), Validity::NonNullable)?
.into_array();
let writer = ListLayoutStrategy::default().with_elements(Arc::new(
ListLayoutStrategy::default().with_elements(Arc::new(ListLayoutStrategy::default())),
));
let layout = write(&writer, outer).await?;
insta::assert_snapshot!(layout.display_tree(), @"
vortex.list, dtype: list(list(list(i32))), children: 2
├── elements: vortex.list, dtype: list(list(i32)), children: 2
│ ├── elements: vortex.list, dtype: list(i32), children: 2
│ │ ├── elements: vortex.flat, dtype: i32, segment: 2
│ │ └── offsets: vortex.flat, dtype: u64, segment: 3
│ └── offsets: vortex.flat, dtype: u64, segment: 1
└── offsets: vortex.flat, dtype: u64, segment: 0
");
Ok(())
}
#[tokio::test]
async fn chunked_list_input_with_chunked_strategy_succeeds() -> VortexResult<()> {
let chunk0 = ListArray::try_new(
buffer![1i32, 2, 3].into_array(),
buffer![0u32, 2, 3].into_array(),
Validity::NonNullable,
)
.unwrap()
.into_array();
let chunk1 = ListArray::try_new(
buffer![4i32, 5, 6, 7].into_array(),
buffer![0u32, 1, 4].into_array(),
Validity::NonNullable,
)
.unwrap()
.into_array();
let chunked =
ChunkedArray::try_new(vec![chunk0, chunk1], i32_list_dtype(false))?.into_array();
let layout = write(&ChunkedLayoutStrategy::new(flat_list_strategy()), chunked).await?;
insta::assert_snapshot!(layout.display_tree(), @"
vortex.chunked, dtype: list(i32), children: 2
├── [0]: vortex.list, dtype: list(i32), children: 2
│ ├── elements: vortex.flat, dtype: i32, segment: 0
│ └── offsets: vortex.flat, dtype: u64, segment: 1
└── [1]: vortex.list, dtype: list(i32), children: 2
├── elements: vortex.flat, dtype: i32, segment: 2
└── offsets: vortex.flat, dtype: u64, segment: 3
");
Ok(())
}
}