1#[derive(Clone, Debug, PartialEq)]
16pub enum QueryNode {
17 Scan {
19 predicate: String,
20 estimated_rows: u64,
21 },
22 Filter {
24 child: Box<QueryNode>,
25 condition: String,
26 },
27 Join {
29 left: Box<QueryNode>,
30 right: Box<QueryNode>,
31 join_key: String,
32 },
33 Project {
35 child: Box<QueryNode>,
36 fields: Vec<String>,
37 },
38 Limit {
40 child: Box<QueryNode>,
41 max_rows: u64,
42 },
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum OptimizationRule {
52 PushFilterDown,
54 EliminateDeadProject,
56 ReorderJoin,
58 FoldConstantLimit,
60 FlattenNestedFilter,
62}
63
64pub fn estimated_cost(node: &QueryNode) -> u64 {
70 match node {
71 QueryNode::Scan { estimated_rows, .. } => *estimated_rows,
72 QueryNode::Filter { child, .. } => estimated_cost(child).saturating_div(2),
73 QueryNode::Join { left, right, .. } => {
74 let lc = estimated_cost(left);
75 let rc = estimated_cost(right);
76 lc.saturating_mul(rc).saturating_div(100).saturating_add(1)
77 }
78 QueryNode::Project { child, .. } => estimated_cost(child),
79 QueryNode::Limit { child, max_rows } => (*max_rows).min(estimated_cost(child)),
80 }
81}
82
83fn apply_rule(rule: OptimizationRule, node: QueryNode) -> (QueryNode, bool) {
90 match rule {
91 OptimizationRule::PushFilterDown => push_filter_down(node),
92 OptimizationRule::EliminateDeadProject => eliminate_dead_project(node),
93 OptimizationRule::ReorderJoin => reorder_join(node),
94 OptimizationRule::FoldConstantLimit => fold_constant_limit(node),
95 OptimizationRule::FlattenNestedFilter => flatten_nested_filter(node),
96 }
97}
98
99fn push_filter_down(node: QueryNode) -> (QueryNode, bool) {
100 match node {
101 QueryNode::Filter { child, condition } => {
102 match *child {
103 QueryNode::Join {
104 left,
105 right,
106 join_key,
107 } => {
108 let new_left = Box::new(QueryNode::Filter {
110 child: left,
111 condition: condition.clone(),
112 });
113 let new_node = QueryNode::Join {
114 left: new_left,
115 right,
116 join_key,
117 };
118 (new_node, true)
119 }
120 other => (
121 QueryNode::Filter {
122 child: Box::new(other),
123 condition,
124 },
125 false,
126 ),
127 }
128 }
129 other => (other, false),
130 }
131}
132
133fn eliminate_dead_project(node: QueryNode) -> (QueryNode, bool) {
134 match node {
135 QueryNode::Project { child, fields } if fields.is_empty() => (*child, true),
136 other => (other, false),
137 }
138}
139
140fn reorder_join(node: QueryNode) -> (QueryNode, bool) {
141 match node {
142 QueryNode::Join {
143 left,
144 right,
145 join_key,
146 } => {
147 let lc = estimated_cost(&left);
148 let rc = estimated_cost(&right);
149 if rc < lc {
150 (
151 QueryNode::Join {
152 left: right,
153 right: left,
154 join_key,
155 },
156 true,
157 )
158 } else {
159 (
160 QueryNode::Join {
161 left,
162 right,
163 join_key,
164 },
165 false,
166 )
167 }
168 }
169 other => (other, false),
170 }
171}
172
173fn fold_constant_limit(node: QueryNode) -> (QueryNode, bool) {
174 match node {
175 QueryNode::Limit { max_rows: 0, .. } => (
176 QueryNode::Scan {
177 predicate: "empty".to_string(),
178 estimated_rows: 0,
179 },
180 true,
181 ),
182 other => (other, false),
183 }
184}
185
186fn flatten_nested_filter(node: QueryNode) -> (QueryNode, bool) {
187 match node {
188 QueryNode::Filter {
189 child,
190 condition: c1,
191 } => match *child {
192 QueryNode::Filter {
193 child: inner,
194 condition: c2,
195 } => {
196 let merged = format!("{c2} AND {c1}");
197 (
198 QueryNode::Filter {
199 child: inner,
200 condition: merged,
201 },
202 true,
203 )
204 }
205 other => (
206 QueryNode::Filter {
207 child: Box::new(other),
208 condition: c1,
209 },
210 false,
211 ),
212 },
213 other => (other, false),
214 }
215}
216
217fn apply_rule_tree(rule: OptimizationRule, node: QueryNode) -> (QueryNode, bool) {
224 let (node, child_changed) = recurse_children(rule, node);
226 let (node, self_changed) = apply_rule(rule, node);
227 (node, child_changed || self_changed)
228}
229
230fn recurse_children(rule: OptimizationRule, node: QueryNode) -> (QueryNode, bool) {
232 match node {
233 QueryNode::Scan { .. } => (node, false),
234 QueryNode::Filter { child, condition } => {
235 let (new_child, changed) = apply_rule_tree(rule, *child);
236 (
237 QueryNode::Filter {
238 child: Box::new(new_child),
239 condition,
240 },
241 changed,
242 )
243 }
244 QueryNode::Join {
245 left,
246 right,
247 join_key,
248 } => {
249 let (new_left, cl) = apply_rule_tree(rule, *left);
250 let (new_right, cr) = apply_rule_tree(rule, *right);
251 (
252 QueryNode::Join {
253 left: Box::new(new_left),
254 right: Box::new(new_right),
255 join_key,
256 },
257 cl || cr,
258 )
259 }
260 QueryNode::Project { child, fields } => {
261 let (new_child, changed) = apply_rule_tree(rule, *child);
262 (
263 QueryNode::Project {
264 child: Box::new(new_child),
265 fields,
266 },
267 changed,
268 )
269 }
270 QueryNode::Limit { child, max_rows } => {
271 let (new_child, changed) = apply_rule_tree(rule, *child);
272 (
273 QueryNode::Limit {
274 child: Box::new(new_child),
275 max_rows,
276 },
277 changed,
278 )
279 }
280 }
281}
282
283#[derive(Clone, Debug)]
289pub struct OptimizationResult {
290 pub original_cost: u64,
292 pub optimized_cost: u64,
294 pub rules_applied: Vec<OptimizationRule>,
296}
297
298impl OptimizationResult {
299 pub fn improvement_pct(&self) -> f64 {
302 if self.original_cost == 0 {
303 return 0.0;
304 }
305 let saved = self.original_cost.saturating_sub(self.optimized_cost) as f64;
306 saved / self.original_cost as f64 * 100.0
307 }
308}
309
310#[derive(Clone, Debug)]
316pub struct OptimizerStats {
317 pub total_optimizations: u64,
319 pub total_cost_saved: u64,
321 pub enabled_rules: usize,
323}
324
325pub struct TensorQueryOptimizer {
331 pub enabled_rules: Vec<OptimizationRule>,
333 pub total_optimizations: u64,
335 pub total_cost_saved: u64,
337}
338
339impl TensorQueryOptimizer {
340 pub fn new(rules: Vec<OptimizationRule>) -> Self {
342 Self {
343 enabled_rules: rules,
344 total_optimizations: 0,
345 total_cost_saved: 0,
346 }
347 }
348
349 pub fn optimize(&mut self, root: QueryNode) -> (QueryNode, OptimizationResult) {
354 let original_cost = estimated_cost(&root);
355 let mut current = root;
356 let mut rules_applied: Vec<OptimizationRule> = Vec::new();
357
358 const MAX_PASSES: usize = 10;
359
360 for _ in 0..MAX_PASSES {
361 let mut pass_changed = false;
362
363 for &rule in &self.enabled_rules {
364 let (new_node, changed) = apply_rule_tree(rule, current);
365 current = new_node;
366 if changed {
367 pass_changed = true;
368 if !rules_applied.contains(&rule) {
369 rules_applied.push(rule);
370 }
371 }
372 }
373
374 if !pass_changed {
375 break;
376 }
377 }
378
379 let optimized_cost = estimated_cost(¤t);
380 let result = OptimizationResult {
381 original_cost,
382 optimized_cost,
383 rules_applied,
384 };
385
386 self.total_optimizations += 1;
387 self.total_cost_saved = self
388 .total_cost_saved
389 .saturating_add(original_cost.saturating_sub(optimized_cost));
390
391 (current, result)
392 }
393
394 pub fn stats(&self) -> OptimizerStats {
396 OptimizerStats {
397 total_optimizations: self.total_optimizations,
398 total_cost_saved: self.total_cost_saved,
399 enabled_rules: self.enabled_rules.len(),
400 }
401 }
402}
403
404#[cfg(test)]
409mod tests {
410 use super::*;
411
412 fn scan(rows: u64) -> QueryNode {
415 QueryNode::Scan {
416 predicate: "true".to_string(),
417 estimated_rows: rows,
418 }
419 }
420
421 fn filter(child: QueryNode, cond: &str) -> QueryNode {
422 QueryNode::Filter {
423 child: Box::new(child),
424 condition: cond.to_string(),
425 }
426 }
427
428 fn join(left: QueryNode, right: QueryNode) -> QueryNode {
429 QueryNode::Join {
430 left: Box::new(left),
431 right: Box::new(right),
432 join_key: "id".to_string(),
433 }
434 }
435
436 fn project(child: QueryNode, fields: Vec<&str>) -> QueryNode {
437 QueryNode::Project {
438 child: Box::new(child),
439 fields: fields.iter().map(|s| s.to_string()).collect(),
440 }
441 }
442
443 fn limit(child: QueryNode, max_rows: u64) -> QueryNode {
444 QueryNode::Limit {
445 child: Box::new(child),
446 max_rows,
447 }
448 }
449
450 #[test]
453 fn cost_scan() {
454 assert_eq!(estimated_cost(&scan(500)), 500);
455 }
456
457 #[test]
458 fn cost_filter() {
459 let node = filter(scan(100), "x > 5");
461 assert_eq!(estimated_cost(&node), 50);
462 }
463
464 #[test]
465 fn cost_join() {
466 let node = join(scan(100), scan(200));
468 assert_eq!(estimated_cost(&node), 201);
469 }
470
471 #[test]
472 fn cost_project() {
473 let node = project(scan(300), vec!["a", "b"]);
474 assert_eq!(estimated_cost(&node), 300);
475 }
476
477 #[test]
478 fn cost_limit() {
479 let node = limit(scan(1000), 50);
480 assert_eq!(estimated_cost(&node), 50);
481
482 let node2 = limit(scan(10), 50);
483 assert_eq!(estimated_cost(&node2), 10);
484 }
485
486 #[test]
489 fn push_filter_down_applied() {
490 let plan = filter(join(scan(100), scan(200)), "a = 1");
491 let mut opt = TensorQueryOptimizer::new(vec![OptimizationRule::PushFilterDown]);
492 let (result, info) = opt.optimize(plan);
493
494 match result {
497 QueryNode::Join { left, .. } => match *left {
498 QueryNode::Filter { condition, .. } => {
499 assert_eq!(condition, "a = 1");
500 }
501 other => panic!("expected Filter on left, got {other:?}"),
502 },
503 other => panic!("expected Join at root, got {other:?}"),
504 }
505 assert!(info
506 .rules_applied
507 .contains(&OptimizationRule::PushFilterDown));
508 }
509
510 #[test]
511 fn push_filter_down_no_op_on_non_join() {
512 let plan = filter(scan(100), "a = 1");
514 let mut opt = TensorQueryOptimizer::new(vec![OptimizationRule::PushFilterDown]);
515 let (result, info) = opt.optimize(plan.clone());
516
517 assert_eq!(result, plan);
518 assert!(info.rules_applied.is_empty());
519 }
520
521 #[test]
524 fn eliminate_dead_project_removes_empty() {
525 let plan = project(scan(100), vec![]);
526 let mut opt = TensorQueryOptimizer::new(vec![OptimizationRule::EliminateDeadProject]);
527 let (result, info) = opt.optimize(plan);
528
529 assert_eq!(result, scan(100));
530 assert!(info
531 .rules_applied
532 .contains(&OptimizationRule::EliminateDeadProject));
533 }
534
535 #[test]
536 fn eliminate_dead_project_keeps_non_empty() {
537 let plan = project(scan(100), vec!["a", "b"]);
538 let mut opt = TensorQueryOptimizer::new(vec![OptimizationRule::EliminateDeadProject]);
539 let (result, info) = opt.optimize(plan.clone());
540
541 assert_eq!(result, plan);
542 assert!(info.rules_applied.is_empty());
543 }
544
545 #[test]
548 fn reorder_join_swaps_when_right_cheaper() {
549 let plan = join(scan(1000), scan(10));
551 let mut opt = TensorQueryOptimizer::new(vec![OptimizationRule::ReorderJoin]);
552 let (result, info) = opt.optimize(plan);
553
554 match result {
555 QueryNode::Join { left, right, .. } => {
556 assert_eq!(estimated_cost(&left), 10);
557 assert_eq!(estimated_cost(&right), 1000);
558 }
559 other => panic!("expected Join, got {other:?}"),
560 }
561 assert!(info.rules_applied.contains(&OptimizationRule::ReorderJoin));
562 }
563
564 #[test]
565 fn reorder_join_no_op_when_left_already_smaller() {
566 let plan = join(scan(10), scan(1000));
567 let mut opt = TensorQueryOptimizer::new(vec![OptimizationRule::ReorderJoin]);
568 let (result, info) = opt.optimize(plan.clone());
569
570 assert_eq!(result, plan);
571 assert!(info.rules_applied.is_empty());
572 }
573
574 #[test]
577 fn fold_constant_limit_zero_replaces_subtree() {
578 let plan = limit(scan(500), 0);
579 let mut opt = TensorQueryOptimizer::new(vec![OptimizationRule::FoldConstantLimit]);
580 let (result, info) = opt.optimize(plan);
581
582 assert_eq!(
583 result,
584 QueryNode::Scan {
585 predicate: "empty".to_string(),
586 estimated_rows: 0
587 }
588 );
589 assert!(info
590 .rules_applied
591 .contains(&OptimizationRule::FoldConstantLimit));
592 }
593
594 #[test]
595 fn fold_constant_limit_nonzero_no_op() {
596 let plan = limit(scan(500), 10);
597 let mut opt = TensorQueryOptimizer::new(vec![OptimizationRule::FoldConstantLimit]);
598 let (result, info) = opt.optimize(plan.clone());
599
600 assert_eq!(result, plan);
601 assert!(info.rules_applied.is_empty());
602 }
603
604 #[test]
607 fn flatten_nested_filter_merges_conditions() {
608 let plan = filter(filter(scan(100), "b = 2"), "a = 1");
609 let mut opt = TensorQueryOptimizer::new(vec![OptimizationRule::FlattenNestedFilter]);
610 let (result, info) = opt.optimize(plan);
611
612 match result {
613 QueryNode::Filter { condition, child } => {
614 assert_eq!(condition, "b = 2 AND a = 1");
615 assert_eq!(*child, scan(100));
616 }
617 other => panic!("expected Filter, got {other:?}"),
618 }
619 assert!(info
620 .rules_applied
621 .contains(&OptimizationRule::FlattenNestedFilter));
622 }
623
624 #[test]
625 fn flatten_nested_filter_no_op_single_filter() {
626 let plan = filter(scan(100), "a = 1");
627 let mut opt = TensorQueryOptimizer::new(vec![OptimizationRule::FlattenNestedFilter]);
628 let (result, info) = opt.optimize(plan.clone());
629
630 assert_eq!(result, plan);
631 assert!(info.rules_applied.is_empty());
632 }
633
634 #[test]
637 fn optimize_applies_multiple_rules_in_one_pass() {
638 let plan = project(filter(join(scan(1000), scan(10)), "x = 1"), vec![]);
643 let mut opt = TensorQueryOptimizer::new(vec![
644 OptimizationRule::EliminateDeadProject,
645 OptimizationRule::PushFilterDown,
646 OptimizationRule::ReorderJoin,
647 ]);
648 let (_, info) = opt.optimize(plan);
649 assert!(info.rules_applied.len() >= 2);
651 }
652
653 #[test]
654 fn optimize_repeats_until_stable() {
655 let plan = filter(filter(filter(scan(100), "c = 3"), "b = 2"), "a = 1");
657 let mut opt = TensorQueryOptimizer::new(vec![OptimizationRule::FlattenNestedFilter]);
658 let (result, _) = opt.optimize(plan);
659
660 match result {
662 QueryNode::Filter { child, .. } => {
663 assert!(!matches!(*child, QueryNode::Filter { .. }));
664 }
665 other => panic!("expected Filter at root, got {other:?}"),
666 }
667 }
668
669 #[test]
672 fn improvement_pct_computed_correctly() {
673 let result = OptimizationResult {
674 original_cost: 200,
675 optimized_cost: 100,
676 rules_applied: vec![],
677 };
678 let pct = result.improvement_pct();
679 assert!((pct - 50.0).abs() < 1e-9, "expected 50.0, got {pct}");
680 }
681
682 #[test]
683 fn improvement_pct_zero_when_original_zero() {
684 let result = OptimizationResult {
685 original_cost: 0,
686 optimized_cost: 0,
687 rules_applied: vec![],
688 };
689 assert_eq!(result.improvement_pct(), 0.0);
690 }
691
692 #[test]
695 fn total_optimizations_increments() {
696 let mut opt = TensorQueryOptimizer::new(vec![OptimizationRule::FoldConstantLimit]);
697 opt.optimize(limit(scan(100), 0));
698 opt.optimize(limit(scan(200), 0));
699 assert_eq!(opt.total_optimizations, 2);
700 }
701
702 #[test]
703 fn total_cost_saved_accumulates() {
704 let mut opt = TensorQueryOptimizer::new(vec![
708 OptimizationRule::EliminateDeadProject,
709 OptimizationRule::FlattenNestedFilter,
710 ]);
711 let (_, r1) = opt.optimize(project(scan(100), vec![]));
712 let (_, r2) = opt.optimize(filter(filter(scan(200), "b=2"), "a=1"));
713 let expected = r1
714 .original_cost
715 .saturating_sub(r1.optimized_cost)
716 .saturating_add(r2.original_cost.saturating_sub(r2.optimized_cost));
717 assert_eq!(opt.total_cost_saved, expected);
718 assert_eq!(opt.total_optimizations, 2);
719 }
720
721 #[test]
722 fn stats_correct() {
723 let mut opt = TensorQueryOptimizer::new(vec![
724 OptimizationRule::PushFilterDown,
725 OptimizationRule::ReorderJoin,
726 ]);
727 opt.optimize(scan(0));
728 let s = opt.stats();
729 assert_eq!(s.total_optimizations, 1);
730 assert_eq!(s.enabled_rules, 2);
731 }
732
733 #[test]
734 fn optimizer_with_empty_rules_no_ops() {
735 let plan = filter(join(scan(100), scan(200)), "x = 1");
736 let mut opt = TensorQueryOptimizer::new(vec![]);
737 let (result, info) = opt.optimize(plan.clone());
738
739 assert_eq!(result, plan);
740 assert!(info.rules_applied.is_empty());
741 }
742
743 #[test]
744 fn rules_applied_tracks_correctly() {
745 let plan = limit(scan(500), 0);
746 let mut opt = TensorQueryOptimizer::new(vec![
747 OptimizationRule::FoldConstantLimit,
748 OptimizationRule::PushFilterDown,
749 ]);
750 let (_, info) = opt.optimize(plan);
751
752 assert!(info
753 .rules_applied
754 .contains(&OptimizationRule::FoldConstantLimit));
755 assert!(!info
756 .rules_applied
757 .contains(&OptimizationRule::PushFilterDown));
758 }
759
760 #[test]
761 fn optimize_returns_original_when_no_rules_match() {
762 let plan = project(scan(42), vec!["name"]);
763 let mut opt = TensorQueryOptimizer::new(vec![
764 OptimizationRule::PushFilterDown,
765 OptimizationRule::ReorderJoin,
766 ]);
767 let (result, info) = opt.optimize(plan.clone());
768
769 assert_eq!(result, plan);
770 assert!(info.rules_applied.is_empty());
771 }
772
773 #[test]
774 fn limit_nonzero_not_folded() {
775 let plan = limit(scan(500), 1);
776 let mut opt = TensorQueryOptimizer::new(vec![OptimizationRule::FoldConstantLimit]);
777 let (result, _) = opt.optimize(plan.clone());
778 assert_eq!(result, plan);
779 }
780
781 #[test]
784 fn cost_nested_filter() {
785 let node = filter(filter(scan(400), "a"), "b");
787 assert_eq!(estimated_cost(&node), 100);
788 }
789
790 #[test]
791 fn cost_limit_zero() {
792 let node = limit(scan(999), 0);
793 assert_eq!(estimated_cost(&node), 0);
794 }
795
796 #[test]
797 fn push_filter_down_does_not_affect_project_child() {
798 let plan = filter(project(scan(100), vec!["x"]), "x = 1");
800 let mut opt = TensorQueryOptimizer::new(vec![OptimizationRule::PushFilterDown]);
801 let (result, info) = opt.optimize(plan.clone());
802 assert_eq!(result, plan);
803 assert!(info.rules_applied.is_empty());
804 }
805
806 #[test]
807 fn reorder_join_equal_costs_no_op() {
808 let plan = join(scan(100), scan(100));
809 let mut opt = TensorQueryOptimizer::new(vec![OptimizationRule::ReorderJoin]);
810 let (result, info) = opt.optimize(plan.clone());
811 assert_eq!(result, plan);
812 assert!(info.rules_applied.is_empty());
813 }
814
815 #[test]
816 fn flatten_then_push_filter_down_combined() {
817 let plan = filter(filter(join(scan(100), scan(200)), "b = 2"), "a = 1");
819 let mut opt = TensorQueryOptimizer::new(vec![
820 OptimizationRule::FlattenNestedFilter,
821 OptimizationRule::PushFilterDown,
822 ]);
823 let (result, info) = opt.optimize(plan);
824
825 match result {
827 QueryNode::Join { left, .. } => match *left {
828 QueryNode::Filter { condition, .. } => {
829 assert!(condition.contains("AND"), "condition was: {condition}");
830 }
831 other => panic!("expected Filter on left, got {other:?}"),
832 },
833 other => panic!("expected Join at root, got {other:?}"),
834 }
835 assert!(info
836 .rules_applied
837 .contains(&OptimizationRule::FlattenNestedFilter));
838 assert!(info
839 .rules_applied
840 .contains(&OptimizationRule::PushFilterDown));
841 }
842
843 #[test]
844 fn improvement_pct_no_improvement() {
845 let result = OptimizationResult {
846 original_cost: 100,
847 optimized_cost: 100,
848 rules_applied: vec![],
849 };
850 assert_eq!(result.improvement_pct(), 0.0);
851 }
852
853 #[test]
854 fn improvement_pct_full_elimination() {
855 let result = OptimizationResult {
857 original_cost: 100,
858 optimized_cost: 0,
859 rules_applied: vec![],
860 };
861 assert!((result.improvement_pct() - 100.0).abs() < 1e-9);
862 }
863}