1use 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}