Skip to main content

vortex_layout/plan/physical/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#[cfg(debug_assertions)]
12use vortex_error::vortex_ensure_eq;
13
14use crate::plan::physical::Plan;
15use crate::plan::physical::PlanRef;
16use crate::plan::physical::PlanVTable;
17
18/// A rewrite over one concrete plan operator.
19///
20/// Rules return one rewrite without recursively optimizing the replacement. The plan optimizer
21/// owns traversal and drives further rewrites.
22pub trait PlanReduceRule<P: PlanVTable>: Debug + Send + Sync + 'static {
23    /// Attempts to replace `plan`.
24    fn reduce(&self, plan: &Plan<P>) -> VortexResult<Option<PlanRef>>;
25}
26
27/// Type-erased interface used by [`PlanRuleSet`].
28pub trait DynPlanReduceRule: Debug + Send + Sync + 'static {
29    /// Returns whether this rule supports the concrete plan operator.
30    fn matches(&self, plan: &PlanRef) -> bool;
31
32    /// Attempts to replace `plan`.
33    fn reduce(&self, plan: &PlanRef) -> VortexResult<Option<PlanRef>>;
34}
35
36/// Bridges a typed [`PlanReduceRule`] to a type-erased static registry.
37pub struct PlanReduceRuleAdapter<P, R> {
38    rule: R,
39    _plan: PhantomData<fn() -> P>,
40}
41
42impl<P, R> PlanReduceRuleAdapter<P, R> {
43    /// Creates an adapter for a typed plan rule.
44    pub const fn new(rule: R) -> Self {
45        Self {
46            rule,
47            _plan: PhantomData,
48        }
49    }
50}
51
52impl<P, R> Debug for PlanReduceRuleAdapter<P, R>
53where
54    P: PlanVTable,
55    R: PlanReduceRule<P>,
56{
57    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        formatter
59            .debug_struct("PlanReduceRuleAdapter")
60            .field("plan", &type_name::<P>())
61            .field("rule", &self.rule)
62            .finish()
63    }
64}
65
66impl<P, R> DynPlanReduceRule for PlanReduceRuleAdapter<P, R>
67where
68    P: PlanVTable,
69    R: PlanReduceRule<P>,
70{
71    fn matches(&self, plan: &PlanRef) -> bool {
72        plan.is::<P>()
73    }
74
75    fn reduce(&self, plan: &PlanRef) -> VortexResult<Option<PlanRef>> {
76        let Some(plan) = plan.as_opt::<P>() else {
77            return Ok(None);
78        };
79        self.rule.reduce(plan)
80    }
81}
82
83/// An ordered static collection of single-plan rewrite rules.
84pub struct PlanRuleSet {
85    rules: &'static [&'static dyn DynPlanReduceRule],
86}
87
88impl PlanRuleSet {
89    /// Creates a rule set whose first successful rewrite wins.
90    pub const fn new(rules: &'static [&'static dyn DynPlanReduceRule]) -> Self {
91        Self { rules }
92    }
93
94    /// Evaluates rules registered for the concrete plan operator.
95    pub fn evaluate(&self, plan: &PlanRef) -> VortexResult<Option<PlanRef>> {
96        for rule in self.rules {
97            if !rule.matches(plan) {
98                continue;
99            }
100            let Some(reduced) = rule.reduce(plan)? else {
101                continue;
102            };
103
104            #[cfg(debug_assertions)]
105            {
106                vortex_ensure_eq!(
107                    reduced.row_count(),
108                    plan.row_count(),
109                    "Plan rewrite from {rule:?} changed row count"
110                );
111                vortex_ensure_eq!(
112                    reduced.dtype(),
113                    plan.dtype(),
114                    "Plan rewrite from {rule:?} changed 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_ensure_eq!(
240                    reduced.row_count(),
241                    parent.row_count(),
242                    "Plan rewrite from {rule:?} changed row count"
243                );
244                vortex_ensure_eq!(
245                    reduced.dtype(),
246                    parent.dtype(),
247                    "Plan rewrite from {rule:?} changed dtype"
248                );
249            }
250
251            return Ok(Some(reduced));
252        }
253        Ok(None)
254    }
255}