Skip to main content

vortex_layout/plan/optimizer/
rules.rs

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
6use std::any::type_name;
7use std::fmt::Debug;
8use std::marker::PhantomData;
9
10use vortex_error::VortexResult;
11
12use crate::plan::Plan;
13use crate::plan::PlanRef;
14use 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.
20pub 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`].
26pub 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.
35pub struct PlanReduceRuleAdapter<P, R> {
36    rule: R,
37    _plan: PhantomData<fn() -> P>,
38}
39
40impl<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
50impl<P, R> Debug for PlanReduceRuleAdapter<P, R>
51where
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
64impl<P, R> DynPlanReduceRule for PlanReduceRuleAdapter<P, R>
65where
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.
82pub struct PlanRuleSet {
83    rules: &'static [&'static dyn DynPlanReduceRule],
84}
85
86impl 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.
128pub 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`].
142pub 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.
156pub struct PlanParentReduceRuleAdapter<C, R> {
157    rule: R,
158    _child: PhantomData<fn() -> C>,
159}
160
161impl<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
171impl<C, R> Debug for PlanParentReduceRuleAdapter<C, R>
172where
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
186impl<C, R> DynPlanParentReduceRule for PlanParentReduceRuleAdapter<C, R>
187where
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.
212pub struct PlanParentRuleSet {
213    rules: &'static [&'static dyn DynPlanParentReduceRule],
214}
215
216impl 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}