1use std::collections::HashSet;
7
8#[derive(Debug, Clone)]
10pub struct OptimizerRule {
11 pub name: &'static str,
12 pub description: &'static str,
13}
14
15#[derive(Debug, Clone, PartialEq)]
17pub enum JoinStrategy {
18 NestedLoop,
19 HashJoin,
20 MergeJoin,
21}
22
23#[derive(Debug, Clone, PartialEq)]
25pub enum PlanOperator {
26 Scan {
27 pattern: String,
28 },
29 Join {
30 strategy: JoinStrategy,
31 },
32 Filter {
33 expr: String,
34 },
35 Project {
36 vars: Vec<String>,
37 },
38 Union,
39 Optional,
40 Distinct,
41 Slice {
42 offset: Option<usize>,
43 limit: Option<usize>,
44 },
45}
46
47#[derive(Debug, Clone)]
49pub struct QueryPlan {
50 pub operator: PlanOperator,
51 pub children: Vec<QueryPlan>,
52 pub estimated_cost: f64,
53}
54
55impl QueryPlan {
56 pub fn new(operator: PlanOperator) -> Self {
58 QueryPlan {
59 operator,
60 children: Vec::new(),
61 estimated_cost: 0.0,
62 }
63 }
64
65 pub fn with_children(operator: PlanOperator, children: Vec<QueryPlan>) -> Self {
67 QueryPlan {
68 operator,
69 children,
70 estimated_cost: 0.0,
71 }
72 }
73
74 pub fn recompute_cost(&mut self) {
76 for child in &mut self.children {
77 child.recompute_cost();
78 }
79 self.estimated_cost = estimate_cost(self);
80 }
81}
82
83pub fn estimate_cost(plan: &QueryPlan) -> f64 {
85 match &plan.operator {
86 PlanOperator::Scan { .. } => 100.0,
87 PlanOperator::Join { .. } => {
88 let left = plan
89 .children
90 .first()
91 .map(|c| c.estimated_cost)
92 .unwrap_or(100.0);
93 let right = plan
94 .children
95 .get(1)
96 .map(|c| c.estimated_cost)
97 .unwrap_or(100.0);
98 left * right * 0.1
99 }
100 PlanOperator::Filter { .. } => {
101 let child = plan
102 .children
103 .first()
104 .map(|c| c.estimated_cost)
105 .unwrap_or(100.0);
106 child * 0.5
107 }
108 PlanOperator::Project { .. } => plan
109 .children
110 .first()
111 .map(|c| c.estimated_cost)
112 .unwrap_or(0.0),
113 PlanOperator::Union => plan.children.iter().map(|c| c.estimated_cost).sum::<f64>(),
114 PlanOperator::Optional => {
115 let left = plan
116 .children
117 .first()
118 .map(|c| c.estimated_cost)
119 .unwrap_or(100.0);
120 let right = plan
121 .children
122 .get(1)
123 .map(|c| c.estimated_cost)
124 .unwrap_or(100.0);
125 left + right * 0.3
126 }
127 PlanOperator::Distinct => plan
128 .children
129 .first()
130 .map(|c| c.estimated_cost * 1.1)
131 .unwrap_or(0.0),
132 PlanOperator::Slice { limit, .. } => {
133 let child = plan
134 .children
135 .first()
136 .map(|c| c.estimated_cost)
137 .unwrap_or(0.0);
138 if let Some(lim) = limit {
139 child * (*lim as f64 / 1000.0).min(1.0)
140 } else {
141 child
142 }
143 }
144 }
145}
146
147pub trait OptimizationPass: Send + Sync {
149 fn name(&self) -> &str;
150 fn apply(&self, plan: QueryPlan) -> QueryPlan;
151}
152
153pub struct FilterPushdownPass;
159
160impl FilterPushdownPass {
161 fn push_filter_into_children(filter_expr: String, child: QueryPlan) -> QueryPlan {
162 match &child.operator {
163 PlanOperator::Join { .. } => {
164 let mut new_children = child.children.clone();
166 if let Some(first) = new_children.first_mut() {
167 let filter_plan = QueryPlan::with_children(
168 PlanOperator::Filter { expr: filter_expr },
169 vec![first.clone()],
170 );
171 *first = filter_plan;
172 }
173 QueryPlan::with_children(child.operator.clone(), new_children)
174 }
175 _ => {
176 QueryPlan::with_children(PlanOperator::Filter { expr: filter_expr }, vec![child])
178 }
179 }
180 }
181}
182
183impl OptimizationPass for FilterPushdownPass {
184 fn name(&self) -> &str {
185 "FilterPushdown"
186 }
187
188 fn apply(&self, plan: QueryPlan) -> QueryPlan {
189 let children: Vec<QueryPlan> = plan.children.into_iter().map(|c| self.apply(c)).collect();
190 match plan.operator {
191 PlanOperator::Filter { ref expr } => {
192 if let Some(child) = children.into_iter().next() {
193 Self::push_filter_into_children(expr.clone(), child)
194 } else {
195 QueryPlan::new(plan.operator)
196 }
197 }
198 op => QueryPlan {
199 operator: op,
200 children,
201 estimated_cost: plan.estimated_cost,
202 },
203 }
204 }
205}
206
207pub struct ProjectionPruningPass;
213
214impl ProjectionPruningPass {
215 fn collect_scan_vars(plan: &QueryPlan) -> HashSet<String> {
216 let mut vars = HashSet::new();
217 match &plan.operator {
218 PlanOperator::Scan { pattern } => {
219 for token in pattern.split_whitespace() {
221 if token.starts_with('?') {
222 vars.insert(token.trim_start_matches('?').to_string());
223 }
224 }
225 }
226 PlanOperator::Project { vars: pv } => {
227 for v in pv {
228 vars.insert(v.clone());
229 }
230 }
231 _ => {}
232 }
233 for child in &plan.children {
234 vars.extend(Self::collect_scan_vars(child));
235 }
236 vars
237 }
238}
239
240impl OptimizationPass for ProjectionPruningPass {
241 fn name(&self) -> &str {
242 "ProjectionPruning"
243 }
244
245 fn apply(&self, plan: QueryPlan) -> QueryPlan {
246 let children: Vec<QueryPlan> = plan.children.into_iter().map(|c| self.apply(c)).collect();
247 match &plan.operator {
248 PlanOperator::Project { vars } => {
249 let upstream: HashSet<String> =
251 children.iter().flat_map(Self::collect_scan_vars).collect();
252 let all_included = vars.iter().all(|v| upstream.contains(v));
254 let reduces = vars.len() < upstream.len();
255 if all_included && !reduces && children.len() == 1 {
256 children
258 .into_iter()
259 .next()
260 .unwrap_or_else(|| QueryPlan::new(plan.operator.clone()))
261 } else {
262 QueryPlan {
263 operator: plan.operator.clone(),
264 children,
265 estimated_cost: plan.estimated_cost,
266 }
267 }
268 }
269 op => QueryPlan {
270 operator: op.clone(),
271 children,
272 estimated_cost: plan.estimated_cost,
273 },
274 }
275 }
276}
277
278pub struct JoinReorderPass;
284
285impl OptimizationPass for JoinReorderPass {
286 fn name(&self) -> &str {
287 "JoinReorder"
288 }
289
290 fn apply(&self, plan: QueryPlan) -> QueryPlan {
291 let children: Vec<QueryPlan> = plan.children.into_iter().map(|c| self.apply(c)).collect();
292 match &plan.operator {
293 PlanOperator::Join { .. } => {
294 let mut sorted = children;
295 sorted.sort_by(|a, b| {
296 a.estimated_cost
297 .partial_cmp(&b.estimated_cost)
298 .unwrap_or(std::cmp::Ordering::Equal)
299 });
300 QueryPlan {
301 operator: plan.operator.clone(),
302 children: sorted,
303 estimated_cost: plan.estimated_cost,
304 }
305 }
306 op => QueryPlan {
307 operator: op.clone(),
308 children,
309 estimated_cost: plan.estimated_cost,
310 },
311 }
312 }
313}
314
315pub struct ConstantFoldingPass;
321
322impl ConstantFoldingPass {
323 fn fold_expr(expr: &str) -> Option<bool> {
324 match expr.trim() {
325 "true" | "TRUE" | "1=1" => Some(true),
326 "false" | "FALSE" | "1=0" | "0=1" => Some(false),
327 _ => None,
328 }
329 }
330}
331
332impl OptimizationPass for ConstantFoldingPass {
333 fn name(&self) -> &str {
334 "ConstantFolding"
335 }
336
337 fn apply(&self, plan: QueryPlan) -> QueryPlan {
338 let operator = plan.operator.clone();
339 let estimated_cost = plan.estimated_cost;
340 let children: Vec<QueryPlan> = plan.children.into_iter().map(|c| self.apply(c)).collect();
341 match operator {
342 PlanOperator::Filter { ref expr } => {
343 match Self::fold_expr(expr) {
344 Some(true) => {
345 let mut iter = children.into_iter();
347 iter.next().unwrap_or_else(|| QueryPlan::new(operator))
348 }
349 Some(false) => {
350 QueryPlan::new(PlanOperator::Scan {
352 pattern: String::from("EMPTY"),
353 })
354 }
355 None => QueryPlan {
356 operator,
357 children,
358 estimated_cost,
359 },
360 }
361 }
362 op => QueryPlan {
363 operator: op,
364 children,
365 estimated_cost,
366 },
367 }
368 }
369}
370
371pub struct RedundantDistinctPass;
377
378impl RedundantDistinctPass {
379 fn is_unique_source(plan: &QueryPlan) -> bool {
380 matches!(plan.operator, PlanOperator::Scan { .. })
381 }
382}
383
384impl OptimizationPass for RedundantDistinctPass {
385 fn name(&self) -> &str {
386 "RedundantDistinct"
387 }
388
389 fn apply(&self, plan: QueryPlan) -> QueryPlan {
390 let children: Vec<QueryPlan> = plan.children.into_iter().map(|c| self.apply(c)).collect();
391 match &plan.operator {
392 PlanOperator::Distinct => {
393 if let Some(child) = children.first() {
395 if matches!(child.operator, PlanOperator::Distinct)
396 || Self::is_unique_source(child)
397 {
398 return children
399 .into_iter()
400 .next()
401 .unwrap_or_else(|| QueryPlan::new(plan.operator.clone()));
402 }
403 }
404 QueryPlan {
405 operator: plan.operator.clone(),
406 children,
407 estimated_cost: plan.estimated_cost,
408 }
409 }
410 op => QueryPlan {
411 operator: op.clone(),
412 children,
413 estimated_cost: plan.estimated_cost,
414 },
415 }
416 }
417}
418
419pub struct SparqlOptimizer {
425 rules: Vec<Box<dyn OptimizationPass>>,
426}
427
428impl SparqlOptimizer {
429 pub fn new() -> Self {
431 let rules: Vec<Box<dyn OptimizationPass>> = vec![
432 Box::new(FilterPushdownPass),
433 Box::new(ProjectionPruningPass),
434 Box::new(JoinReorderPass),
435 Box::new(ConstantFoldingPass),
436 Box::new(RedundantDistinctPass),
437 ];
438 SparqlOptimizer { rules }
439 }
440
441 pub fn optimize(&self, mut plan: QueryPlan) -> QueryPlan {
443 plan.recompute_cost();
445 for pass in &self.rules {
446 plan = pass.apply(plan);
447 plan.recompute_cost();
448 }
449 plan
450 }
451
452 pub fn rule_info(&self) -> Vec<OptimizerRule> {
454 vec![
455 OptimizerRule {
456 name: "FilterPushdown",
457 description: "Push filters below joins",
458 },
459 OptimizerRule {
460 name: "ProjectionPruning",
461 description: "Remove redundant projections",
462 },
463 OptimizerRule {
464 name: "JoinReorder",
465 description: "Reorder joins by cost ascending",
466 },
467 OptimizerRule {
468 name: "ConstantFolding",
469 description: "Fold constant expressions",
470 },
471 OptimizerRule {
472 name: "RedundantDistinct",
473 description: "Remove redundant DISTINCT",
474 },
475 ]
476 }
477}
478
479impl Default for SparqlOptimizer {
480 fn default() -> Self {
481 Self::new()
482 }
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488
489 fn scan(pattern: &str) -> QueryPlan {
490 let mut p = QueryPlan::new(PlanOperator::Scan {
491 pattern: pattern.to_string(),
492 });
493 p.estimated_cost = 100.0;
494 p
495 }
496
497 fn join(left: QueryPlan, right: QueryPlan) -> QueryPlan {
498 let mut p = QueryPlan::with_children(
499 PlanOperator::Join {
500 strategy: JoinStrategy::NestedLoop,
501 },
502 vec![left, right],
503 );
504 p.recompute_cost();
505 p
506 }
507
508 fn filter(expr: &str, child: QueryPlan) -> QueryPlan {
509 let mut p = QueryPlan::with_children(
510 PlanOperator::Filter {
511 expr: expr.to_string(),
512 },
513 vec![child],
514 );
515 p.recompute_cost();
516 p
517 }
518
519 fn project(vars: Vec<&str>, child: QueryPlan) -> QueryPlan {
520 let mut p = QueryPlan::with_children(
521 PlanOperator::Project {
522 vars: vars.into_iter().map(String::from).collect(),
523 },
524 vec![child],
525 );
526 p.recompute_cost();
527 p
528 }
529
530 fn distinct(child: QueryPlan) -> QueryPlan {
531 let mut p = QueryPlan::with_children(PlanOperator::Distinct, vec![child]);
532 p.recompute_cost();
533 p
534 }
535
536 #[test]
538 fn test_cost_scan() {
539 let p = scan("?s ?p ?o");
540 assert_eq!(estimate_cost(&p), 100.0);
541 }
542
543 #[test]
544 fn test_cost_join() {
545 let j = join(scan("?s ?p ?o"), scan("?s ?p2 ?o2"));
546 assert!((j.estimated_cost - 1000.0).abs() < 1.0);
547 }
548
549 #[test]
550 fn test_cost_filter() {
551 let mut f = filter("?s = 1", scan("?s ?p ?o"));
552 f.recompute_cost();
553 assert!((f.estimated_cost - 50.0).abs() < 1.0);
554 }
555
556 #[test]
557 fn test_cost_project() {
558 let mut p = project(vec!["s"], scan("?s ?p ?o"));
559 p.recompute_cost();
560 assert_eq!(p.estimated_cost, 100.0);
561 }
562
563 #[test]
564 fn test_cost_union() {
565 let u = QueryPlan::with_children(
566 PlanOperator::Union,
567 vec![scan("?a ?b ?c"), scan("?x ?y ?z")],
568 );
569 assert_eq!(estimate_cost(&u), 200.0);
570 }
571
572 #[test]
573 fn test_cost_distinct() {
574 let d = distinct(scan("?s ?p ?o"));
575 assert!((estimate_cost(&d) - 110.0).abs() < 0.01);
576 }
577
578 #[test]
579 fn test_cost_slice_with_limit() {
580 let mut s = QueryPlan::with_children(
581 PlanOperator::Slice {
582 offset: None,
583 limit: Some(10),
584 },
585 vec![scan("?s ?p ?o")],
586 );
587 s.recompute_cost();
588 assert!((s.estimated_cost - 1.0).abs() < 0.01);
590 }
591
592 #[test]
593 fn test_cost_slice_no_limit() {
594 let mut s = QueryPlan::with_children(
595 PlanOperator::Slice {
596 offset: Some(5),
597 limit: None,
598 },
599 vec![scan("?s ?p ?o")],
600 );
601 s.recompute_cost();
602 assert_eq!(s.estimated_cost, 100.0);
603 }
604
605 #[test]
606 fn test_cost_optional() {
607 let opt = QueryPlan::with_children(
608 PlanOperator::Optional,
609 vec![scan("?a ?b ?c"), scan("?x ?y ?z")],
610 );
611 assert!((estimate_cost(&opt) - 130.0).abs() < 0.01);
613 }
614
615 #[test]
617 fn test_filter_pushdown_through_join() {
618 let pass = FilterPushdownPass;
619 let j = join(scan("?s ?p ?o"), scan("?s ?p2 ?o2"));
620 let f = filter("?s = 1", j);
621 let result = pass.apply(f);
622 if let PlanOperator::Join { .. } = &result.operator {
624 let left = &result.children[0];
625 assert!(matches!(left.operator, PlanOperator::Filter { .. }));
626 } else {
627 panic!("Expected Join at top level after pushdown");
628 }
629 }
630
631 #[test]
632 fn test_filter_pushdown_non_join_child() {
633 let pass = FilterPushdownPass;
634 let f = filter("?x > 5", scan("?x ?y ?z"));
635 let result = pass.apply(f);
636 assert!(matches!(result.operator, PlanOperator::Filter { .. }));
638 }
639
640 #[test]
641 fn test_filter_pushdown_empty_children() {
642 let pass = FilterPushdownPass;
643 let f = QueryPlan::new(PlanOperator::Filter {
644 expr: "?x > 0".to_string(),
645 });
646 let result = pass.apply(f);
647 assert!(matches!(result.operator, PlanOperator::Filter { .. }));
648 }
649
650 #[test]
652 fn test_projection_pruning_removes_redundant() {
653 let pass = ProjectionPruningPass;
654 let child = scan("?s ?p ?o");
656 let p = project(vec!["s", "p", "o"], child);
657 let result = pass.apply(p);
658 assert!(matches!(result.operator, PlanOperator::Scan { .. }));
660 }
661
662 #[test]
663 fn test_projection_pruning_keeps_reducing() {
664 let pass = ProjectionPruningPass;
665 let child = scan("?s ?p ?o");
666 let p = project(vec!["s"], child);
667 let result = pass.apply(p);
668 assert!(matches!(result.operator, PlanOperator::Project { .. }));
670 }
671
672 #[test]
673 fn test_projection_pruning_no_children() {
674 let pass = ProjectionPruningPass;
675 let p = QueryPlan::new(PlanOperator::Project {
676 vars: vec!["s".to_string()],
677 });
678 let result = pass.apply(p);
679 assert!(matches!(result.operator, PlanOperator::Project { .. }));
680 }
681
682 #[test]
684 fn test_join_reorder_sorts_by_cost() {
685 let pass = JoinReorderPass;
686 let expensive = {
687 let mut p = scan("?a ?b ?c");
688 p.estimated_cost = 500.0;
689 p
690 };
691 let cheap = {
692 let mut p = scan("?x ?y ?z");
693 p.estimated_cost = 50.0;
694 p
695 };
696 let j = QueryPlan::with_children(
697 PlanOperator::Join {
698 strategy: JoinStrategy::HashJoin,
699 },
700 vec![expensive, cheap],
701 );
702 let result = pass.apply(j);
703 assert!(result.children[0].estimated_cost <= result.children[1].estimated_cost);
704 }
705
706 #[test]
707 fn test_join_reorder_already_sorted() {
708 let pass = JoinReorderPass;
709 let mut c1 = scan("?a ?b ?c");
710 c1.estimated_cost = 10.0;
711 let mut c2 = scan("?x ?y ?z");
712 c2.estimated_cost = 200.0;
713 let j = QueryPlan::with_children(
714 PlanOperator::Join {
715 strategy: JoinStrategy::MergeJoin,
716 },
717 vec![c1, c2],
718 );
719 let result = pass.apply(j);
720 assert!(result.children[0].estimated_cost <= result.children[1].estimated_cost);
721 }
722
723 #[test]
724 fn test_join_reorder_non_join_unchanged() {
725 let pass = JoinReorderPass;
726 let s = scan("?s ?p ?o");
727 let result = pass.apply(s);
728 assert!(matches!(result.operator, PlanOperator::Scan { .. }));
729 }
730
731 #[test]
733 fn test_constant_folding_true_removes_filter() {
734 let pass = ConstantFoldingPass;
735 let f = filter("true", scan("?s ?p ?o"));
736 let result = pass.apply(f);
737 assert!(matches!(result.operator, PlanOperator::Scan { .. }));
738 }
739
740 #[test]
741 fn test_constant_folding_false_returns_empty() {
742 let pass = ConstantFoldingPass;
743 let f = filter("false", scan("?s ?p ?o"));
744 let result = pass.apply(f);
745 if let PlanOperator::Scan { pattern } = &result.operator {
746 assert_eq!(pattern, "EMPTY");
747 } else {
748 panic!("Expected empty scan");
749 }
750 }
751
752 #[test]
753 fn test_constant_folding_tautology_1eq1() {
754 let pass = ConstantFoldingPass;
755 let f = filter("1=1", scan("?s ?p ?o"));
756 let result = pass.apply(f);
757 assert!(matches!(result.operator, PlanOperator::Scan { .. }));
758 }
759
760 #[test]
761 fn test_constant_folding_false_1eq0() {
762 let pass = ConstantFoldingPass;
763 let f = filter("1=0", scan("?s ?p ?o"));
764 let result = pass.apply(f);
765 if let PlanOperator::Scan { pattern } = &result.operator {
766 assert_eq!(pattern, "EMPTY");
767 } else {
768 panic!("Expected empty scan");
769 }
770 }
771
772 #[test]
773 fn test_constant_folding_non_constant_unchanged() {
774 let pass = ConstantFoldingPass;
775 let f = filter("?x > 5", scan("?s ?p ?o"));
776 let result = pass.apply(f);
777 assert!(matches!(result.operator, PlanOperator::Filter { .. }));
778 }
779
780 #[test]
781 fn test_constant_folding_uppercase_true() {
782 let pass = ConstantFoldingPass;
783 let f = filter("TRUE", scan("?s ?p ?o"));
784 let result = pass.apply(f);
785 assert!(matches!(result.operator, PlanOperator::Scan { .. }));
786 }
787
788 #[test]
790 fn test_redundant_distinct_scan_removed() {
791 let pass = RedundantDistinctPass;
792 let d = distinct(scan("?s ?p ?o"));
793 let result = pass.apply(d);
794 assert!(matches!(result.operator, PlanOperator::Scan { .. }));
795 }
796
797 #[test]
798 fn test_redundant_distinct_double_distinct() {
799 let pass = RedundantDistinctPass;
800 let d = distinct(distinct(scan("?s ?p ?o")));
801 let result = pass.apply(d);
802 assert!(matches!(
804 result.operator,
805 PlanOperator::Scan { .. } | PlanOperator::Distinct
806 ));
807 }
808
809 #[test]
810 fn test_redundant_distinct_over_join_kept() {
811 let pass = RedundantDistinctPass;
812 let j = join(scan("?s ?p ?o"), scan("?s ?p2 ?o2"));
813 let d = distinct(j);
814 let result = pass.apply(d);
815 assert!(matches!(result.operator, PlanOperator::Distinct));
816 }
817
818 #[test]
819 fn test_redundant_distinct_empty_children() {
820 let pass = RedundantDistinctPass;
821 let d = QueryPlan::new(PlanOperator::Distinct);
822 let result = pass.apply(d);
823 assert!(matches!(result.operator, PlanOperator::Distinct));
824 }
825
826 #[test]
828 fn test_optimizer_new() {
829 let opt = SparqlOptimizer::new();
830 assert_eq!(opt.rules.len(), 5);
831 }
832
833 #[test]
834 fn test_optimizer_default() {
835 let opt = SparqlOptimizer::default();
836 assert_eq!(opt.rules.len(), 5);
837 }
838
839 #[test]
840 fn test_optimizer_rule_info() {
841 let opt = SparqlOptimizer::new();
842 let rules = opt.rule_info();
843 assert_eq!(rules.len(), 5);
844 assert!(rules.iter().any(|r| r.name == "FilterPushdown"));
845 assert!(rules.iter().any(|r| r.name == "JoinReorder"));
846 }
847
848 #[test]
849 fn test_optimizer_chain_simple() {
850 let opt = SparqlOptimizer::new();
851 let plan = scan("?s ?p ?o");
852 let result = opt.optimize(plan);
853 assert!(matches!(result.operator, PlanOperator::Scan { .. }));
854 }
855
856 #[test]
857 fn test_optimizer_chain_filter_join() {
858 let opt = SparqlOptimizer::new();
859 let j = join(scan("?s ?p ?o"), scan("?s ?p2 ?o2"));
860 let f = filter("?s = 1", j);
861 let result = opt.optimize(f);
862 assert!(result.estimated_cost >= 0.0);
864 }
865
866 #[test]
867 fn test_optimizer_chain_constant_filter() {
868 let opt = SparqlOptimizer::new();
869 let f = filter("true", scan("?s ?p ?o"));
870 let result = opt.optimize(f);
871 assert!(matches!(result.operator, PlanOperator::Scan { .. }));
872 }
873
874 #[test]
875 fn test_optimizer_chain_redundant_distinct() {
876 let opt = SparqlOptimizer::new();
877 let d = distinct(scan("?s ?p ?o"));
878 let result = opt.optimize(d);
879 assert!(matches!(result.operator, PlanOperator::Scan { .. }));
880 }
881
882 #[test]
883 fn test_optimizer_empty_plan() {
884 let opt = SparqlOptimizer::new();
885 let plan = QueryPlan::new(PlanOperator::Union);
886 let result = opt.optimize(plan);
887 assert!(matches!(result.operator, PlanOperator::Union));
888 }
889
890 #[test]
891 fn test_optimizer_deeply_nested() {
892 let opt = SparqlOptimizer::new();
893 let s1 = scan("?a ?b ?c");
894 let s2 = scan("?d ?e ?f");
895 let s3 = scan("?g ?h ?i");
896 let j1 = join(s1, s2);
897 let j2 = join(j1, s3);
898 let f = filter("?a > 0", j2);
899 let d = distinct(f);
900 let result = opt.optimize(d);
901 assert!(result.estimated_cost >= 0.0);
902 }
903
904 #[test]
905 fn test_plan_recompute_cost() {
906 let mut j = join(scan("?s ?p ?o"), scan("?x ?y ?z"));
907 j.recompute_cost();
908 assert!((j.estimated_cost - 1000.0).abs() < 1.0);
909 }
910
911 #[test]
912 fn test_join_strategy_variants() {
913 let strategies = [
914 JoinStrategy::NestedLoop,
915 JoinStrategy::HashJoin,
916 JoinStrategy::MergeJoin,
917 ];
918 for strategy in &strategies {
919 let p = QueryPlan::with_children(
920 PlanOperator::Join {
921 strategy: strategy.clone(),
922 },
923 vec![scan("?a ?b ?c"), scan("?x ?y ?z")],
924 );
925 assert!(matches!(p.operator, PlanOperator::Join { .. }));
926 }
927 }
928}