Skip to content

Commit 54e5096

Browse files
committed
Shared limit pushdown
Signed-off-by: Adam Gutglick <adam@spiraldb.com>
1 parent 7b432d4 commit 54e5096

4 files changed

Lines changed: 288 additions & 6 deletions

File tree

vortex-datafusion/src/persistent/opener.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -397,9 +397,7 @@ impl FileOpener for VortexOpener {
397397
})
398398
.transpose()?;
399399

400-
if let Some(limit) = limit
401-
&& filter.is_none()
402-
{
400+
if let Some(limit) = limit {
403401
scan_builder = scan_builder.with_limit(limit);
404402
}
405403

@@ -783,6 +781,31 @@ mod tests {
783781
Ok(())
784782
}
785783

784+
#[tokio::test]
785+
async fn test_open_applies_limit_after_filtering() -> anyhow::Result<()> {
786+
let object_store = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
787+
let file_path = "filtered-limit/file.vortex";
788+
let batch = record_batch!((
789+
"a",
790+
Int32,
791+
vec![Some(1), Some(2), Some(3), Some(4), Some(5), Some(6)]
792+
))
793+
.unwrap();
794+
let data_size =
795+
write_arrow_to_vortex(Arc::clone(&object_store), file_path, batch.clone()).await?;
796+
let file = PartitionedFile::new(file_path.to_string(), data_size);
797+
let table_schema = TableSchema::from_file_schema(batch.schema());
798+
let filter = logical2physical(&col("a").gt(lit(0_i32)), table_schema.table_schema());
799+
800+
let mut opener = make_opener(object_store, table_schema, Some(filter));
801+
opener.limit = Some(3);
802+
803+
let batches = opener.open(file)?.await?.try_collect::<Vec<_>>().await?;
804+
assert_eq!(batches.iter().map(RecordBatch::num_rows).sum::<usize>(), 3);
805+
806+
Ok(())
807+
}
808+
786809
#[tokio::test]
787810
async fn test_open_empty_file() -> anyhow::Result<()> {
788811
use futures::TryStreamExt;

vortex-layout/src/scan/layout.rs

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ use vortex_scan::selection::Selection;
4040
use vortex_session::VortexSession;
4141

4242
use crate::LayoutReaderRef;
43+
use crate::scan::limit::SharedRowLimit;
44+
use crate::scan::limit::limit_array_stream_shared;
4345
use crate::scan::scan_builder::ScanBuilder;
4446

4547
/// An implementation of a [`DataSource`] that reads data from a [`LayoutReaderRef`].
@@ -157,13 +159,16 @@ impl DataSource for LayoutReaderDataSource {
157159
}
158160
}
159161

162+
let shared_limit = scan_request.limit.map(SharedRowLimit::new);
163+
160164
Ok(Box::new(LayoutReaderScan {
161165
reader: Arc::clone(&self.reader),
162166
session: self.session.clone(),
163167
dtype,
164168
projection: scan_request.projection,
165169
filter: scan_request.filter,
166170
limit: scan_request.limit,
171+
shared_limit,
167172
selection: scan_request.selection,
168173
ordered: scan_request.ordered,
169174
metrics_registry: self.metrics_registry.clone(),
@@ -185,6 +190,7 @@ struct LayoutReaderScan {
185190
projection: Expression,
186191
filter: Option<Expression>,
187192
limit: Option<u64>,
193+
shared_limit: Option<SharedRowLimit>,
188194
ordered: bool,
189195
selection: Selection,
190196
metrics_registry: Option<Arc<dyn MetricsRegistry>>,
@@ -251,6 +257,7 @@ impl Stream for LayoutReaderScan {
251257
projection: this.projection.clone(),
252258
filter: this.filter.clone(),
253259
limit: split_limit,
260+
shared_limit: this.shared_limit.clone(),
254261
ordered: this.ordered,
255262
row_range,
256263
selection: this.selection.clone(),
@@ -278,6 +285,7 @@ struct LayoutReaderSplit {
278285
projection: Expression,
279286
filter: Option<Expression>,
280287
limit: Option<u64>,
288+
shared_limit: Option<SharedRowLimit>,
281289
ordered: bool,
282290
row_range: Range<u64>,
283291
selection: Selection,
@@ -312,6 +320,7 @@ impl Partition for LayoutReaderSplit {
312320
}
313321

314322
fn execute(self: Box<Self>) -> VortexResult<SendableArrayStream> {
323+
let shared_limit = self.shared_limit.clone();
315324
let builder = ScanBuilder::new(self.session, self.reader)
316325
.with_row_range(self.row_range)
317326
.with_selection(self.selection)
@@ -324,7 +333,7 @@ impl Partition for LayoutReaderSplit {
324333
let dtype = builder.dtype()?;
325334
// Use into_stream() which creates a LazyScanStream that spawns individual I/O
326335
// tasks onto the runtime, enabling parallel execution across executor threads.
327-
let stream = builder.into_stream()?;
336+
let stream = limit_array_stream_shared(builder.into_stream()?, shared_limit);
328337

329338
Ok(ArrayStreamExt::boxed(ArrayStreamAdapter::new(
330339
dtype, stream,
@@ -391,3 +400,143 @@ impl Partition for Empty {
391400
)))
392401
}
393402
}
403+
404+
#[cfg(test)]
405+
mod tests {
406+
use std::ops::Range;
407+
use std::sync::Arc;
408+
409+
use futures::TryStreamExt;
410+
use vortex_array::IntoArray;
411+
use vortex_array::MaskFuture;
412+
use vortex_array::VortexSessionExecute;
413+
use vortex_array::array_session;
414+
use vortex_array::arrays::PrimitiveArray;
415+
use vortex_array::dtype::DType;
416+
use vortex_array::dtype::FieldMask;
417+
use vortex_array::dtype::Nullability;
418+
use vortex_array::dtype::PType;
419+
use vortex_array::expr::Expression;
420+
use vortex_array::expr::root;
421+
use vortex_error::VortexResult;
422+
use vortex_io::runtime::BlockingRuntime;
423+
use vortex_io::runtime::single::SingleThreadRuntime;
424+
use vortex_mask::Mask;
425+
use vortex_scan::DataSource;
426+
use vortex_scan::ScanRequest;
427+
428+
use super::LayoutReaderDataSource;
429+
use crate::ArrayFuture;
430+
use crate::LayoutReader;
431+
use crate::RowSplits;
432+
use crate::SplitRange;
433+
use crate::scan::test::session_with_handle;
434+
435+
#[derive(Debug)]
436+
struct TestLayoutReader {
437+
name: Arc<str>,
438+
dtype: DType,
439+
row_count: u64,
440+
}
441+
442+
impl TestLayoutReader {
443+
fn new(row_count: u64) -> Self {
444+
Self {
445+
name: Arc::from("test"),
446+
dtype: DType::Primitive(PType::I32, Nullability::NonNullable),
447+
row_count,
448+
}
449+
}
450+
}
451+
452+
impl LayoutReader for TestLayoutReader {
453+
fn name(&self) -> &Arc<str> {
454+
&self.name
455+
}
456+
457+
fn as_any(&self) -> &dyn std::any::Any {
458+
self
459+
}
460+
461+
fn dtype(&self) -> &DType {
462+
&self.dtype
463+
}
464+
465+
fn row_count(&self) -> u64 {
466+
self.row_count
467+
}
468+
469+
fn register_splits(
470+
&self,
471+
_field_mask: &[FieldMask],
472+
split_range: &SplitRange,
473+
splits: &mut RowSplits,
474+
) -> VortexResult<()> {
475+
splits.push(split_range.root_row_range().end);
476+
Ok(())
477+
}
478+
479+
fn pruning_evaluation(
480+
&self,
481+
_row_range: &Range<u64>,
482+
_expr: &Expression,
483+
mask: Mask,
484+
) -> VortexResult<MaskFuture> {
485+
Ok(MaskFuture::ready(mask))
486+
}
487+
488+
fn filter_evaluation(
489+
&self,
490+
_row_range: &Range<u64>,
491+
_expr: &Expression,
492+
mask: MaskFuture,
493+
) -> VortexResult<MaskFuture> {
494+
Ok(mask)
495+
}
496+
497+
fn projection_evaluation(
498+
&self,
499+
row_range: &Range<u64>,
500+
_expr: &Expression,
501+
mask: MaskFuture,
502+
) -> VortexResult<ArrayFuture> {
503+
let row_range = row_range.clone();
504+
505+
Ok(Box::pin(async move {
506+
let start = i32::try_from(row_range.start)?;
507+
let end = i32::try_from(row_range.end)?;
508+
PrimitiveArray::from_iter(start..end)
509+
.into_array()
510+
.filter(mask.await?)
511+
}))
512+
}
513+
}
514+
515+
#[test]
516+
fn filtered_limit_is_global_across_scan_partitions() -> VortexResult<()> {
517+
let runtime = SingleThreadRuntime::default();
518+
let session = session_with_handle(runtime.handle());
519+
let source = LayoutReaderDataSource::new(Arc::new(TestLayoutReader::new(6)), session)
520+
.with_split_max_row_count(2);
521+
522+
let scan = runtime.block_on(source.scan(ScanRequest {
523+
filter: Some(root()),
524+
limit: Some(3),
525+
ordered: true,
526+
..Default::default()
527+
}))?;
528+
let partitions = runtime.block_on(scan.partitions().try_collect::<Vec<_>>())?;
529+
530+
let mut ctx = array_session().create_execution_ctx();
531+
let mut values = Vec::new();
532+
for partition in partitions {
533+
for chunk in runtime.block_on_stream(partition.execute()?) {
534+
let primitive = chunk?.execute::<PrimitiveArray>(&mut ctx)?;
535+
values.extend(primitive.into_buffer::<i32>());
536+
}
537+
}
538+
539+
assert_eq!(values, [0, 1, 2]);
540+
Ok(())
541+
}
542+
}

vortex-layout/src/scan/limit.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

44
use std::pin::Pin;
5+
use std::sync::Arc;
6+
use std::sync::atomic::AtomicU64;
7+
use std::sync::atomic::Ordering;
58
use std::task::Context;
69
use std::task::Poll;
710

@@ -25,6 +28,45 @@ where
2528
}
2629
}
2730

31+
/// A row limit shared by streams that execute independent scan partitions.
32+
#[derive(Clone)]
33+
pub(crate) struct SharedRowLimit(Arc<AtomicU64>);
34+
35+
impl SharedRowLimit {
36+
pub(crate) fn new(limit: u64) -> Self {
37+
Self(Arc::new(AtomicU64::new(limit)))
38+
}
39+
40+
fn reserve(&self, requested: u64) -> (u64, bool) {
41+
let mut remaining = self.0.load(Ordering::Relaxed);
42+
loop {
43+
let reserved = remaining.min(requested);
44+
match self.0.compare_exchange_weak(
45+
remaining,
46+
remaining - reserved,
47+
Ordering::Relaxed,
48+
Ordering::Relaxed,
49+
) {
50+
Ok(_) => return (reserved, reserved == remaining),
51+
Err(actual) => remaining = actual,
52+
}
53+
}
54+
}
55+
}
56+
57+
pub(crate) fn limit_array_stream_shared<S>(
58+
stream: S,
59+
limit: Option<SharedRowLimit>,
60+
) -> BoxStream<'static, VortexResult<ArrayRef>>
61+
where
62+
S: Stream<Item = VortexResult<ArrayRef>> + Send + 'static,
63+
{
64+
match limit {
65+
Some(limit) => SharedRowLimitedStream::new(stream.boxed(), limit).boxed(),
66+
None => stream.boxed(),
67+
}
68+
}
69+
2870
struct RowLimitedStream {
2971
inner: BoxStream<'static, VortexResult<ArrayRef>>,
3072
remaining: u64,
@@ -72,3 +114,54 @@ impl Stream for RowLimitedStream {
72114
}
73115
}
74116
}
117+
118+
struct SharedRowLimitedStream {
119+
inner: BoxStream<'static, VortexResult<ArrayRef>>,
120+
limit: SharedRowLimit,
121+
}
122+
123+
impl SharedRowLimitedStream {
124+
fn new(inner: BoxStream<'static, VortexResult<ArrayRef>>, limit: SharedRowLimit) -> Self {
125+
Self { inner, limit }
126+
}
127+
128+
fn abort_pending(&mut self) {
129+
let inner = std::mem::replace(&mut self.inner, stream::empty().boxed());
130+
drop(inner);
131+
}
132+
}
133+
134+
impl Stream for SharedRowLimitedStream {
135+
type Item = VortexResult<ArrayRef>;
136+
137+
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
138+
match self.inner.as_mut().poll_next(cx) {
139+
Poll::Ready(Some(Ok(chunk))) => {
140+
let chunk_len = chunk.len() as u64;
141+
let (reserved, exhausted) = self.limit.reserve(chunk_len);
142+
143+
if exhausted {
144+
self.abort_pending();
145+
}
146+
147+
if reserved == 0 {
148+
if exhausted {
149+
Poll::Ready(None)
150+
} else {
151+
Poll::Ready(Some(Ok(chunk)))
152+
}
153+
} else if reserved == chunk_len {
154+
Poll::Ready(Some(Ok(chunk)))
155+
} else {
156+
let limit = match usize::try_from(reserved) {
157+
Ok(limit) => limit,
158+
Err(_) => unreachable!("reserved rows cannot exceed the current chunk"),
159+
};
160+
self.abort_pending();
161+
Poll::Ready(Some(chunk.slice(0..limit)))
162+
}
163+
}
164+
other => other,
165+
}
166+
}
167+
}

0 commit comments

Comments
 (0)