Skip to content

Commit 8fd3d94

Browse files
committed
Push expressions through plan operators
Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
1 parent e2533f2 commit 8fd3d94

8 files changed

Lines changed: 803 additions & 22 deletions

File tree

vortex-layout/src/plan/optimize.rs

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

4-
//! Generic bottom-up optimization over physical plans.
4+
//! Plan optimization.
5+
//!
6+
//! Optimization is driven top-down from [`Eval`] nodes, which apply the static parent-reduction
7+
//! rules in [`crate::plan::optimizer`] as they become applicable. Operators without a rule simply
8+
//! optimize their children.
59
610
use vortex_error::VortexResult;
711

@@ -10,26 +14,14 @@ use crate::plan::PlanRef;
1014

1115
/// Optimizes `plan`, preserving its dtype and row domain.
1216
pub fn optimize(plan: PlanRef) -> VortexResult<PlanRef> {
13-
let mut children = Vec::with_capacity(plan.child_count());
14-
let mut changed = false;
15-
for child in plan.children().iter() {
16-
let child = child?;
17-
let optimized = optimize(child.clone())?;
18-
changed |= !PlanRef::ptr_eq(&child, &optimized);
19-
children.push(optimized);
17+
if let Some(eval) = plan.as_opt::<Eval>() {
18+
return eval.optimize_top_down(None);
2019
}
2120

22-
let plan = if changed {
23-
plan.with_children(children)?
24-
} else {
25-
plan
26-
};
27-
28-
let Some(eval) = plan.as_opt::<Eval>() else {
29-
return Ok(plan);
30-
};
31-
if eval.expression().is_root() {
32-
return eval.child_plan();
33-
}
34-
Ok(plan)
21+
let children = plan
22+
.children()
23+
.iter()
24+
.map(|child| optimize(child?))
25+
.collect::<VortexResult<Vec<_>>>()?;
26+
plan.with_children(children)
3527
}

vortex-layout/src/plan/optimizer/mod.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,33 @@ pub use rules::DynPlanParentReduceRule;
99
pub use rules::PlanParentReduceRule;
1010
pub use rules::PlanParentReduceRuleAdapter;
1111
pub use rules::PlanParentRuleSet;
12+
use vortex_error::VortexResult;
13+
14+
use super::Concat;
15+
use super::Pack;
16+
use super::PlanRef;
17+
use super::Take;
18+
use super::plans::ExpressionConcatRule;
19+
use super::plans::ExpressionPackRule;
20+
use super::plans::ExpressionTakeRule;
21+
22+
static EXPRESSION_CONCAT_RULE: PlanParentReduceRuleAdapter<Concat, ExpressionConcatRule> =
23+
PlanParentReduceRuleAdapter::new(ExpressionConcatRule);
24+
static EXPRESSION_TAKE_RULE: PlanParentReduceRuleAdapter<Take, ExpressionTakeRule> =
25+
PlanParentReduceRuleAdapter::new(ExpressionTakeRule);
26+
static EXPRESSION_PACK_RULE: PlanParentReduceRuleAdapter<Pack, ExpressionPackRule> =
27+
PlanParentReduceRuleAdapter::new(ExpressionPackRule);
28+
29+
static PARENT_RULES: PlanParentRuleSet = PlanParentRuleSet::new(&[
30+
&EXPRESSION_CONCAT_RULE,
31+
&EXPRESSION_TAKE_RULE,
32+
&EXPRESSION_PACK_RULE,
33+
]);
34+
35+
/// Attempts a static rewrite for `parent` and its child at `child_idx`.
36+
pub(crate) fn reduce_parent(parent: &PlanRef, child_idx: usize) -> VortexResult<Option<PlanRef>> {
37+
let Some(child) = parent.child(child_idx)? else {
38+
return Ok(None);
39+
};
40+
PARENT_RULES.evaluate(&child, parent, child_idx)
41+
}

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@ use std::sync::Arc;
66

77
use vortex_array::EmptyMetadata;
88
use vortex_array::dtype::DType;
9+
use vortex_array::expr::ExactBoundExpr;
10+
use vortex_array::expr::label_bound_tree;
911
use vortex_error::VortexResult;
1012
use vortex_error::vortex_bail;
1113
use vortex_session::registry::CachedId;
1214

15+
use crate::layouts::row_idx::RowIdx as RowIdxFn;
16+
use crate::plan::Eval;
17+
use crate::plan::EvalPlan;
1318
use crate::plan::Plan;
1419
use crate::plan::PlanChildren;
1520
use crate::plan::PlanId;
1621
use crate::plan::PlanParts;
1722
use crate::plan::PlanRef;
1823
use crate::plan::PlanVTable;
24+
use crate::plan::optimizer::PlanParentReduceRule;
1925

2026
/// Concatenates its children row-wise.
2127
#[derive(Clone, Debug)]
@@ -134,3 +140,45 @@ impl PlanVTable for Concat {
134140
Cow::Owned(format!("chunks[{index}]"))
135141
}
136142
}
143+
144+
/// Pushes an expression into every chunk of a [`Concat`].
145+
#[derive(Debug)]
146+
pub(crate) struct ExpressionConcatRule;
147+
148+
impl PlanParentReduceRule<Concat> for ExpressionConcatRule {
149+
type Parent = Eval;
150+
151+
fn reduce_parent(
152+
&self,
153+
child: &Plan<Concat>,
154+
parent: &Plan<Eval>,
155+
_child_idx: usize,
156+
) -> VortexResult<Option<PlanRef>> {
157+
let expression = parent.expression();
158+
// Row-index expressions are relative to the whole row domain, so they cannot be evaluated
159+
// chunk by chunk.
160+
let references_row_idx = label_bound_tree(
161+
expression,
162+
|node| {
163+
node.as_scalar()
164+
.is_some_and(|scalar_fn| scalar_fn.is::<RowIdxFn>())
165+
},
166+
|acc, &child| acc | child,
167+
)
168+
.get(&ExactBoundExpr(expression.clone()))
169+
.copied()
170+
.unwrap_or(false);
171+
if references_row_idx {
172+
return Ok(None);
173+
}
174+
175+
let chunks = child
176+
.children()
177+
.iter()
178+
.map(|chunk| Ok(EvalPlan::new(expression.clone(), chunk?).into_plan()))
179+
.collect::<VortexResult<Vec<_>>>()?;
180+
Ok(Some(
181+
ConcatPlan::try_new(expression.dtype().clone(), chunks)?.into_plan(),
182+
))
183+
}
184+
}

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

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@ use std::borrow::Cow;
55
use std::fmt;
66

77
use vortex_array::EmptyMetadata;
8+
use vortex_array::dtype::DType;
9+
use vortex_array::dtype::FieldName;
810
use vortex_array::expr::BoundExpression;
11+
use vortex_array::expr::traversal::NodeExt;
12+
use vortex_array::expr::traversal::Transformed;
13+
use vortex_array::expr::traversal::TraversalOrder;
14+
use vortex_array::scalar_fn::ScalarFnVTableExt;
15+
use vortex_array::scalar_fn::fns::get_item::GetItem;
916
use vortex_error::VortexResult;
1017
use vortex_session::registry::CachedId;
1118

@@ -16,6 +23,8 @@ use crate::plan::PlanParts;
1623
use crate::plan::PlanRef;
1724
use crate::plan::PlanVTable;
1825
use crate::plan::check_child_count;
26+
use crate::plan::optimize;
27+
use crate::plan::optimizer::reduce_parent;
1928

2029
/// Applies an expression to the output of its child.
2130
#[derive(Clone, Debug)]
@@ -99,3 +108,94 @@ impl PlanVTable for Eval {
99108
}
100109
}
101110
}
111+
112+
impl EvalPlan {
113+
/// Optimizes this plan top-down, applying parent-reduction rules as they become applicable.
114+
///
115+
/// `blocked_child_type` suppresses one rule re-firing on its own residual output, which would
116+
/// otherwise loop when a rewrite leaves an expression above the same child kind.
117+
pub(crate) fn optimize_top_down(
118+
&self,
119+
blocked_child_type: Option<PlanId>,
120+
) -> VortexResult<PlanRef> {
121+
if self.expression().is_root() {
122+
return optimize(self.child_plan()?);
123+
}
124+
125+
let child = self.child_plan()?;
126+
let child_type = child.id();
127+
let parent = EvalPlan::new(self.expression().clone(), child.clone()).into_plan();
128+
if blocked_child_type != Some(child_type)
129+
&& let Some(rewritten) = reduce_parent(&parent, 0)?
130+
{
131+
return Self::optimize_rewrite(rewritten, child_type);
132+
}
133+
134+
let child = optimize(child)?;
135+
136+
let child_type = child.id();
137+
let parent = EvalPlan::new(self.expression().clone(), child).into_plan();
138+
if blocked_child_type != Some(child_type)
139+
&& let Some(rewritten) = reduce_parent(&parent, 0)?
140+
{
141+
return Self::optimize_rewrite(rewritten, child_type);
142+
}
143+
Ok(parent)
144+
}
145+
146+
fn optimize_rewrite(rewritten: PlanRef, previous_child_type: PlanId) -> VortexResult<PlanRef> {
147+
let Some(eval) = rewritten.as_opt::<Eval>() else {
148+
return optimize(rewritten);
149+
};
150+
// A residual expression may remain above the same child kind after a successful rewrite.
151+
// Do not immediately apply that rule again; recursively optimize only the retained child.
152+
let child_type = eval.child_plan()?.id();
153+
let blocked = (child_type == previous_child_type).then_some(previous_child_type);
154+
eval.optimize_top_down(blocked)
155+
}
156+
}
157+
158+
/// Rewrites partition accessors in `expression` to read from a partitioned root.
159+
pub(crate) fn rewrite_partition_root(
160+
expression: BoundExpression,
161+
root_dtype: DType,
162+
collapsed: &[(FieldName, FieldName)],
163+
) -> VortexResult<BoundExpression> {
164+
Ok(expression
165+
.transform_down(|node| {
166+
if let Some(value_name) = node
167+
.as_scalar()
168+
.and_then(|scalar_fn| scalar_fn.as_opt::<GetItem>())
169+
{
170+
let partition_access = &node.children()[0];
171+
if let Some(partition_name) = partition_access
172+
.as_scalar()
173+
.and_then(|scalar_fn| scalar_fn.as_opt::<GetItem>())
174+
&& partition_access.children()[0].is_root()
175+
&& collapsed.iter().any(|(partition, value)| {
176+
partition == partition_name && value == value_name
177+
})
178+
{
179+
return Ok(Transformed {
180+
value: BoundExpression::try_new(
181+
GetItem.bind(partition_name.clone()),
182+
[BoundExpression::new_root(root_dtype.clone())],
183+
)?,
184+
changed: true,
185+
order: TraversalOrder::Skip,
186+
});
187+
}
188+
}
189+
190+
if node.is_root() {
191+
Ok(Transformed {
192+
value: BoundExpression::new_root(root_dtype.clone()),
193+
changed: true,
194+
order: TraversalOrder::Skip,
195+
})
196+
} else {
197+
Ok(Transformed::no(node))
198+
}
199+
})?
200+
.into_inner())
201+
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

44
mod concat;
5-
mod eval;
5+
pub(crate) mod eval;
66
mod list_pack;
77
mod pack;
88
mod row_idx;
@@ -12,12 +12,14 @@ mod take;
1212
pub use concat::Concat;
1313
pub use concat::ConcatData;
1414
pub use concat::ConcatPlan;
15+
pub(crate) use concat::ExpressionConcatRule;
1516
pub use eval::Eval;
1617
pub use eval::EvalData;
1718
pub use eval::EvalPlan;
1819
pub use list_pack::ListPack;
1920
pub use list_pack::ListPackData;
2021
pub use list_pack::ListPackPlan;
22+
pub(crate) use pack::ExpressionPackRule;
2123
pub use pack::Pack;
2224
pub use pack::PackData;
2325
pub use pack::PackPlan;
@@ -28,5 +30,6 @@ pub use row_idx::RowIdxPlanMetadata;
2830
pub use segment_scan::SegmentScan;
2931
pub use segment_scan::SegmentScanData;
3032
pub use segment_scan::SegmentScanPlan;
33+
pub(crate) use take::ExpressionTakeRule;
3134
pub use take::Take;
3235
pub use take::TakePlan;

0 commit comments

Comments
 (0)