use std::num::NonZeroUsize;
use std::ops::Range;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use arrow_array::cast::AsArray;
use arrow_array::types::Int32Type;
use futures::TryStreamExt;
use futures::stream;
use rstest::rstest;
use vortex_array::ArrayContext;
use vortex_array::ArrayRef;
use vortex_array::IntoArray;
use vortex_array::VortexSessionExecute;
use vortex_array::array_session;
use vortex_array::arrays::ChunkedArray;
use vortex_array::arrays::ListArray;
use vortex_array::arrays::PrimitiveArray;
use vortex_array::arrays::StructArray;
use vortex_array::arrays::VarBinArray;
use vortex_array::assert_arrays_eq;
use vortex_array::dtype::DType;
use vortex_array::expr::get_item;
use vortex_array::expr::gt;
use vortex_array::expr::lit;
use vortex_array::expr::pack;
use vortex_array::expr::root;
use vortex_array::validity::Validity;
use vortex_arrow::ArrowSessionExt;
use vortex_buffer::Alignment;
use vortex_buffer::Buffer;
use vortex_error::VortexResult;
use vortex_io::session::RuntimeSessionExt;
use vortex_scan::strict_sorted_buffer::StrictSortedBuffer;
use vortex_session::VortexSession;
use super::MAX_ROWS_PER_CHUNK;
use super::PreparedScan;
use super::ScanBuilder;
use super::ScanCache;
use super::ScanFile;
use super::row_chunks;
use super::rows_per_chunk;
use crate::LayoutRef;
use crate::LayoutStrategy;
use crate::layouts::chunked::writer::ChunkedLayoutStrategy;
use crate::layouts::dict::DictLayout;
use crate::layouts::flat::writer::FlatLayoutStrategy;
use crate::layouts::row_idx::row_idx;
use crate::layouts::table::TableStrategy;
use crate::layouts::zoned::writer::ZonedLayoutOptions;
use crate::layouts::zoned::writer::ZonedStrategy;
use crate::scan::execution::SegmentLocation;
use crate::scan::scan_builder::ScanBuilder as V1ScanBuilder;
use crate::segments::SegmentFuture;
use crate::segments::SegmentId;
use crate::segments::SegmentSource;
use crate::segments::TestSegments;
use crate::sequence::SequenceId;
use crate::sequence::SequentialStreamAdapter;
use crate::sequence::SequentialStreamExt as _;
use crate::test::new_session;
const CHUNK_ROWS: usize = 1000;
const CHUNKS: usize = 4;
fn table_chunk(start: usize) -> VortexResult<ArrayRef> {
let mut ctx = array_session().create_execution_ctx();
let rows = start..start + CHUNK_ROWS;
let ints = Buffer::from_iter(
rows.clone()
.map(|row| i32::try_from(row).unwrap_or(i32::MAX)),
);
let strings = VarBinArray::from_iter(
rows.map(|row| (row % 3 != 0).then(|| format!("row {row}"))),
DType::Utf8(true.into()),
);
let elements = Buffer::from_iter((0..2 * CHUNK_ROWS).map(|i| i64::try_from(i).unwrap_or(0)));
let offsets = Buffer::from_iter((0..=CHUNK_ROWS).map(|i| u32::try_from(2 * i).unwrap_or(0)));
let lists = ListArray::try_new(
elements.into_array(),
offsets.into_array(),
Validity::NonNullable,
&mut ctx,
)?;
Ok(StructArray::from_fields(&[
("ints", ints.into_array()),
("strings", strings.into_array()),
("lists", lists.into_array()),
])?
.into_array())
}
async fn write(
session: &VortexSession,
strategy: &dyn LayoutStrategy,
) -> VortexResult<(Arc<dyn SegmentSource>, LayoutRef)> {
let segments = Arc::new(TestSegments::default());
let (mut sequence_id, eof) = SequenceId::root().split();
let chunks = (0..CHUNKS)
.map(|chunk| Ok((sequence_id.advance(), table_chunk(chunk * CHUNK_ROWS)?)))
.collect::<Vec<_>>();
let dtype = table_chunk(0)?.dtype().clone();
let layout = strategy
.write_stream(
ArrayContext::empty().into(),
Arc::<TestSegments>::clone(&segments),
SequentialStreamAdapter::new(dtype, stream::iter(chunks)).sendable(),
eof,
session,
)
.await?;
Ok((segments, layout))
}
fn chunked_flat() -> Arc<dyn LayoutStrategy> {
Arc::new(ChunkedLayoutStrategy::new(FlatLayoutStrategy::default()))
}
fn table() -> Arc<dyn LayoutStrategy> {
Arc::new(TableStrategy::new(chunked_flat(), chunked_flat()))
}
fn zoned_table() -> Arc<dyn LayoutStrategy> {
let zoned = ZonedStrategy::new(
ChunkedLayoutStrategy::new(FlatLayoutStrategy::default()),
FlatLayoutStrategy::default(),
ZonedLayoutOptions {
block_size: NonZeroUsize::new(CHUNK_ROWS).unwrap_or(NonZeroUsize::MIN),
..Default::default()
},
);
Arc::new(TableStrategy::new(chunked_flat(), Arc::new(zoned)))
}
fn scan_file(segments: &Arc<dyn SegmentSource>, layout: &LayoutRef) -> VortexResult<ScanFile> {
let mut count = 0;
for layout in layout.depth_first_traversal() {
for id in layout?.segment_ids() {
count = count.max(*id as usize + 1);
}
}
let locations = (0..count as u64)
.map(|offset| SegmentLocation {
offset,
length: 0,
alignment: Alignment::none(),
})
.collect();
Ok(ScanFile {
layout: Arc::clone(layout),
locations,
segments: Arc::clone(segments),
cache: None,
})
}
fn builder(
session: &VortexSession,
segments: &Arc<dyn SegmentSource>,
layout: &LayoutRef,
) -> VortexResult<ScanBuilder<ArrayRef>> {
Ok(ScanBuilder::new(
session.clone(),
scan_file(segments, layout)?,
))
}
async fn collect(
dtype: DType,
stream: impl futures::Stream<Item = VortexResult<ArrayRef>>,
) -> VortexResult<ArrayRef> {
let chunks: Vec<ArrayRef> = stream.try_collect().await?;
Ok(ChunkedArray::try_new(chunks, dtype)?.into_array())
}
async fn assert_same_as_v1(
session: &VortexSession,
make: impl Fn() -> VortexResult<ScanBuilder<ArrayRef>>,
) -> VortexResult<()> {
let builder = make()?;
let dtype = builder.dtype().clone();
let reader = builder.file.layout.new_reader(
"".into(),
Arc::clone(&builder.file.segments),
session,
&Default::default(),
)?;
let mut v1 = V1ScanBuilder::new(session.clone(), reader)
.with_projection(builder.projection.clone())
.with_selection(builder.selection.clone())
.with_row_offset(builder.row_offset)
.with_ordered(builder.ordered)
.with_concurrency(builder.concurrency);
if let Some(rows) = builder.row_range.clone() {
v1 = v1.with_row_range(rows);
}
let expected = collect(dtype.clone(), v1.into_stream()?).await?;
let actual = collect(dtype, builder.into_stream()?).await?;
assert_arrays_eq!(actual, expected, &mut session.create_execution_ctx());
Ok(())
}
#[derive(Clone, Copy, Debug)]
enum Strategy {
Flat,
Table,
ZonedTable,
}
impl Strategy {
fn get(self) -> Arc<dyn LayoutStrategy> {
match self {
Strategy::Flat => chunked_flat(),
Strategy::Table => table(),
Strategy::ZonedTable => zoned_table(),
}
}
}
#[rstest]
#[case::whole(None, None)]
#[case::row_range(Some(250..3_100), None)]
#[case::empty_row_range(Some(10..10), None)]
#[case::row_indices(None, Some(&[0, 1, 999, 1000, 2500, 3999][..]))]
#[case::row_range_and_indices(Some(900..2600), Some(&[0, 999, 1000, 2500, 3999][..]))]
#[tokio::test]
async fn matches_v1(
#[values(Strategy::Flat, Strategy::Table, Strategy::ZonedTable)] strategy: Strategy,
#[case] row_range: Option<Range<u64>>,
#[case] rows: Option<&'static [u64]>,
) -> VortexResult<()> {
let session = new_session().with_tokio();
let (segments, layout) = write(&session, strategy.get().as_ref()).await?;
assert_same_as_v1(&session, || {
let mut builder = builder(&session, &segments, &layout)?;
if let Some(row_range) = row_range.clone() {
builder = builder.with_row_range(row_range);
}
if let Some(rows) = rows {
builder =
builder.with_row_indices(StrictSortedBuffer::try_new(Buffer::copy_from(rows))?);
}
Ok(builder)
})
.await
}
#[rstest]
#[tokio::test]
async fn projection_matches_v1(
#[values(Strategy::Flat, Strategy::Table, Strategy::ZonedTable)] strategy: Strategy,
) -> VortexResult<()> {
let session = new_session().with_tokio();
let (segments, layout) = write(&session, strategy.get().as_ref()).await?;
let dtype = layout.dtype().clone();
assert_same_as_v1(&session, || {
let projection = pack(
[
("strings", get_item("strings", root())),
("idx", row_idx()),
("big", gt(get_item("ints", root()), lit(1500i32))),
],
false.into(),
);
Ok(builder(&session, &segments, &layout)?
.with_projection(projection.bind(&dtype)?)
.with_row_offset(100))
})
.await
}
#[tokio::test]
async fn arrow_stream_projects_across_chunks() -> VortexResult<()> {
let session = new_session().with_tokio();
let (segments, layout) = write(&session, table().as_ref()).await?;
let projection =
pack([("value", get_item("ints", root()))], false.into()).bind(layout.dtype())?;
let scan = builder(&session, &segments, &layout)?
.with_projection(projection)
.with_row_range(990..1010);
let schema = Arc::new(session.arrow().to_arrow_schema(scan.dtype())?);
let batches: Vec<_> = scan.into_record_batch_stream(schema)?.try_collect().await?;
let values: Vec<_> = batches
.iter()
.flat_map(|batch| {
batch
.column(0)
.as_primitive::<Int32Type>()
.values()
.iter()
.copied()
})
.collect();
assert_eq!(values, (990_i32..1010).collect::<Vec<_>>());
Ok(())
}
#[rstest]
#[case::whole(0..150_000, vec![0..65_536, 65_536..131_072, 131_072..150_000])]
#[case::unaligned(10..140_000, vec![10..65_536, 65_536..131_072, 131_072..140_000])]
#[case::boundary(65_536..131_072, vec![65_536..131_072])]
#[case::empty(5..5, vec![])]
#[case::near_max(u64::MAX - 3..u64::MAX, vec![u64::MAX - 3..u64::MAX])]
fn fixed_row_chunks(#[case] rows: Range<u64>, #[case] expected: Vec<Range<u64>>) {
assert_eq!(row_chunks(rows, 65_536), expected);
}
#[rstest]
#[case::empty(0, 0, 65_536)]
#[case::single_chunk(1000, 100_000_000, 65_536)]
#[case::no_bytes(100_000, 0, 65_536)]
#[case::small_rows(1_000_000, 64_000_000, 65_536)]
#[case::large_rows(1_000_000, 384_000_000, 16_384)]
#[case::minimum(1_000_000, 2_000_000_000, 8192)]
fn chunk_byte_budget(#[case] rows: u64, #[case] bytes: u32, #[case] expected: usize) {
let locations = [SegmentLocation {
offset: 0,
length: bytes,
alignment: Alignment::none(),
}];
assert_eq!(rows_per_chunk(rows, &locations), expected);
}
#[test]
fn smaller_chunks_keep_file_alignment() {
assert_eq!(
row_chunks(7500..33_000, 16_384),
[7500..16_384, 16_384..32_768, 32_768..33_000]
);
}
struct CountingSegments {
inner: Arc<dyn SegmentSource>,
reads: AtomicUsize,
}
impl SegmentSource for CountingSegments {
fn request(&self, id: SegmentId) -> SegmentFuture {
self.reads.fetch_add(1, Ordering::Relaxed);
self.inner.request(id)
}
}
#[tokio::test]
async fn dictionary_values_are_reused_between_splits() -> VortexResult<()> {
let session = new_session().with_tokio();
const STORED_CHUNK_ROWS: usize = 70_000;
const STORED_CHUNKS: usize = 3;
let segments = Arc::new(TestSegments::default());
let (mut sequence, eof) = SequenceId::root().split();
let arrays = (0..STORED_CHUNKS)
.map(|index| {
let start = i32::try_from(index * STORED_CHUNK_ROWS)?;
let end = start + i32::try_from(STORED_CHUNK_ROWS)?;
Ok((
sequence.advance(),
PrimitiveArray::from_iter(start..end).into_array(),
))
})
.collect::<VortexResult<Vec<_>>>()?;
let dtype = DType::Primitive(vortex_array::dtype::PType::I32, false.into());
let ints = chunked_flat()
.write_stream(
ArrayContext::empty().into(),
Arc::<TestSegments>::clone(&segments),
SequentialStreamAdapter::new(dtype, stream::iter(arrays.into_iter().map(Ok)))
.sendable(),
eof,
&session,
)
.await?;
let layout = DictLayout::new(Arc::clone(&ints), ints).into_layout();
let counted = Arc::new(CountingSegments {
inner: segments,
reads: AtomicUsize::new(0),
});
let segments = Arc::clone(&counted) as Arc<dyn SegmentSource>;
let scan = PreparedScan::try_new(builder(&session, &segments, &layout)?)?;
let splits = scan.split_plans();
assert_eq!(
splits.len(),
(STORED_CHUNKS * STORED_CHUNK_ROWS).div_ceil(MAX_ROWS_PER_CHUNK)
);
for (index, split) in splits.into_iter().enumerate() {
let rows = split.scope.rows.clone();
let before = counted.reads.load(Ordering::Relaxed);
let arrays = split.run(Arc::clone(&scan.io)).await?;
let reads = counted.reads.load(Ordering::Relaxed) - before;
let code_chunks =
(rows.end - 1) / STORED_CHUNK_ROWS as u64 - rows.start / STORED_CHUNK_ROWS as u64 + 1;
assert_eq!(
reads,
if index == 0 {
STORED_CHUNKS
} else {
usize::try_from(code_chunks)?
}
);
let expected =
PrimitiveArray::from_iter(i32::try_from(rows.start)?..i32::try_from(rows.end)?);
assert_arrays_eq!(
ChunkedArray::try_new(arrays, layout.dtype().clone())?.into_array(),
expected,
&mut session.create_execution_ctx()
);
}
Ok(())
}
#[tokio::test]
async fn list_layouts_return_an_error() -> VortexResult<()> {
let session = new_session().with_tokio();
let strategy = TableStrategy::new(chunked_flat(), chunked_flat()).with_list_layout();
let (segments, layout) = write(&session, &strategy).await?;
let builder = builder(&session, &segments, &layout)?;
let error = builder
.into_stream()?
.try_next()
.await
.expect_err("V2 must reject an unsupported layout");
assert!(error.to_string().contains("vortex.plan.list_pack"));
Ok(())
}
#[tokio::test]
async fn unordered_returns_every_row() -> VortexResult<()> {
let session = new_session().with_tokio();
let (segments, layout) = write(&session, table().as_ref()).await?;
let scan = PreparedScan::try_new(builder(&session, &segments, &layout)?.with_ordered(false))?;
let chunks: Vec<ArrayRef> = scan.execute_stream()?.try_collect().await?;
let rows: usize = chunks.iter().map(|chunk| chunk.len()).sum();
assert_eq!(rows, CHUNKS * CHUNK_ROWS);
Ok(())
}
#[tokio::test]
async fn primitive_column() -> VortexResult<()> {
let session = new_session().with_tokio();
let (segments, layout) = write(&session, table().as_ref()).await?;
let dtype = layout.dtype().clone();
let scan = PreparedScan::try_new(
builder(&session, &segments, &layout)?
.with_projection(get_item("ints", root()).bind(&dtype)?)
.with_row_range(990..1010),
)?;
let actual = collect(
DType::Primitive(vortex_array::dtype::PType::I32, false.into()),
scan.execute_stream()?,
)
.await?;
assert_arrays_eq!(
actual,
PrimitiveArray::from_iter(990i32..1010),
&mut session.create_execution_ctx()
);
Ok(())
}
#[rstest]
#[tokio::test]
async fn work_chunks_ignore_layout_boundaries(
#[values(Strategy::Flat, Strategy::Table, Strategy::ZonedTable)] strategy: Strategy,
) -> VortexResult<()> {
let session = new_session().with_tokio();
let (segments, layout) = write(&session, strategy.get().as_ref()).await?;
let scan = PreparedScan::try_new(builder(&session, &segments, &layout)?)?;
assert_eq!(scan.splits, vec![0..(CHUNKS * CHUNK_ROWS) as u64]);
let arrays: Vec<_> = scan.execute_stream()?.try_collect().await?;
assert_eq!(arrays.len(), 1);
assert_eq!(arrays[0].len(), CHUNKS * CHUNK_ROWS);
Ok(())
}
async fn chunked_ints(
session: &VortexSession,
segments: &Arc<TestSegments>,
) -> VortexResult<LayoutRef> {
let (mut sequence, eof) = SequenceId::root().split();
let arrays = (0..3i32)
.map(|index| {
Ok((
sequence.advance(),
PrimitiveArray::from_iter(index * 10..index * 10 + 10).into_array(),
))
})
.collect::<Vec<_>>();
let dtype = DType::Primitive(vortex_array::dtype::PType::I32, false.into());
chunked_flat()
.write_stream(
ArrayContext::empty().into(),
Arc::<TestSegments>::clone(segments),
SequentialStreamAdapter::new(dtype, stream::iter(arrays)).sendable(),
eof,
session,
)
.await
}
#[rstest]
#[case::cached(true, 1)]
#[case::uncached(false, 4)]
#[tokio::test]
async fn scan_cache_reuses_values_between_scans(
#[case] cached: bool,
#[case] second_scan_reads: usize,
) -> VortexResult<()> {
let session = new_session().with_tokio();
let segments = Arc::new(TestSegments::default());
let values = chunked_ints(&session, &segments).await?;
let codes = chunked_ints(&session, &segments).await?;
let layout = DictLayout::new(values, codes).into_layout();
let counted = Arc::new(CountingSegments {
inner: segments,
reads: AtomicUsize::new(0),
});
let segments = Arc::clone(&counted) as Arc<dyn SegmentSource>;
let mut file = scan_file(&segments, &layout)?;
file.cache = cached.then(ScanCache::default);
let mut reads = Vec::new();
for _ in 0..2 {
let before = counted.reads.load(Ordering::Relaxed);
let scan = ScanBuilder::new(session.clone(), file.clone())
.with_row_indices(StrictSortedBuffer::try_new(Buffer::from(vec![12u64, 15]))?);
let actual = collect(layout.dtype().clone(), scan.into_stream()?).await?;
assert_arrays_eq!(
actual,
PrimitiveArray::from_iter([12i32, 15]),
&mut session.create_execution_ctx()
);
reads.push(counted.reads.load(Ordering::Relaxed) - before);
}
assert_eq!(reads, vec![4, second_scan_reads]);
Ok(())
}
#[tokio::test]
async fn selected_split_scope_is_bounded_before_projection() -> VortexResult<()> {
let session = new_session().with_tokio();
let (segments, layout) = write(&session, table().as_ref()).await?;
let scan = PreparedScan::try_new(
builder(&session, &segments, &layout)?
.with_row_indices(StrictSortedBuffer::try_new(Buffer::from(vec![7, 2001]))?),
)?;
let splits = scan.split_plans();
assert_eq!(splits.len(), 1);
assert_eq!(splits[0].scope.rows, 7..2002);
Ok(())
}
#[tokio::test]
async fn repeated_projections_reuse_their_values() -> VortexResult<()> {
let session = new_session().with_tokio();
let segments = Arc::new(TestSegments::default());
let values = chunked_ints(&session, &segments).await?;
let codes = chunked_ints(&session, &segments).await?;
let layout = DictLayout::new(values, codes).into_layout();
let counted = Arc::new(CountingSegments {
inner: segments,
reads: AtomicUsize::new(0),
});
let segments = Arc::clone(&counted) as Arc<dyn SegmentSource>;
let mut file = scan_file(&segments, &layout)?;
file.cache = Some(ScanCache::default());
let projection = gt(root(), lit(15i32)).bind(layout.dtype())?;
let mut reads = Vec::new();
for _ in 0..3 {
let before = counted.reads.load(Ordering::Relaxed);
let scan = ScanBuilder::new(session.clone(), file.clone())
.with_projection(projection.clone())
.with_row_indices(StrictSortedBuffer::try_new(Buffer::from(vec![12u64, 18]))?);
let actual = collect(projection.dtype().clone(), scan.into_stream()?).await?;
assert_arrays_eq!(
actual,
vortex_array::arrays::BoolArray::from_iter([false, true]),
&mut session.create_execution_ctx()
);
reads.push(counted.reads.load(Ordering::Relaxed) - before);
}
assert_eq!(reads, vec![4, 1, 1]);
Ok(())
}