Skip to content

Commit 956389d

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

9 files changed

Lines changed: 918 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: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,38 @@ 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::RowIdx;
18+
use super::Take;
19+
use super::plans::ExpressionConcatRule;
20+
use super::plans::ExpressionPackRule;
21+
use super::plans::ExpressionRowIdxRule;
22+
use super::plans::ExpressionTakeRule;
23+
24+
static EXPRESSION_CONCAT_RULE: PlanParentReduceRuleAdapter<Concat, ExpressionConcatRule> =
25+
PlanParentReduceRuleAdapter::new(ExpressionConcatRule);
26+
static EXPRESSION_TAKE_RULE: PlanParentReduceRuleAdapter<Take, ExpressionTakeRule> =
27+
PlanParentReduceRuleAdapter::new(ExpressionTakeRule);
28+
static EXPRESSION_ROW_IDX_RULE: PlanParentReduceRuleAdapter<RowIdx, ExpressionRowIdxRule> =
29+
PlanParentReduceRuleAdapter::new(ExpressionRowIdxRule);
30+
static EXPRESSION_PACK_RULE: PlanParentReduceRuleAdapter<Pack, ExpressionPackRule> =
31+
PlanParentReduceRuleAdapter::new(ExpressionPackRule);
32+
33+
static PARENT_RULES: PlanParentRuleSet = PlanParentRuleSet::new(&[
34+
&EXPRESSION_CONCAT_RULE,
35+
&EXPRESSION_TAKE_RULE,
36+
&EXPRESSION_ROW_IDX_RULE,
37+
&EXPRESSION_PACK_RULE,
38+
]);
39+
40+
/// Attempts a static rewrite for `parent` and its child at `child_idx`.
41+
pub(crate) fn reduce_parent(parent: &PlanRef, child_idx: usize) -> VortexResult<Option<PlanRef>> {
42+
let Some(child) = parent.child(child_idx)? else {
43+
return Ok(None);
44+
};
45+
PARENT_RULES.evaluate(&child, parent, child_idx)
46+
}

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

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_error::vortex_bail;
1118
use vortex_session::registry::CachedId;
@@ -17,6 +24,8 @@ use crate::plan::PlanParts;
1724
use crate::plan::PlanRef;
1825
use crate::plan::PlanVTable;
1926
use crate::plan::check_child_count;
27+
use crate::plan::optimize;
28+
use crate::plan::optimizer::reduce_parent;
2029

2130
/// Applies an expression to the output of its child.
2231
#[derive(Clone, Debug)]
@@ -123,3 +132,94 @@ fn validate_expression_child(expression: &BoundExpression, child: &PlanRef) -> V
123132
}
124133
Ok(())
125134
}
135+
136+
impl EvalPlan {
137+
/// Optimizes this plan top-down, applying parent-reduction rules as they become applicable.
138+
///
139+
/// `blocked_child_type` suppresses one rule re-firing on its own residual output, which would
140+
/// otherwise loop when a rewrite leaves an expression above the same child kind.
141+
pub(crate) fn optimize_top_down(
142+
&self,
143+
blocked_child_type: Option<PlanId>,
144+
) -> VortexResult<PlanRef> {
145+
if self.expression().is_root() {
146+
return optimize(self.child_plan()?);
147+
}
148+
149+
let child = self.child_plan()?;
150+
let child_type = child.id();
151+
let parent = EvalPlan::try_new(self.expression().clone(), child.clone())?.into_plan();
152+
if blocked_child_type != Some(child_type)
153+
&& let Some(rewritten) = reduce_parent(&parent, 0)?
154+
{
155+
return Self::optimize_rewrite(rewritten, child_type);
156+
}
157+
158+
let child = optimize(child)?;
159+
160+
let child_type = child.id();
161+
let parent = EvalPlan::try_new(self.expression().clone(), child)?.into_plan();
162+
if blocked_child_type != Some(child_type)
163+
&& let Some(rewritten) = reduce_parent(&parent, 0)?
164+
{
165+
return Self::optimize_rewrite(rewritten, child_type);
166+
}
167+
Ok(parent)
168+
}
169+
170+
fn optimize_rewrite(rewritten: PlanRef, previous_child_type: PlanId) -> VortexResult<PlanRef> {
171+
let Some(eval) = rewritten.as_opt::<Eval>() else {
172+
return optimize(rewritten);
173+
};
174+
// A residual expression may remain above the same child kind after a successful rewrite.
175+
// Do not immediately apply that rule again; recursively optimize only the retained child.
176+
let child_type = eval.child_plan()?.id();
177+
let blocked = (child_type == previous_child_type).then_some(previous_child_type);
178+
eval.optimize_top_down(blocked)
179+
}
180+
}
181+
182+
/// Rewrites partition accessors in `expression` to read from a partitioned root.
183+
pub(crate) fn rewrite_partition_root(
184+
expression: BoundExpression,
185+
root_dtype: DType,
186+
collapsed: &[(FieldName, FieldName)],
187+
) -> VortexResult<BoundExpression> {
188+
Ok(expression
189+
.transform_down(|node| {
190+
if let Some(value_name) = node
191+
.as_scalar()
192+
.and_then(|scalar_fn| scalar_fn.as_opt::<GetItem>())
193+
{
194+
let partition_access = &node.children()[0];
195+
if let Some(partition_name) = partition_access
196+
.as_scalar()
197+
.and_then(|scalar_fn| scalar_fn.as_opt::<GetItem>())
198+
&& partition_access.children()[0].is_root()
199+
&& collapsed.iter().any(|(partition, value)| {
200+
partition == partition_name && value == value_name
201+
})
202+
{
203+
return Ok(Transformed {
204+
value: BoundExpression::try_new(
205+
GetItem.bind(partition_name.clone()),
206+
[BoundExpression::new_root(root_dtype.clone())],
207+
)?,
208+
changed: true,
209+
order: TraversalOrder::Skip,
210+
});
211+
}
212+
}
213+
214+
if node.is_root() {
215+
Ok(Transformed {
216+
value: BoundExpression::new_root(root_dtype.clone()),
217+
changed: true,
218+
order: TraversalOrder::Skip,
219+
})
220+
} else {
221+
Ok(Transformed::no(node))
222+
}
223+
})?
224+
.into_inner())
225+
}

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

Lines changed: 5 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,21 +12,25 @@ 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;
26+
pub(crate) use row_idx::ExpressionRowIdxRule;
2427
pub use row_idx::RowIdx;
2528
pub use row_idx::RowIdxData;
2629
pub use row_idx::RowIdxPlan;
2730
pub use row_idx::RowIdxPlanMetadata;
2831
pub use segment_scan::SegmentScan;
2932
pub use segment_scan::SegmentScanData;
3033
pub use segment_scan::SegmentScanPlan;
34+
pub(crate) use take::ExpressionTakeRule;
3135
pub use take::Take;
3236
pub use take::TakePlan;

0 commit comments

Comments
 (0)