mod cache;
mod io;
mod split;
#[cfg(test)]
mod tests;
use std::ops::Range;
use std::sync::Arc;
use arrow_array::RecordBatch;
use arrow_schema::ArrowError;
use arrow_schema::Field;
use arrow_schema::SchemaRef;
use futures::Stream;
use futures::StreamExt;
use futures::TryStreamExt;
use futures::stream;
use futures::stream::BoxStream;
use vortex_array::ArrayRef;
use vortex_array::VortexSessionExecute;
use vortex_array::dtype::DType;
use vortex_array::expr::BoundExpression;
use vortex_error::VortexResult;
use vortex_io::request::IoService;
use vortex_io::session::RuntimeSessionExt;
use vortex_scan::planning::planner::WorkScope;
use vortex_scan::selection::Selection;
use vortex_scan::strict_sorted_buffer::StrictSortedBuffer;
use vortex_session::VortexSession;
use vortex_utils::parallelism::get_available_parallelism;
pub use self::cache::ScanCache;
use crate::LayoutRef;
use crate::plan::physical::lower;
use crate::plan::physical::optimize;
use crate::plan::physical::plan_row_idx_expression;
use crate::scan::arrow::to_record_batch;
use crate::scan::execution::ScanPlans;
pub use crate::scan::execution::SegmentLocation;
use crate::scan::execution::plan_selection;
use crate::scan::v2::io::SegmentScanIo;
use crate::scan::v2::io::segment_ranges;
use crate::scan::v2::split::SplitPlan;
use crate::segments::SegmentSource;
const ROW_BLOCK_SIZE: usize = 8192;
const MAX_ROWS_PER_CHUNK: usize = 8 * ROW_BLOCK_SIZE;
const TARGET_BYTES_PER_CHUNK: u64 = 8 * 1024 * 1024;
fn rows_per_chunk(row_count: u64, locations: &[SegmentLocation]) -> usize {
if row_count <= MAX_ROWS_PER_CHUNK as u64 {
return MAX_ROWS_PER_CHUNK;
}
let bytes: u64 = locations
.iter()
.map(|location| u64::from(location.length))
.sum();
let bytes_per_row = bytes.div_ceil(row_count).max(1);
let blocks = TARGET_BYTES_PER_CHUNK / bytes_per_row / ROW_BLOCK_SIZE as u64;
blocks.clamp(1, 8) as usize * ROW_BLOCK_SIZE
}
fn row_chunks(rows: Range<u64>, rows_per_chunk: usize) -> Vec<Range<u64>> {
if rows.is_empty() {
return Vec::new();
}
let first = rows.start / rows_per_chunk as u64 * rows_per_chunk as u64;
(first..rows.end)
.step_by(rows_per_chunk)
.map(|start| {
start.max(rows.start)..start.saturating_add(rows_per_chunk as u64).min(rows.end)
})
.collect()
}
#[derive(Clone)]
pub struct ScanFile {
pub layout: LayoutRef,
pub locations: Arc<[SegmentLocation]>,
pub segments: Arc<dyn SegmentSource>,
pub cache: Option<ScanCache>,
}
pub struct ScanBuilder<A = ArrayRef> {
session: VortexSession,
file: ScanFile,
projection: BoundExpression,
ordered: bool,
row_range: Option<Range<u64>>,
selection: Selection,
concurrency: usize,
map_fn: Arc<dyn Fn(ArrayRef) -> VortexResult<A> + Send + Sync>,
row_offset: u64,
}
impl ScanBuilder<ArrayRef> {
pub fn new(session: VortexSession, file: ScanFile) -> Self {
Self {
projection: BoundExpression::new_root(file.layout.dtype().clone()),
session,
file,
ordered: true,
row_range: None,
selection: Selection::default(),
concurrency: 4,
map_fn: Arc::new(Ok),
row_offset: 0,
}
}
pub fn into_record_batch_stream(
self,
schema: SchemaRef,
) -> VortexResult<impl Stream<Item = Result<RecordBatch, ArrowError>> + Send + 'static> {
let struct_field = Field::new_struct("", schema.fields().clone(), false);
let session = self.session.clone();
Ok(self
.map(move |chunk| {
let mut ctx = session.create_execution_ctx();
to_record_batch(chunk, &struct_field, &mut ctx)
})
.into_stream()?
.map_err(|error| ArrowError::ExternalError(Box::new(error))))
}
}
impl<A: 'static + Send> ScanBuilder<A> {
pub fn dtype(&self) -> &DType {
self.projection.dtype()
}
pub fn with_projection(mut self, projection: BoundExpression) -> Self {
self.projection = projection;
self
}
pub fn with_ordered(mut self, ordered: bool) -> Self {
self.ordered = ordered;
self
}
pub fn with_row_range(mut self, row_range: Range<u64>) -> Self {
self.row_range = Some(row_range);
self
}
pub fn with_selection(mut self, selection: Selection) -> Self {
self.selection = selection;
self
}
pub fn with_row_indices(self, row_indices: StrictSortedBuffer<u64>) -> Self {
self.with_selection(Selection::IncludeByIndex(row_indices))
}
pub fn with_row_offset(mut self, row_offset: u64) -> Self {
self.row_offset = row_offset;
self
}
pub fn with_concurrency(mut self, concurrency: usize) -> Self {
assert!(concurrency > 0);
self.concurrency = concurrency;
self
}
fn map<B: 'static>(
self,
map_fn: impl Fn(A) -> VortexResult<B> + 'static + Send + Sync,
) -> ScanBuilder<B> {
let old_map_fn = self.map_fn;
ScanBuilder {
session: self.session,
file: self.file,
projection: self.projection,
ordered: self.ordered,
row_range: self.row_range,
selection: self.selection,
concurrency: self.concurrency,
row_offset: self.row_offset,
map_fn: Arc::new(move |array| old_map_fn(array).and_then(&map_fn)),
}
}
pub fn into_stream(self) -> VortexResult<BoxStream<'static, VortexResult<A>>> {
let handle = self.session.handle();
Ok(stream::once(async move {
handle
.spawn_cpu(move || PreparedScan::try_new(self)?.execute_stream())
.await
})
.try_flatten()
.boxed())
}
}
struct PreparedScan<A: 'static + Send> {
session: VortexSession,
plans: ScanPlans,
io: Arc<dyn IoService>,
splits: Vec<Range<u64>>,
selection: Selection,
map_fn: Arc<dyn Fn(ArrayRef) -> VortexResult<A> + Send + Sync>,
ordered: bool,
concurrency: usize,
}
impl<A: 'static + Send> PreparedScan<A> {
fn try_new(builder: ScanBuilder<A>) -> VortexResult<Self> {
let file = builder.file;
let cache = file.cache.unwrap_or_default();
let root = match cache.root() {
Some(root) => root,
None => cache.insert_root(lower(&file.layout)?),
};
let row_count = root.row_count();
let identity = builder.projection.is_root() && builder.projection.dtype() == root.dtype();
let projection = if identity {
root
} else {
match cache.projection(&builder.projection) {
Some(plan) => plan,
None => {
let plan =
optimize(plan_row_idx_expression(builder.projection.clone(), root)?)?;
cache.insert_projection(builder.projection, plan)
}
}
};
let chunk_rows = if identity {
rows_per_chunk(row_count, &file.locations)
} else {
MAX_ROWS_PER_CHUNK
};
let splits = row_chunks(builder.row_range.unwrap_or(0..row_count), chunk_rows);
let ranges = match cache.ranges() {
Some(ranges) => ranges,
None => cache.insert_ranges(segment_ranges(&file.locations)),
};
let io = Arc::new(SegmentScanIo::new(file.segments, ranges));
Ok(Self {
session: builder.session.clone(),
plans: ScanPlans {
session: builder.session,
locations: file.locations,
projection,
row_offset: builder.row_offset,
values: cache.values(),
},
io,
splits,
selection: builder.selection,
map_fn: builder.map_fn,
ordered: builder.ordered,
concurrency: builder.concurrency,
})
}
fn split_plans(&self) -> Vec<SplitPlan> {
self.splits
.iter()
.filter_map(|rows| {
let (scope, root) = plan_selection(
&self.plans,
WorkScope {
file_ordinal: 0,
rows: rows.clone(),
},
&self.selection,
)?;
Some(SplitPlan { scope, root })
})
.collect()
}
fn execute_stream(self) -> VortexResult<BoxStream<'static, VortexResult<A>>> {
let concurrency = self.concurrency * get_available_parallelism().unwrap_or(1);
let handle = self.session.handle();
let split_plans = self.split_plans();
let io = self.io;
let map_fn = self.map_fn;
let tasks = stream::iter(split_plans).map(move |split| {
let io = Arc::clone(&io);
let map_fn = Arc::clone(&map_fn);
handle.spawn(async move {
split
.run(io)
.await?
.into_iter()
.map(|array| map_fn(array))
.collect::<VortexResult<Vec<_>>>()
})
});
let batches = if self.ordered {
tasks.buffered(concurrency).boxed()
} else {
tasks.buffer_unordered(concurrency).boxed()
};
Ok(batches
.map_ok(|batches| stream::iter(batches.into_iter().map(Ok)))
.try_flatten()
.boxed())
}
}