Skip to main content

hawdb_cascades/
stage.rs

1// Copyright 2026 Nowledge
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::str::FromStr;
16
17use crate::rule::{apply_rule_batch, AppliedRule, OptimizerRule};
18use crate::search::{RuleEvent, RuleOutcome};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub enum ApplyOrder {
22    Once,
23    TopDown,
24    BottomUp,
25    FixedPoint,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct OptimizationStage {
30    name: &'static str,
31    apply_order: ApplyOrder,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub struct StageStats {
36    pub input_count: usize,
37    pub output_count: usize,
38    pub applied_rules: usize,
39    pub skipped_rules: usize,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct StageTrace {
44    name: String,
45    apply_order: ApplyOrder,
46    stats: StageStats,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct StageRuleBatch<E> {
51    expressions: Vec<AppliedRule<E>>,
52    events: Vec<RuleEvent>,
53    trace: StageTrace,
54}
55
56pub struct RuleStage<'a, E> {
57    stage: OptimizationStage,
58    rules: Vec<&'a dyn OptimizerRule<E>>,
59}
60
61pub struct OptimizationPipeline<'a, E> {
62    stages: Vec<RuleStage<'a, E>>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct PipelineExecution<E> {
67    expression: E,
68    events: Vec<RuleEvent>,
69    traces: Vec<StageTrace>,
70}
71
72impl ApplyOrder {
73    pub fn all() -> &'static [Self] {
74        const ALL: &[ApplyOrder] = &[
75            ApplyOrder::Once,
76            ApplyOrder::TopDown,
77            ApplyOrder::BottomUp,
78            ApplyOrder::FixedPoint,
79        ];
80        ALL
81    }
82
83    pub fn as_str(self) -> &'static str {
84        match self {
85            ApplyOrder::Once => "once",
86            ApplyOrder::TopDown => "top_down",
87            ApplyOrder::BottomUp => "bottom_up",
88            ApplyOrder::FixedPoint => "fixed_point",
89        }
90    }
91}
92
93impl FromStr for ApplyOrder {
94    type Err = &'static str;
95
96    fn from_str(value: &str) -> Result<Self, Self::Err> {
97        Self::all()
98            .iter()
99            .copied()
100            .find(|order| order.as_str() == value)
101            .ok_or("unknown optimizer stage apply order")
102    }
103}
104
105impl OptimizationStage {
106    pub const fn new(name: &'static str, apply_order: ApplyOrder) -> Self {
107        Self { name, apply_order }
108    }
109
110    pub fn name(&self) -> &'static str {
111        self.name
112    }
113
114    pub fn apply_order(&self) -> ApplyOrder {
115        self.apply_order
116    }
117
118    pub fn trace(&self, stats: StageStats) -> StageTrace {
119        StageTrace {
120            name: self.name.to_string(),
121            apply_order: self.apply_order,
122            stats,
123        }
124    }
125
126    pub fn execute_rule_batch<E>(
127        &self,
128        expression: &E,
129        rules: &[&dyn OptimizerRule<E>],
130    ) -> StageRuleBatch<E> {
131        let batch = apply_rule_batch(expression, rules);
132        let (expressions, events) = batch.into_parts();
133        let applied_rules = events
134            .iter()
135            .filter(|event| event.outcome() == RuleOutcome::Applied)
136            .count();
137        let skipped_rules = events
138            .iter()
139            .filter(|event| event.outcome() == RuleOutcome::Skipped)
140            .count();
141        let stats =
142            StageStats::new(1, expressions.len()).with_rule_counts(applied_rules, skipped_rules);
143
144        StageRuleBatch {
145            expressions,
146            events,
147            trace: self.trace(stats),
148        }
149    }
150}
151
152impl StageStats {
153    pub const fn new(input_count: usize, output_count: usize) -> Self {
154        Self {
155            input_count,
156            output_count,
157            applied_rules: 0,
158            skipped_rules: 0,
159        }
160    }
161
162    pub const fn with_rule_counts(mut self, applied_rules: usize, skipped_rules: usize) -> Self {
163        self.applied_rules = applied_rules;
164        self.skipped_rules = skipped_rules;
165        self
166    }
167}
168
169impl StageTrace {
170    pub fn name(&self) -> &str {
171        &self.name
172    }
173
174    pub fn apply_order(&self) -> ApplyOrder {
175        self.apply_order
176    }
177
178    pub fn stats(&self) -> StageStats {
179        self.stats
180    }
181}
182
183impl<E> StageRuleBatch<E> {
184    pub fn expressions(&self) -> &[AppliedRule<E>] {
185        &self.expressions
186    }
187
188    pub fn events(&self) -> &[RuleEvent] {
189        &self.events
190    }
191
192    pub fn trace(&self) -> &StageTrace {
193        &self.trace
194    }
195
196    pub fn into_parts(self) -> (Vec<AppliedRule<E>>, Vec<RuleEvent>, StageTrace) {
197        (self.expressions, self.events, self.trace)
198    }
199}
200
201impl<'a, E> RuleStage<'a, E> {
202    pub fn new(stage: OptimizationStage, rules: Vec<&'a dyn OptimizerRule<E>>) -> Self {
203        Self { stage, rules }
204    }
205
206    pub fn stage(&self) -> &OptimizationStage {
207        &self.stage
208    }
209
210    pub fn rules(&self) -> &[&'a dyn OptimizerRule<E>] {
211        &self.rules
212    }
213}
214
215impl<'a, E> OptimizationPipeline<'a, E> {
216    pub fn new(stages: Vec<RuleStage<'a, E>>) -> Self {
217        Self { stages }
218    }
219
220    pub fn stages(&self) -> &[RuleStage<'a, E>] {
221        &self.stages
222    }
223}
224
225impl<E: Clone> OptimizationPipeline<'_, E> {
226    pub fn execute(&self, expression: E) -> PipelineExecution<E> {
227        let mut expression = expression;
228        let mut events = Vec::new();
229        let mut traces = Vec::new();
230
231        for stage in &self.stages {
232            let batch = stage.stage.execute_rule_batch(&expression, &stage.rules);
233            let (expressions, stage_events, trace) = batch.into_parts();
234            events.extend(stage_events);
235            traces.push(trace);
236            if let Some(applied) = expressions.into_iter().next() {
237                expression = applied.into_application().into_expression();
238            }
239        }
240
241        PipelineExecution {
242            expression,
243            events,
244            traces,
245        }
246    }
247}
248
249impl<E> PipelineExecution<E> {
250    pub fn expression(&self) -> &E {
251        &self.expression
252    }
253
254    pub fn into_expression(self) -> E {
255        self.expression
256    }
257
258    pub fn events(&self) -> &[RuleEvent] {
259        &self.events
260    }
261
262    pub fn traces(&self) -> &[StageTrace] {
263        &self.traces
264    }
265
266    pub fn into_parts(self) -> (E, Vec<RuleEvent>, Vec<StageTrace>) {
267        (self.expression, self.events, self.traces)
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::{ApplyOrder, OptimizationPipeline, OptimizationStage, RuleStage, StageStats};
274    use crate::{OptimizerRule, RuleApplication, RuleId, RuleKind, RuleOutcome, RulePromise};
275
276    #[derive(Debug, Clone, PartialEq, Eq)]
277    struct TestExpr(&'static str);
278
279    struct RenameRule {
280        id: RuleId,
281        priority: u16,
282        input: &'static str,
283        output: Option<&'static str>,
284    }
285
286    impl OptimizerRule<TestExpr> for RenameRule {
287        fn id(&self) -> RuleId {
288            self.id
289        }
290
291        fn promise(&self, expression: &TestExpr) -> RulePromise {
292            if expression.0 == self.input {
293                RulePromise::new(self.priority)
294            } else {
295                RulePromise::NEVER
296            }
297        }
298
299        fn apply(&self, _expression: &TestExpr) -> Option<RuleApplication<TestExpr>> {
300            self.output
301                .map(|output| RuleApplication::new(TestExpr(output), format!("to={output}")))
302        }
303    }
304
305    #[test]
306    fn apply_order_strings_round_trip() {
307        for order in ApplyOrder::all() {
308            assert_eq!(order.as_str().parse::<ApplyOrder>(), Ok(*order));
309        }
310        assert!("sideways".parse::<ApplyOrder>().is_err());
311    }
312
313    #[test]
314    fn stage_trace_records_stable_stage_metadata() {
315        let stage = OptimizationStage::new("predicate_pushdown", ApplyOrder::BottomUp);
316        let trace = stage.trace(StageStats::new(3, 2).with_rule_counts(1, 4));
317
318        assert_eq!(trace.name(), "predicate_pushdown");
319        assert_eq!(trace.apply_order(), ApplyOrder::BottomUp);
320        assert_eq!(trace.stats().input_count, 3);
321        assert_eq!(trace.stats().output_count, 2);
322        assert_eq!(trace.stats().applied_rules, 1);
323        assert_eq!(trace.stats().skipped_rules, 4);
324    }
325
326    #[test]
327    fn stage_executes_rules_and_records_structured_trace() {
328        let stage = OptimizationStage::new("access_path_selection", ApplyOrder::BottomUp);
329        let high = RenameRule {
330            id: RuleId::new("node_equality_index_seek", RuleKind::Implementation),
331            priority: 100,
332            input: "filter",
333            output: Some("seek"),
334        };
335        let skipped = RenameRule {
336            id: RuleId::new("node_text_index_seek", RuleKind::Implementation),
337            priority: 10,
338            input: "filter",
339            output: None,
340        };
341
342        let batch = stage.execute_rule_batch(&TestExpr("filter"), &[&skipped, &high]);
343
344        assert_eq!(batch.expressions().len(), 1);
345        assert_eq!(
346            batch.expressions()[0].id().stable_name(),
347            "implementation:node_equality_index_seek"
348        );
349        assert_eq!(batch.events().len(), 2);
350        assert_eq!(batch.events()[0].outcome(), RuleOutcome::Applied);
351        assert_eq!(batch.events()[1].outcome(), RuleOutcome::Skipped);
352        assert_eq!(batch.trace().name(), "access_path_selection");
353        assert_eq!(batch.trace().stats().input_count, 1);
354        assert_eq!(batch.trace().stats().output_count, 1);
355        assert_eq!(batch.trace().stats().applied_rules, 1);
356        assert_eq!(batch.trace().stats().skipped_rules, 1);
357    }
358
359    #[test]
360    fn pipeline_executes_named_stages_in_order() {
361        let scan_to_filter = RenameRule {
362            id: RuleId::new("scan_to_filter", RuleKind::Transformation),
363            priority: 50,
364            input: "scan",
365            output: Some("filter"),
366        };
367        let filter_to_seek = RenameRule {
368            id: RuleId::new("filter_to_seek", RuleKind::Implementation),
369            priority: 100,
370            input: "filter",
371            output: Some("seek"),
372        };
373        let pipeline = OptimizationPipeline::new(vec![
374            RuleStage::new(
375                OptimizationStage::new("logical_rewrite", ApplyOrder::TopDown),
376                vec![&scan_to_filter],
377            ),
378            RuleStage::new(
379                OptimizationStage::new("access_path_selection", ApplyOrder::BottomUp),
380                vec![&filter_to_seek],
381            ),
382        ]);
383
384        let execution = pipeline.execute(TestExpr("scan"));
385
386        assert_eq!(execution.expression(), &TestExpr("seek"));
387        assert_eq!(execution.events().len(), 2);
388        assert_eq!(execution.traces().len(), 2);
389        assert_eq!(execution.traces()[0].name(), "logical_rewrite");
390        assert_eq!(execution.traces()[1].name(), "access_path_selection");
391        assert_eq!(execution.traces()[0].stats().applied_rules, 1);
392        assert_eq!(execution.traces()[1].stats().applied_rules, 1);
393    }
394}