Skip to content

Commit 8eee7bd

Browse files
committed
Add plan optimizer rules and push expressions
Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
1 parent a0a0b20 commit 8eee7bd

11 files changed

Lines changed: 1265 additions & 13 deletions

File tree

vortex-layout/src/plan/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mod children;
1111
mod display;
1212
mod lower;
1313
mod optimize;
14+
pub mod optimizer;
1415
mod plans;
1516
mod typed;
1617
mod vtable;

vortex-layout/src/plan/optimize.rs

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,35 @@
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+
//! The optimizer applies static rewrites top-down, optimizes children, then retries rewrites
7+
//! exposed by the optimized children.
58
69
use vortex_error::VortexResult;
710

8-
use crate::plan::Eval;
911
use crate::plan::PlanRef;
12+
use crate::plan::optimizer::reduce_parent;
13+
use crate::plan::optimizer::reduce_plan;
14+
15+
fn reduce(plan: &PlanRef) -> VortexResult<Option<PlanRef>> {
16+
if let Some(rewritten) = reduce_plan(plan)? {
17+
return Ok(Some(rewritten));
18+
}
19+
for child_idx in 0..plan.child_count() {
20+
if let Some(rewritten) = reduce_parent(plan, child_idx)? {
21+
return Ok(Some(rewritten));
22+
}
23+
}
24+
Ok(None)
25+
}
1026

1127
/// Optimizes `plan`, preserving its dtype and row domain.
1228
pub fn optimize(plan: PlanRef) -> VortexResult<PlanRef> {
29+
if let Some(rewritten) = reduce(&plan)? {
30+
return optimize(rewritten);
31+
}
32+
1333
let mut children = Vec::with_capacity(plan.child_count());
1434
let mut changed = false;
1535
for child in plan.children().iter() {
@@ -19,17 +39,13 @@ pub fn optimize(plan: PlanRef) -> VortexResult<PlanRef> {
1939
children.push(optimized);
2040
}
2141

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 {
42+
if !changed {
2943
return Ok(plan);
30-
};
31-
if eval.expression().is_root() {
32-
return eval.child_plan();
44+
}
45+
46+
let plan = plan.with_children(children)?;
47+
if let Some(rewritten) = reduce(&plan)? {
48+
return optimize(rewritten);
3349
}
3450
Ok(plan)
3551
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Static rewrite rules for physical plans.
5+
6+
mod rules;
7+
8+
pub use rules::DynPlanParentReduceRule;
9+
pub use rules::DynPlanReduceRule;
10+
pub use rules::PlanParentReduceRule;
11+
pub use rules::PlanParentReduceRuleAdapter;
12+
pub use rules::PlanParentRuleSet;
13+
pub use rules::PlanReduceRule;
14+
pub use rules::PlanReduceRuleAdapter;
15+
pub use rules::PlanRuleSet;
16+
use vortex_error::VortexResult;
17+
18+
use super::Concat;
19+
use super::Eval;
20+
use super::Pack;
21+
use super::PlanRef;
22+
use super::RowIdx;
23+
use super::Take;
24+
use super::plans::EvalIdentityRule;
25+
use super::plans::ExpressionConcatRule;
26+
use super::plans::ExpressionPackRule;
27+
use super::plans::ExpressionRowIdxRule;
28+
use super::plans::ExpressionTakeRule;
29+
30+
static EVAL_IDENTITY_RULE: PlanReduceRuleAdapter<Eval, EvalIdentityRule> =
31+
PlanReduceRuleAdapter::new(EvalIdentityRule);
32+
33+
static PLAN_RULES: PlanRuleSet = PlanRuleSet::new(&[&EVAL_IDENTITY_RULE]);
34+
35+
static EXPRESSION_CONCAT_RULE: PlanParentReduceRuleAdapter<Concat, ExpressionConcatRule> =
36+
PlanParentReduceRuleAdapter::new(ExpressionConcatRule);
37+
static EXPRESSION_TAKE_RULE: PlanParentReduceRuleAdapter<Take, ExpressionTakeRule> =
38+
PlanParentReduceRuleAdapter::new(ExpressionTakeRule);
39+
static EXPRESSION_ROW_IDX_RULE: PlanParentReduceRuleAdapter<RowIdx, ExpressionRowIdxRule> =
40+
PlanParentReduceRuleAdapter::new(ExpressionRowIdxRule);
41+
static EXPRESSION_PACK_RULE: PlanParentReduceRuleAdapter<Pack, ExpressionPackRule> =
42+
PlanParentReduceRuleAdapter::new(ExpressionPackRule);
43+
44+
static PARENT_RULES: PlanParentRuleSet = PlanParentRuleSet::new(&[
45+
&EXPRESSION_CONCAT_RULE,
46+
&EXPRESSION_TAKE_RULE,
47+
&EXPRESSION_ROW_IDX_RULE,
48+
&EXPRESSION_PACK_RULE,
49+
]);
50+
51+
/// Attempts a static rewrite for `plan`.
52+
pub(crate) fn reduce_plan(plan: &PlanRef) -> VortexResult<Option<PlanRef>> {
53+
PLAN_RULES.evaluate(plan)
54+
}
55+
56+
/// Attempts a static rewrite for `parent` and its child at `child_idx`.
57+
pub(crate) fn reduce_parent(parent: &PlanRef, child_idx: usize) -> VortexResult<Option<PlanRef>> {
58+
let Some(child) = parent.child(child_idx)? else {
59+
return Ok(None);
60+
};
61+
PARENT_RULES.evaluate(&child, parent, child_idx)
62+
}
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Typed and type-erased interfaces for plan rewrites.
5+
6+
use std::any::type_name;
7+
use std::fmt::Debug;
8+
use std::marker::PhantomData;
9+
10+
use vortex_error::VortexResult;
11+
12+
use crate::plan::Plan;
13+
use crate::plan::PlanRef;
14+
use crate::plan::PlanVTable;
15+
16+
/// A rewrite over one concrete plan operator.
17+
///
18+
/// Rules return one rewrite without recursively optimizing the replacement. The plan optimizer
19+
/// owns traversal and drives further rewrites.
20+
pub trait PlanReduceRule<P: PlanVTable>: Debug + Send + Sync + 'static {
21+
/// Attempts to replace `plan`.
22+
fn reduce(&self, plan: &Plan<P>) -> VortexResult<Option<PlanRef>>;
23+
}
24+
25+
/// Type-erased interface used by [`PlanRuleSet`].
26+
pub trait DynPlanReduceRule: Debug + Send + Sync + 'static {
27+
/// Returns whether this rule supports the concrete plan operator.
28+
fn matches(&self, plan: &PlanRef) -> bool;
29+
30+
/// Attempts to replace `plan`.
31+
fn reduce(&self, plan: &PlanRef) -> VortexResult<Option<PlanRef>>;
32+
}
33+
34+
/// Bridges a typed [`PlanReduceRule`] to a type-erased static registry.
35+
pub struct PlanReduceRuleAdapter<P, R> {
36+
rule: R,
37+
_plan: PhantomData<fn() -> P>,
38+
}
39+
40+
impl<P, R> PlanReduceRuleAdapter<P, R> {
41+
/// Creates an adapter for a typed plan rule.
42+
pub const fn new(rule: R) -> Self {
43+
Self {
44+
rule,
45+
_plan: PhantomData,
46+
}
47+
}
48+
}
49+
50+
impl<P, R> Debug for PlanReduceRuleAdapter<P, R>
51+
where
52+
P: PlanVTable,
53+
R: PlanReduceRule<P>,
54+
{
55+
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56+
formatter
57+
.debug_struct("PlanReduceRuleAdapter")
58+
.field("plan", &type_name::<P>())
59+
.field("rule", &self.rule)
60+
.finish()
61+
}
62+
}
63+
64+
impl<P, R> DynPlanReduceRule for PlanReduceRuleAdapter<P, R>
65+
where
66+
P: PlanVTable,
67+
R: PlanReduceRule<P>,
68+
{
69+
fn matches(&self, plan: &PlanRef) -> bool {
70+
plan.is::<P>()
71+
}
72+
73+
fn reduce(&self, plan: &PlanRef) -> VortexResult<Option<PlanRef>> {
74+
let Some(plan) = plan.as_opt::<P>() else {
75+
return Ok(None);
76+
};
77+
self.rule.reduce(plan)
78+
}
79+
}
80+
81+
/// An ordered static collection of single-plan rewrite rules.
82+
pub struct PlanRuleSet {
83+
rules: &'static [&'static dyn DynPlanReduceRule],
84+
}
85+
86+
impl PlanRuleSet {
87+
/// Creates a rule set whose first successful rewrite wins.
88+
pub const fn new(rules: &'static [&'static dyn DynPlanReduceRule]) -> Self {
89+
Self { rules }
90+
}
91+
92+
/// Evaluates rules registered for the concrete plan operator.
93+
pub fn evaluate(&self, plan: &PlanRef) -> VortexResult<Option<PlanRef>> {
94+
for rule in self.rules {
95+
if !rule.matches(plan) {
96+
continue;
97+
}
98+
let Some(reduced) = rule.reduce(plan)? else {
99+
continue;
100+
};
101+
102+
#[cfg(debug_assertions)]
103+
{
104+
vortex_error::vortex_ensure!(
105+
reduced.row_count() == plan.row_count(),
106+
"Plan rewrite from {rule:?} changed row count from {} to {}",
107+
plan.row_count(),
108+
reduced.row_count()
109+
);
110+
vortex_error::vortex_ensure!(
111+
reduced.dtype() == plan.dtype(),
112+
"Plan rewrite from {rule:?} changed dtype from {} to {}",
113+
plan.dtype(),
114+
reduced.dtype()
115+
);
116+
}
117+
118+
return Ok(Some(reduced));
119+
}
120+
Ok(None)
121+
}
122+
}
123+
124+
/// A metadata-only rewrite where a child plan rewrites its parent plan.
125+
///
126+
/// Rules return one rewrite without recursively optimizing the replacement. The plan optimizer
127+
/// owns traversal and drives further rewrites.
128+
pub trait PlanParentReduceRule<C: PlanVTable>: Debug + Send + Sync + 'static {
129+
/// The concrete parent operator matched by this rule.
130+
type Parent: PlanVTable;
131+
132+
/// Attempts to replace `parent` based on its child at `child_idx`.
133+
fn reduce_parent(
134+
&self,
135+
child: &Plan<C>,
136+
parent: &Plan<Self::Parent>,
137+
child_idx: usize,
138+
) -> VortexResult<Option<PlanRef>>;
139+
}
140+
141+
/// Type-erased interface used by [`PlanParentRuleSet`].
142+
pub trait DynPlanParentReduceRule: Debug + Send + Sync + 'static {
143+
/// Returns whether this rule supports the concrete child and parent operators.
144+
fn matches(&self, child: &PlanRef, parent: &PlanRef) -> bool;
145+
146+
/// Attempts to replace `parent` based on `child` at `child_idx`.
147+
fn reduce_parent(
148+
&self,
149+
child: &PlanRef,
150+
parent: &PlanRef,
151+
child_idx: usize,
152+
) -> VortexResult<Option<PlanRef>>;
153+
}
154+
155+
/// Bridges a typed [`PlanParentReduceRule`] to a type-erased static registry.
156+
pub struct PlanParentReduceRuleAdapter<C, R> {
157+
rule: R,
158+
_child: PhantomData<fn() -> C>,
159+
}
160+
161+
impl<C, R> PlanParentReduceRuleAdapter<C, R> {
162+
/// Creates an adapter for a typed parent-child rule.
163+
pub const fn new(rule: R) -> Self {
164+
Self {
165+
rule,
166+
_child: PhantomData,
167+
}
168+
}
169+
}
170+
171+
impl<C, R> Debug for PlanParentReduceRuleAdapter<C, R>
172+
where
173+
C: PlanVTable,
174+
R: PlanParentReduceRule<C>,
175+
{
176+
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177+
formatter
178+
.debug_struct("PlanParentReduceRuleAdapter")
179+
.field("parent", &type_name::<R::Parent>())
180+
.field("child", &type_name::<C>())
181+
.field("rule", &self.rule)
182+
.finish()
183+
}
184+
}
185+
186+
impl<C, R> DynPlanParentReduceRule for PlanParentReduceRuleAdapter<C, R>
187+
where
188+
C: PlanVTable,
189+
R: PlanParentReduceRule<C>,
190+
{
191+
fn matches(&self, child: &PlanRef, parent: &PlanRef) -> bool {
192+
child.is::<C>() && parent.is::<R::Parent>()
193+
}
194+
195+
fn reduce_parent(
196+
&self,
197+
child: &PlanRef,
198+
parent: &PlanRef,
199+
child_idx: usize,
200+
) -> VortexResult<Option<PlanRef>> {
201+
let Some(child) = child.as_opt::<C>() else {
202+
return Ok(None);
203+
};
204+
let Some(parent) = parent.as_opt::<R::Parent>() else {
205+
return Ok(None);
206+
};
207+
self.rule.reduce_parent(child, parent, child_idx)
208+
}
209+
}
210+
211+
/// An ordered static collection of parent-child plan rewrite rules.
212+
pub struct PlanParentRuleSet {
213+
rules: &'static [&'static dyn DynPlanParentReduceRule],
214+
}
215+
216+
impl PlanParentRuleSet {
217+
/// Creates a rule set whose first successful rewrite wins.
218+
pub const fn new(rules: &'static [&'static dyn DynPlanParentReduceRule]) -> Self {
219+
Self { rules }
220+
}
221+
222+
/// Evaluates rules registered for the concrete `(parent, child)` pair.
223+
pub fn evaluate(
224+
&self,
225+
child: &PlanRef,
226+
parent: &PlanRef,
227+
child_idx: usize,
228+
) -> VortexResult<Option<PlanRef>> {
229+
for rule in self.rules {
230+
if !rule.matches(child, parent) {
231+
continue;
232+
}
233+
let Some(reduced) = rule.reduce_parent(child, parent, child_idx)? else {
234+
continue;
235+
};
236+
237+
#[cfg(debug_assertions)]
238+
{
239+
vortex_error::vortex_ensure!(
240+
reduced.row_count() == parent.row_count(),
241+
"Plan rewrite from {rule:?} changed row count from {} to {}",
242+
parent.row_count(),
243+
reduced.row_count()
244+
);
245+
vortex_error::vortex_ensure!(
246+
reduced.dtype() == parent.dtype(),
247+
"Plan rewrite from {rule:?} changed dtype from {} to {}",
248+
parent.dtype(),
249+
reduced.dtype()
250+
);
251+
}
252+
253+
return Ok(Some(reduced));
254+
}
255+
Ok(None)
256+
}
257+
}

0 commit comments

Comments
 (0)