Skip to content

Commit 00c5f8f

Browse files
committed
Add layout scan physical plan model
Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
1 parent c4bb934 commit 00c5f8f

19 files changed

Lines changed: 2335 additions & 0 deletions

File tree

docs/developer-guide/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ internals/session
2323
internals/async-runtime
2424
internals/vtables
2525
internals/execution
26+
internals/scan-planning
2627
internals/stats-pruning
2728
internals/io
2829
internals/serialization
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Scan Plans
2+
3+
A scan plan is the physical plan for satisfying one scan query. It is a tree of physical operators
4+
over a row domain, describing the reads and derived work needed to produce that query's result.
5+
6+
## Operators, not layout mirrors
7+
8+
Plan operators describe *what work happens*, not *which layout produced it*. Their identity and
9+
operator-specific state are independent of the source layout kind. The complete plan node is not:
10+
its common lazy-child container can own hidden source state used to materialize individual children
11+
on demand.
12+
13+
| Operator | Work |
14+
| --- | --- |
15+
| `SegmentScan` | read one segment and decode it to an array |
16+
| `Concat` | concatenate its children row-wise |
17+
| `Pack` | assemble a struct from one child per field, plus optional validity |
18+
| `Take` | index `values` by `codes` |
19+
| `ListPack` | assemble a list from elements and offsets, plus optional validity |
20+
| `Eval` | apply an expression to its child |
21+
| `RowIdx` | offset row numbers into the file's row domain |
22+
23+
Naming operators for what they compute is what lets one rule cover every case. `Concat` of
24+
`Concat` flattens on shape alone, and `Take` over `SegmentScan` is the dictionary pushdown,
25+
regardless of the source layout.
26+
27+
The stored layout tree describes all physical data in a file. A plan is query-specific: it is built
28+
from that tree for one projection, filter, and row domain. Different queries over the same file can
29+
therefore produce different plans.
30+
31+
## Optimization
32+
33+
Child replacement is implemented by the common plan container rather than by every operator. It
34+
replaces the external child container, clones `PlanData`, then invokes the operator's
35+
`PlanVTable::with_children` callback to validate the new children and refresh derived caches such
36+
as `Concat` row offsets. Rules therefore rewrite the generic tree without reconstructing common
37+
plan fields inside each operator.
38+
39+
Optimization rewrites the initial tree so that each expression is evaluated as close as possible to
40+
the physical data that can satisfy it. Every rewrite must preserve the query result, including its
41+
dtype, row domain, row order, row identity, null behavior, and observable errors.
42+
43+
Planning does not read segment data. It constructs and optimizes a description of the work that a
44+
later execution stage will perform.
45+
46+
## Vtables
47+
48+
Each operator is a small vtable type implementing `PlanVTable`, paired with a `Plan<V>` container
49+
over a shared `PlanRef`. `PlanRef` points to one allocation whose ordinary fields hold the operator
50+
ID, dtype, row count, and lazy children. Only the unsized tail containing the vtable and
51+
`V::PlanData` is erased behind `dyn DynPlan`, so common-field reads do not use dynamic dispatch.
52+
`Plan<V>` provides typed access to that operator data through `Deref`.
53+
54+
`PlanVTable` also carries `id` and a `Metadata` codec. Operators with no unrecoverable state
55+
already serialize their metadata; the ones holding a read context or a bound expression return
56+
`None` until those codecs exist.
57+
58+
## Future work
59+
60+
Plans currently stop at construction and optimization. Still to come: a plan registry and foreign
61+
operator placeholder so third-party operators survive a round trip, a serialization envelope, and
62+
an execution stage that walks an optimized plan, reads the referenced segments, and returns the
63+
query result.

vortex-layout/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
//! optional bound filter, optional row range, [`Selection`](vortex_scan::selection::Selection),
1616
//! split strategy, and task concurrency settings, then produces array streams or iterators.
1717
pub mod layouts;
18+
pub mod plan;
1819

1920
pub use children::*;
2021
pub use encoding::*;

vortex-layout/src/plan/children.rs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use std::fmt;
5+
use std::sync::Arc;
6+
7+
use once_cell::sync::OnceCell;
8+
use vortex_error::VortexResult;
9+
use vortex_error::vortex_bail;
10+
use vortex_error::vortex_err;
11+
12+
use crate::plan::PlanRef;
13+
14+
type ChildInitializer = dyn Fn(usize) -> VortexResult<PlanRef> + 'static + Send + Sync;
15+
16+
/// Ordered plan children that may be initialized one slot at a time.
17+
///
18+
/// Eagerly constructed operators store already-filled slots. Layout lowering instead installs an
19+
/// initializer that owns the source layout and lowers each child on first access.
20+
#[derive(Clone)]
21+
pub struct PlanChildren {
22+
initializer: Option<Arc<ChildInitializer>>,
23+
cache: Arc<[OnceCell<PlanRef>]>,
24+
}
25+
26+
impl PlanChildren {
27+
/// Creates lazy child slots backed by `initializer`.
28+
pub(crate) fn lazy(
29+
len: usize,
30+
initializer: impl Fn(usize) -> VortexResult<PlanRef> + 'static + Send + Sync,
31+
) -> Self {
32+
Self {
33+
initializer: Some(Arc::new(initializer)),
34+
cache: (0..len).map(|_| OnceCell::new()).collect::<Vec<_>>().into(),
35+
}
36+
}
37+
38+
/// Returns the number of children without initializing any slot.
39+
pub fn len(&self) -> usize {
40+
self.cache.len()
41+
}
42+
43+
/// Returns whether there are no children.
44+
pub fn is_empty(&self) -> bool {
45+
self.cache.is_empty()
46+
}
47+
48+
/// Returns a child, initializing and caching its slot on first access.
49+
pub fn get(&self, index: usize) -> VortexResult<Option<PlanRef>> {
50+
let Some(cell) = self.cache.get(index) else {
51+
return Ok(None);
52+
};
53+
if let Some(child) = cell.get() {
54+
return Ok(Some(child.clone()));
55+
}
56+
57+
let initializer = self
58+
.initializer
59+
.as_ref()
60+
.ok_or_else(|| vortex_err!("Plan child {index} was not initialized"))?;
61+
Ok(Some(cell.get_or_try_init(|| initializer(index))?.clone()))
62+
}
63+
64+
/// Iterates over the children in logical order, initializing slots as they are visited.
65+
pub fn iter(&self) -> impl ExactSizeIterator<Item = VortexResult<PlanRef>> + '_ {
66+
(0..self.len()).map(|index| {
67+
self.get(index)?
68+
.ok_or_else(|| vortex_err!("Plan child {index} is absent"))
69+
})
70+
}
71+
72+
/// Materializes all children into an eager vector.
73+
pub fn to_vec(&self) -> VortexResult<Vec<PlanRef>> {
74+
self.iter().collect()
75+
}
76+
77+
/// Returns a child collection with one slot replaced.
78+
pub fn with_child(&self, index: usize, child: PlanRef) -> VortexResult<Self> {
79+
if index >= self.len() {
80+
vortex_bail!("Plan child index out of bounds: {index} of {}", self.len());
81+
}
82+
83+
let source = self.clone();
84+
Ok(Self::lazy(source.len(), move |child_index| {
85+
if child_index == index {
86+
return Ok(child.clone());
87+
}
88+
source
89+
.get(child_index)?
90+
.ok_or_else(|| vortex_err!("Plan child {child_index} is absent"))
91+
}))
92+
}
93+
}
94+
95+
impl From<Vec<PlanRef>> for PlanChildren {
96+
fn from(children: Vec<PlanRef>) -> Self {
97+
let cache = children
98+
.into_iter()
99+
.map(OnceCell::with_value)
100+
.collect::<Vec<_>>()
101+
.into();
102+
Self {
103+
initializer: None,
104+
cache,
105+
}
106+
}
107+
}
108+
109+
impl<const N: usize> From<[PlanRef; N]> for PlanChildren {
110+
fn from(children: [PlanRef; N]) -> Self {
111+
Vec::from(children).into()
112+
}
113+
}
114+
115+
impl Default for PlanChildren {
116+
fn default() -> Self {
117+
Vec::new().into()
118+
}
119+
}
120+
121+
impl fmt::Debug for PlanChildren {
122+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123+
formatter
124+
.debug_struct("PlanChildren")
125+
.field("len", &self.len())
126+
.field(
127+
"initialized",
128+
&self
129+
.cache
130+
.iter()
131+
.filter(|slot| slot.get().is_some())
132+
.count(),
133+
)
134+
.finish()
135+
}
136+
}

vortex-layout/src/plan/display.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use std::fmt;
5+
6+
pub use vortex_utils::tree::DepthContext as PlanTreeContext;
7+
pub use vortex_utils::tree::IndentedFormatter as PlanIndentedFormatter;
8+
use vortex_utils::tree::TreeDisplayAdapter;
9+
pub use vortex_utils::tree::TreeDisplayExtractor as PlanTreeExtractor;
10+
use vortex_utils::tree::write_indented_tree;
11+
12+
use super::PlanRef;
13+
14+
/// Adds the plan's display representation to a tree node's header.
15+
pub struct PlanSummaryExtractor;
16+
17+
impl PlanSummaryExtractor {
18+
/// Writes a plan directly to `formatter`.
19+
pub fn write(plan: &PlanRef, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
20+
write!(formatter, "{plan}")
21+
}
22+
}
23+
24+
impl PlanTreeExtractor<PlanRef, PlanTreeContext> for PlanSummaryExtractor {
25+
fn write_header(
26+
&self,
27+
plan: &PlanRef,
28+
_context: &PlanTreeContext,
29+
formatter: &mut fmt::Formatter<'_>,
30+
) -> fmt::Result {
31+
write!(formatter, " ")?;
32+
Self::write(plan, formatter)
33+
}
34+
}
35+
36+
/// Composable display builder for a physical plan tree.
37+
///
38+
/// Call `plan.tree_display()` for the default extractors. Use `plan.tree_display_builder()` to
39+
/// start with only node and child names, then add extractors with [`Self::with`].
40+
pub struct PlanTreeDisplay<'a> {
41+
plan: &'a PlanRef,
42+
extractors: Vec<Box<dyn PlanTreeExtractor<PlanRef, PlanTreeContext>>>,
43+
}
44+
45+
impl<'a> PlanTreeDisplay<'a> {
46+
/// Creates a tree display for `plan` with no extractors.
47+
pub fn new(plan: &'a PlanRef) -> Self {
48+
Self {
49+
plan,
50+
extractors: Vec::new(),
51+
}
52+
}
53+
54+
/// Creates a tree display using each plan's display representation.
55+
pub fn default_display(plan: &'a PlanRef) -> Self {
56+
Self::new(plan).with(PlanSummaryExtractor)
57+
}
58+
59+
/// Adds an extractor to the display pipeline.
60+
pub fn with<E: PlanTreeExtractor<PlanRef, PlanTreeContext> + 'static>(
61+
mut self,
62+
extractor: E,
63+
) -> Self {
64+
self.extractors.push(Box::new(extractor));
65+
self
66+
}
67+
68+
/// Adds a pre-boxed extractor to the display pipeline.
69+
pub fn with_boxed(
70+
mut self,
71+
extractor: Box<dyn PlanTreeExtractor<PlanRef, PlanTreeContext>>,
72+
) -> Self {
73+
self.extractors.push(extractor);
74+
self
75+
}
76+
}
77+
78+
impl TreeDisplayAdapter for PlanTreeDisplay<'_> {
79+
type Context = PlanTreeContext;
80+
type Node = PlanRef;
81+
82+
fn write_node(
83+
&self,
84+
plan: &PlanRef,
85+
context: &PlanTreeContext,
86+
formatter: &mut fmt::Formatter<'_>,
87+
) -> fmt::Result {
88+
for extractor in &self.extractors {
89+
extractor.write_header(plan, context, formatter)?;
90+
}
91+
Ok(())
92+
}
93+
94+
fn write_details(
95+
&self,
96+
plan: &PlanRef,
97+
context: &PlanTreeContext,
98+
formatter: &mut PlanIndentedFormatter<'_, '_>,
99+
) -> fmt::Result {
100+
for extractor in &self.extractors {
101+
extractor.write_details(plan, context, formatter)?;
102+
}
103+
Ok(())
104+
}
105+
106+
fn visit_children(
107+
&self,
108+
plan: &PlanRef,
109+
visit: &mut dyn FnMut(&str, &PlanRef, bool) -> fmt::Result,
110+
) -> fmt::Result {
111+
let children = plan.children();
112+
for index in 0..children.len() {
113+
let child = plan.child_required(index).map_err(|_| fmt::Error)?;
114+
let child_name = plan.child_name(index);
115+
visit(child_name.as_ref(), &child, index + 1 == children.len())?;
116+
}
117+
Ok(())
118+
}
119+
}
120+
121+
impl fmt::Display for PlanTreeDisplay<'_> {
122+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123+
write_indented_tree(
124+
self,
125+
"root",
126+
self.plan,
127+
&mut PlanTreeContext::default(),
128+
formatter,
129+
)
130+
}
131+
}

0 commit comments

Comments
 (0)