Skip to content

Commit fba0962

Browse files
joseph-isaacsclaude
andcommitted
Add plan-native scan execution
Give `PlanVTable` an `execute` hook taking a row range and a selection mask, and implement it for every operator: `SegmentScan` reads and decodes its segment, the structural operators combine their children, and `Eval` applies the expression to its child's output. Add `vortex-scan-v2`, which copies the existing scan orchestration around this API so the `LayoutReader` scanner is untouched while the plan-native path is developed. `Take` now records whether every dictionary value is referenced by some code. That fact previously came from the dict layout; since operators no longer hold a layout, lowering carries it into the operator's data. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012obBhJ8oPZoBbKyeS79yMv
1 parent 24bf2c8 commit fba0962

29 files changed

Lines changed: 1949 additions & 10 deletions

Cargo.lock

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ members = [
2323
"vortex-btrblocks",
2424
"vortex-layout",
2525
"vortex-scan",
26+
"vortex-scan-v2",
2627
"vortex-file",
2728
"vortex-ipc",
2829
"vortex",
@@ -322,6 +323,7 @@ vortex-proto = { version = "0.1.0", path = "./vortex-proto", default-features =
322323
vortex-row = { version = "0.1.0", path = "./vortex-row", default-features = false }
323324
vortex-runend = { version = "0.1.0", path = "./encodings/runend", default-features = false }
324325
vortex-scan = { version = "0.1.0", path = "./vortex-scan", default-features = false }
326+
vortex-scan-v2 = { version = "0.1.0", path = "./vortex-scan-v2", default-features = false }
325327
vortex-sequence = { version = "0.1.0", path = "encodings/sequence", default-features = false }
326328
vortex-session = { version = "0.1.0", path = "./vortex-session", default-features = false }
327329
vortex-sparse = { version = "0.1.0", path = "./encodings/sparse", default-features = false }

docs/developer-guide/internals/scan-planning.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,17 @@ holding the common fields — dtype, row count, and children. Operator-specific
5757
already serialize their metadata; the ones holding a read context or a bound expression return
5858
`None` until those codecs exist.
5959

60+
## Execution
61+
62+
Each operator executes over a row range and selection mask. `SegmentScan` reads its segment,
63+
structural operators combine their children, and `Eval` applies the remaining derived work.
64+
`vortex-scan-v2` copies the existing scan orchestration around this API, so the original
65+
`LayoutReader` scanner is untouched while the plan-native path is developed.
66+
6067
## Future work
6168

62-
Plans currently stop at construction and optimization. Still to come: a plan registry and foreign
63-
operator placeholder so third-party operators survive a round trip, a serialization envelope, and
64-
an execution stage that walks an optimized plan, reads the referenced segments, and returns the
65-
query result.
69+
Still to come: a plan registry and foreign operator placeholder so third-party operators survive
70+
a round trip, and a serialization envelope.
6671

6772
Lowering does not yet take a projection or row range, so it lowers the whole layout tree. Once it
6873
does, an unsupported layout in a column the query never reads will no longer fail the scan.

vortex-layout/src/layouts/row_idx/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ fn row_idx_dtype() -> DType {
297297
}
298298

299299
// Returns a SequenceArray representing the row indices for the given row range,
300-
fn idx_array(row_offset: u64, row_range: &Range<u64>) -> SequenceArray {
300+
pub(crate) fn idx_array(row_offset: u64, row_range: &Range<u64>) -> SequenceArray {
301301
Sequence::try_new(
302302
PValue::U64(row_offset + row_range.start),
303303
PValue::U64(1),
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use std::sync::Arc;
5+
6+
use futures::future::BoxFuture;
7+
use vortex_array::ArrayRef;
8+
use vortex_error::VortexResult;
9+
use vortex_session::VortexSession;
10+
11+
use crate::segments::SegmentSource;
12+
13+
/// Future resolving to the array produced by a physical plan.
14+
pub type PlanArrayFuture = BoxFuture<'static, VortexResult<ArrayRef>>;
15+
16+
/// Runtime dependencies shared by every node in a plan execution.
17+
#[derive(Clone)]
18+
pub struct PlanExecutionContext {
19+
segment_source: Arc<dyn SegmentSource>,
20+
session: VortexSession,
21+
}
22+
23+
impl PlanExecutionContext {
24+
/// Creates an execution context over a segment source and Vortex session.
25+
pub fn new(segment_source: Arc<dyn SegmentSource>, session: VortexSession) -> Self {
26+
Self {
27+
segment_source,
28+
session,
29+
}
30+
}
31+
32+
/// Returns the segment source used to satisfy leaf reads.
33+
pub fn segment_source(&self) -> &Arc<dyn SegmentSource> {
34+
&self.segment_source
35+
}
36+
37+
/// Returns the Vortex session used for array decoding and expression execution.
38+
pub fn session(&self) -> &VortexSession {
39+
&self.session
40+
}
41+
}

vortex-layout/src/plan/lower.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,11 @@ fn lower_dict(layout: &DictLayout) -> VortexResult<TakePlan> {
123123
.slot(0)?
124124
.ok_or_else(|| vortex_err!("Dictionary values child is absent"))?,
125125
)?;
126-
Ok(TakePlan::new(codes, values))
126+
Ok(TakePlan::new_with_all_values_referenced(
127+
codes,
128+
values,
129+
layout.has_all_values_referenced(),
130+
))
127131
}
128132

129133
fn lower_list(layout: &ListLayout) -> VortexResult<ListPackPlan> {

vortex-layout/src/plan/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
//! can reason about a plan's shape alone. [`lower`] is the one-way bridge from a stored layout.
99
1010
mod display;
11+
mod execution;
1112
mod lower;
1213
mod optimize;
1314
pub mod optimizer;
@@ -22,6 +23,8 @@ pub use display::PlanSummaryExtractor;
2223
pub use display::PlanTreeContext;
2324
pub use display::PlanTreeDisplay;
2425
pub use display::PlanTreeExtractor;
26+
pub use execution::PlanArrayFuture;
27+
pub use execution::PlanExecutionContext;
2528
pub use lower::lower;
2629
pub use optimize::optimize;
2730
pub use plans::Concat;
@@ -50,6 +53,7 @@ pub use plans::SegmentScan;
5053
pub use plans::SegmentScanData;
5154
pub use plans::SegmentScanPlan;
5255
pub use plans::Take;
56+
pub use plans::TakeData;
5357
pub use plans::TakePlan;
5458
pub use plans::Zoned;
5559
pub use plans::ZonedPlan;

vortex-layout/src/plan/plans/concat.rs

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

44
use std::borrow::Cow;
5+
use std::future;
6+
use std::ops::Range;
57
use std::sync::Arc;
68

9+
use futures::FutureExt;
10+
use futures::TryStreamExt;
11+
use futures::stream::FuturesOrdered;
12+
use vortex_array::Canonical;
713
use vortex_array::EmptyMetadata;
14+
use vortex_array::IntoArray;
15+
use vortex_array::MaskFuture;
16+
use vortex_array::arrays::ChunkedArray;
817
use vortex_array::dtype::DType;
918
use vortex_array::expr::ExactBoundExpr;
1019
use vortex_array::expr::label_bound_tree;
20+
use vortex_error::VortexExpect;
1121
use vortex_error::VortexResult;
1222
use vortex_error::vortex_bail;
23+
use vortex_error::vortex_ensure;
24+
use vortex_error::vortex_err;
1325
use vortex_session::registry::CachedId;
1426

1527
use crate::layouts::row_idx::RowIdx as RowIdxFn;
1628
use crate::plan::Eval;
1729
use crate::plan::EvalPlan;
1830
use crate::plan::Plan;
31+
use crate::plan::PlanArrayFuture;
32+
use crate::plan::PlanExecutionContext;
1933
use crate::plan::PlanId;
2034
use crate::plan::PlanParts;
2135
use crate::plan::PlanRef;
@@ -95,6 +109,56 @@ impl PlanVTable for Concat {
95109
ConcatPlan::try_new(plan.dtype().clone(), children)
96110
}
97111

112+
fn execute(
113+
plan: &Plan<Self>,
114+
ctx: &PlanExecutionContext,
115+
row_range: &Range<u64>,
116+
mask: MaskFuture,
117+
) -> VortexResult<PlanArrayFuture> {
118+
vortex_ensure!(
119+
row_range.start <= row_range.end && row_range.end <= plan.row_count(),
120+
"Concat row range {:?} is outside 0..{}",
121+
row_range,
122+
plan.row_count()
123+
);
124+
vortex_ensure!(
125+
mask.len() == usize::try_from(row_range.end - row_range.start)?,
126+
"Concat mask length mismatch"
127+
);
128+
if row_range.is_empty() {
129+
let empty = Canonical::empty(plan.dtype()).into_array();
130+
return Ok(future::ready(Ok(empty)).boxed());
131+
}
132+
133+
let mut chunk_futures = Vec::new();
134+
for (chunk, &chunk_offset) in plan.children().iter().zip(plan.row_offsets()) {
135+
let chunk_end = chunk_offset
136+
.checked_add(chunk.row_count())
137+
.ok_or_else(|| vortex_err!("Chunk row offset overflow"))?;
138+
let start = row_range.start.max(chunk_offset);
139+
let end = row_range.end.min(chunk_end);
140+
if start < end {
141+
let child_range = start - chunk_offset..end - chunk_offset;
142+
let mask_range = usize::try_from(start - row_range.start)?
143+
..usize::try_from(end - row_range.start)?;
144+
chunk_futures.push(chunk.execute(ctx, &child_range, mask.slice(mask_range))?);
145+
}
146+
}
147+
148+
Ok(async move {
149+
let chunks: Vec<_> = FuturesOrdered::from_iter(chunk_futures)
150+
.try_collect()
151+
.await?;
152+
vortex_ensure!(!chunks.is_empty(), "Non-empty row range selected no chunks");
153+
if chunks.len() == 1 {
154+
return Ok(chunks.into_iter().next().vortex_expect("one chunk"));
155+
}
156+
let dtype = chunks[0].dtype().clone();
157+
Ok(ChunkedArray::try_new(chunks, dtype)?.into_array())
158+
}
159+
.boxed())
160+
}
161+
98162
fn child_name(_plan: &Plan<Self>, index: usize) -> Cow<'_, str> {
99163
Cow::Owned(format!("chunks[{index}]"))
100164
}

vortex-layout/src/plan/plans/eval.rs

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

44
use std::borrow::Cow;
5+
use std::ops::Range;
56

7+
use futures::FutureExt;
68
use vortex_array::EmptyMetadata;
9+
use vortex_array::MaskFuture;
710
use vortex_array::dtype::DType;
811
use vortex_array::dtype::FieldName;
912
use vortex_array::expr::BoundExpression;
@@ -16,6 +19,8 @@ use vortex_error::VortexResult;
1619
use vortex_session::registry::CachedId;
1720

1821
use crate::plan::Plan;
22+
use crate::plan::PlanArrayFuture;
23+
use crate::plan::PlanExecutionContext;
1924
use crate::plan::PlanId;
2025
use crate::plan::PlanParts;
2126
use crate::plan::PlanRef;
@@ -83,6 +88,17 @@ impl PlanVTable for Eval {
8388
))
8489
}
8590

91+
fn execute(
92+
plan: &Plan<Self>,
93+
ctx: &PlanExecutionContext,
94+
row_range: &Range<u64>,
95+
mask: MaskFuture,
96+
) -> VortexResult<PlanArrayFuture> {
97+
let child = plan.child_plan().execute(ctx, row_range, mask)?;
98+
let expression = plan.expression().clone();
99+
Ok(async move { child.await?.apply_bound(&expression) }.boxed())
100+
}
101+
86102
fn child_name(_plan: &Plan<Self>, index: usize) -> Cow<'_, str> {
87103
if index == 0 {
88104
Cow::Borrowed("child")

0 commit comments

Comments
 (0)