1use anyhow::Result;
26use serde::{Deserialize, Serialize};
27use std::collections::{HashMap, HashSet};
28use std::fmt;
29
30use crate::planner::planning::types::{ExecutionPlan, ExecutionStep, StepType};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ExplainFormat {
35 Brief,
37 Detailed,
39 Json,
41 Tree,
43 CostAnalysis,
45}
46
47pub struct QueryPlanExplainer {
49 config: ExplainerConfig,
50}
51
52#[derive(Debug, Clone)]
54pub struct ExplainerConfig {
55 pub include_costs: bool,
57 pub include_services: bool,
59 pub include_suggestions: bool,
61 pub max_tree_depth: usize,
63 pub show_timing_estimates: bool,
65}
66
67impl Default for ExplainerConfig {
68 fn default() -> Self {
69 Self {
70 include_costs: true,
71 include_services: true,
72 include_suggestions: true,
73 max_tree_depth: 10,
74 show_timing_estimates: true,
75 }
76 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct PlanExplanation {
82 pub summary: String,
84 pub total_cost: u64,
86 pub step_count: usize,
88 pub service_count: usize,
90 pub steps: Vec<StepExplanation>,
92 pub service_dependencies: HashMap<String, Vec<String>>,
94 pub parallel_steps: Vec<Vec<String>>,
96 pub suggestions: Vec<OptimizationSuggestion>,
98 pub critical_path: Vec<String>,
100 pub estimated_duration_ms: u64,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct StepExplanation {
107 pub step_id: String,
109 pub step_type: String,
111 pub service_id: Option<String>,
113 pub operation: String,
115 pub estimated_cost: f64,
117 pub estimated_duration_ms: u64,
119 pub dependencies: Vec<String>,
121 pub can_parallelize: bool,
123 pub description: String,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct OptimizationSuggestion {
130 pub category: SuggestionCategory,
132 pub severity: SuggestionSeverity,
134 pub description: String,
136 pub affected_steps: Vec<String>,
138 pub potential_improvement: String,
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
144pub enum SuggestionCategory {
145 JoinOrdering,
147 ServiceSelection,
149 Caching,
151 Parallelization,
153 FilterPushdown,
155 DataTransfer,
157 IndexUsage,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
163pub enum SuggestionSeverity {
164 Low,
166 Medium,
168 High,
170 Critical,
172}
173
174impl QueryPlanExplainer {
175 pub fn new() -> Self {
177 Self {
178 config: ExplainerConfig::default(),
179 }
180 }
181
182 pub fn with_config(config: ExplainerConfig) -> Self {
184 Self { config }
185 }
186
187 pub fn explain_plan(&self, plan: &ExecutionPlan, format: ExplainFormat) -> Result<String> {
189 let explanation = self.analyze_plan(plan)?;
190
191 match format {
192 ExplainFormat::Brief => Ok(self.format_brief(&explanation)),
193 ExplainFormat::Detailed => Ok(self.format_detailed(&explanation)),
194 ExplainFormat::Json => Ok(serde_json::to_string_pretty(&explanation)?),
195 ExplainFormat::Tree => Ok(self.format_tree(&explanation)),
196 ExplainFormat::CostAnalysis => Ok(self.format_cost_analysis(&explanation)),
197 }
198 }
199
200 fn analyze_plan(&self, plan: &ExecutionPlan) -> Result<PlanExplanation> {
202 let steps = plan
203 .steps
204 .iter()
205 .map(|step| self.explain_step(step, plan))
206 .collect::<Vec<_>>();
207
208 let service_count = steps
209 .iter()
210 .filter_map(|s| s.service_id.as_ref())
211 .collect::<HashSet<_>>()
212 .len();
213
214 let service_dependencies = self.analyze_service_dependencies(&steps);
215 let parallel_steps = self.identify_parallel_steps(&steps);
216 let critical_path = self.compute_critical_path(&steps);
217 let suggestions = if self.config.include_suggestions {
218 self.generate_suggestions(plan, &steps)
219 } else {
220 Vec::new()
221 };
222
223 let total_cost = steps.iter().map(|s| s.estimated_cost as u64).sum();
224 let estimated_duration_ms = self.estimate_total_duration(&steps, &critical_path);
225
226 let summary = self.generate_summary(steps.len(), service_count, ¶llel_steps);
227
228 Ok(PlanExplanation {
229 summary,
230 total_cost,
231 step_count: steps.len(),
232 service_count,
233 steps,
234 service_dependencies,
235 parallel_steps,
236 suggestions,
237 critical_path,
238 estimated_duration_ms,
239 })
240 }
241
242 fn explain_step(&self, step: &ExecutionStep, _plan: &ExecutionPlan) -> StepExplanation {
244 let step_id = step.step_id.clone();
245 let step_type = format!("{:?}", step.step_type);
246 let service_id = step.service_id.clone();
247
248 let operation = match &step.step_type {
249 StepType::ServiceQuery => {
250 format!("Execute query on service: {}", step.query_fragment)
251 }
252 StepType::GraphQLQuery => {
253 format!("Execute GraphQL query: {}", step.query_fragment)
254 }
255 StepType::Join => "Join results from previous steps".to_string(),
256 StepType::Filter => "Apply filters".to_string(),
257 StepType::Aggregate => "Aggregate results".to_string(),
258 StepType::Sort => "Sort results".to_string(),
259 StepType::EntityResolution => "Resolve entities across services".to_string(),
260 StepType::ResultStitching => "Stitch results together".to_string(),
261 StepType::SchemaStitch => "Stitch schemas together".to_string(),
262 StepType::Union => "Union results from multiple sources".to_string(),
263 };
264
265 let estimated_cost = step.estimated_cost;
266 let estimated_duration_ms = self.estimate_step_duration(step);
267
268 let dependencies = step.dependencies.clone();
269 let can_parallelize = dependencies.is_empty() || dependencies.len() <= 1;
270
271 let description = self.generate_step_description(step);
272
273 StepExplanation {
274 step_id,
275 step_type,
276 service_id,
277 operation,
278 estimated_cost,
279 estimated_duration_ms,
280 dependencies,
281 can_parallelize,
282 description,
283 }
284 }
285
286 fn generate_step_description(&self, step: &ExecutionStep) -> String {
288 let mut desc = String::new();
289
290 match &step.step_type {
291 StepType::ServiceQuery | StepType::GraphQLQuery => {
292 if let Some(service_id) = &step.service_id {
293 desc.push_str(&format!("Query service '{}' ", service_id));
294 }
295 if step.query_fragment.len() > 100 {
296 desc.push_str(&format!(
297 "with query ({}... characters)",
298 step.query_fragment.len()
299 ));
300 } else {
301 desc.push_str(&format!("with query: {}", step.query_fragment));
302 }
303 }
304 StepType::Join => {
305 desc.push_str("Perform join operation ");
306 if !step.dependencies.is_empty() {
307 desc.push_str(&format!(
308 "combining results from steps {:?}",
309 step.dependencies
310 ));
311 }
312 }
313 StepType::Filter => {
314 desc.push_str("Apply filtering conditions to reduce result set");
315 }
316 StepType::Aggregate => {
317 desc.push_str("Aggregate data (COUNT, SUM, AVG, etc.)");
318 }
319 StepType::Sort => {
320 desc.push_str("Sort results by specified criteria");
321 }
322 StepType::EntityResolution => {
323 desc.push_str("Resolve entity references across different services");
324 }
325 StepType::ResultStitching => {
326 desc.push_str("Combine and stitch results from multiple sources");
327 }
328 StepType::SchemaStitch => {
329 desc.push_str("Stitch schemas together for federation");
330 }
331 StepType::Union => {
332 desc.push_str("Union results from multiple query branches");
333 }
334 }
335
336 desc
340 }
341
342 fn estimate_step_duration(&self, step: &ExecutionStep) -> u64 {
344 if !self.config.show_timing_estimates {
345 return 0;
346 }
347
348 let base_duration = match &step.step_type {
350 StepType::ServiceQuery => 50, StepType::GraphQLQuery => 40,
352 StepType::Join => 10,
353 StepType::Filter => 2,
354 StepType::Aggregate => 5,
355 StepType::Sort => 8,
356 StepType::EntityResolution => 15,
357 StepType::ResultStitching => 5,
358 StepType::SchemaStitch => 8,
359 StepType::Union => 3,
360 };
361
362 let cost_multiplier = 1.0 + (step.estimated_cost / 100.0).min(10.0);
364
365 (base_duration as f64 * cost_multiplier) as u64
366 }
367
368 fn analyze_service_dependencies(
370 &self,
371 steps: &[StepExplanation],
372 ) -> HashMap<String, Vec<String>> {
373 let mut dependencies: HashMap<String, HashSet<String>> = HashMap::new();
374
375 let step_map: HashMap<&String, &StepExplanation> =
377 steps.iter().map(|s| (&s.step_id, s)).collect();
378
379 for step in steps {
380 if let Some(service_id) = &step.service_id {
381 for dep_id in &step.dependencies {
383 if let Some(dep_step) = step_map.get(dep_id) {
384 if let Some(dep_service) = &dep_step.service_id {
385 if dep_service != service_id {
386 dependencies
387 .entry(service_id.clone())
388 .or_default()
389 .insert(dep_service.clone());
390 }
391 }
392 }
393 }
394 }
395 }
396
397 dependencies
398 .into_iter()
399 .map(|(k, v)| (k, v.into_iter().collect()))
400 .collect()
401 }
402
403 fn identify_parallel_steps(&self, steps: &[StepExplanation]) -> Vec<Vec<String>> {
405 let mut parallel_groups = Vec::new();
406 let mut processed = HashSet::new();
407
408 for step in steps {
409 if processed.contains(&step.step_id) {
410 continue;
411 }
412
413 if step.dependencies.is_empty() {
414 let mut group = vec![step.step_id.clone()];
416 processed.insert(step.step_id.clone());
417
418 for other_step in steps {
419 if other_step.step_id != step.step_id
420 && !processed.contains(&other_step.step_id)
421 && other_step.dependencies.is_empty()
422 {
423 group.push(other_step.step_id.clone());
424 processed.insert(other_step.step_id.clone());
425 }
426 }
427
428 if group.len() > 1 {
429 parallel_groups.push(group);
430 }
431 }
432 }
433
434 parallel_groups
435 }
436
437 fn compute_critical_path(&self, steps: &[StepExplanation]) -> Vec<String> {
439 if steps.is_empty() {
440 return Vec::new();
441 }
442
443 let mut path = Vec::new();
445 let current_id = steps.first().map(|s| s.step_id.clone());
446 if current_id.is_none() {
447 return path;
448 }
449
450 let mut current_id = current_id.unwrap();
451 let mut visited = HashSet::new();
452
453 path.push(current_id.clone());
454 visited.insert(current_id.clone());
455
456 loop {
458 let mut next_step: Option<&StepExplanation> = None;
459 let mut max_cost = 0.0;
460
461 for step in steps {
462 if !visited.contains(&step.step_id)
463 && step.dependencies.contains(¤t_id)
464 && step.estimated_cost > max_cost
465 {
466 max_cost = step.estimated_cost;
467 next_step = Some(step);
468 }
469 }
470
471 if let Some(step) = next_step {
472 path.push(step.step_id.clone());
473 visited.insert(step.step_id.clone());
474 current_id = step.step_id.clone();
475 } else {
476 break;
477 }
478 }
479
480 path
481 }
482
483 fn estimate_total_duration(&self, steps: &[StepExplanation], critical_path: &[String]) -> u64 {
485 let step_map: HashMap<&String, &StepExplanation> =
486 steps.iter().map(|s| (&s.step_id, s)).collect();
487
488 critical_path
489 .iter()
490 .filter_map(|id| step_map.get(id))
491 .map(|step| step.estimated_duration_ms)
492 .sum()
493 }
494
495 fn generate_summary(
497 &self,
498 step_count: usize,
499 service_count: usize,
500 parallel_steps: &[Vec<String>],
501 ) -> String {
502 format!(
503 "Federated query execution plan with {} steps across {} services. {} parallelization opportunities identified.",
504 step_count,
505 service_count,
506 parallel_steps.len()
507 )
508 }
509
510 fn generate_suggestions(
512 &self,
513 _plan: &ExecutionPlan,
514 steps: &[StepExplanation],
515 ) -> Vec<OptimizationSuggestion> {
516 let mut suggestions = Vec::new();
517
518 let sequential_queries: Vec<String> = steps
520 .iter()
521 .filter(|s| matches!(s.step_type.as_str(), "ServiceQuery" | "GraphQLQuery"))
522 .map(|s| s.step_id.clone())
523 .collect();
524
525 if sequential_queries.len() > 1 {
526 suggestions.push(OptimizationSuggestion {
527 category: SuggestionCategory::Parallelization,
528 severity: SuggestionSeverity::High,
529 description: format!(
530 "Found {} service queries that could potentially run in parallel",
531 sequential_queries.len()
532 ),
533 affected_steps: sequential_queries,
534 potential_improvement: "Reduce total execution time by up to 50%".to_string(),
535 });
536 }
537
538 for step in steps {
540 if step.estimated_cost > 100.0 {
541 suggestions.push(OptimizationSuggestion {
542 category: SuggestionCategory::Caching,
543 severity: SuggestionSeverity::Medium,
544 description: format!(
545 "Step {} has high cost ({:.0}) - consider caching",
546 step.step_id, step.estimated_cost
547 ),
548 affected_steps: vec![step.step_id.clone()],
549 potential_improvement: "Reduce repeated query overhead".to_string(),
550 });
551 }
552 }
553
554 let join_steps: Vec<_> = steps.iter().filter(|s| s.step_type == "Join").collect();
556
557 if join_steps.len() > 2 {
558 suggestions.push(OptimizationSuggestion {
559 category: SuggestionCategory::JoinOrdering,
560 severity: SuggestionSeverity::High,
561 description: "Multiple joins detected - verify join ordering is optimal"
562 .to_string(),
563 affected_steps: join_steps.iter().map(|s| s.step_id.clone()).collect(),
564 potential_improvement: "Optimize join order to process smaller result sets first"
565 .to_string(),
566 });
567 }
568
569 suggestions
570 }
571
572 fn format_brief(&self, explanation: &PlanExplanation) -> String {
574 format!(
575 "{}\n\nTotal Cost: {}\nSteps: {}\nServices: {}\nEstimated Duration: {}ms\nParallel Opportunities: {}",
576 explanation.summary,
577 explanation.total_cost,
578 explanation.step_count,
579 explanation.service_count,
580 explanation.estimated_duration_ms,
581 explanation.parallel_steps.len()
582 )
583 }
584
585 fn format_detailed(&self, explanation: &PlanExplanation) -> String {
587 let mut output = String::new();
588
589 output.push_str("=== QUERY EXECUTION PLAN ===\n\n");
590 output.push_str(&format!("{}\n\n", explanation.summary));
591
592 output.push_str(&format!(
593 "Total Estimated Cost: {}\n",
594 explanation.total_cost
595 ));
596 output.push_str(&format!(
597 "Total Estimated Duration: {}ms\n",
598 explanation.estimated_duration_ms
599 ));
600 output.push_str(&format!("Number of Steps: {}\n", explanation.step_count));
601 output.push_str(&format!(
602 "Services Involved: {}\n\n",
603 explanation.service_count
604 ));
605
606 output.push_str("--- EXECUTION STEPS ---\n\n");
607 for step in &explanation.steps {
608 output.push_str(&format!("Step {}: {}\n", step.step_id, step.step_type));
609 output.push_str(&format!(" {}\n", step.description));
610 if let Some(service_id) = &step.service_id {
611 output.push_str(&format!(" Service: {}\n", service_id));
612 }
613 output.push_str(&format!(" Cost: {:.2}\n", step.estimated_cost));
614 output.push_str(&format!(" Duration: {}ms\n", step.estimated_duration_ms));
615 if !step.dependencies.is_empty() {
616 output.push_str(&format!(" Depends on steps: {:?}\n", step.dependencies));
617 }
618 if step.can_parallelize {
619 output.push_str(" ✓ Can run in parallel\n");
620 }
621 output.push('\n');
622 }
623
624 if !explanation.parallel_steps.is_empty() {
625 output.push_str("--- PARALLELIZATION OPPORTUNITIES ---\n\n");
626 for (idx, group) in explanation.parallel_steps.iter().enumerate() {
627 output.push_str(&format!("Parallel Group {}: Steps {:?}\n", idx + 1, group));
628 }
629 output.push('\n');
630 }
631
632 if !explanation.critical_path.is_empty() {
633 output.push_str("--- CRITICAL PATH ---\n\n");
634 output.push_str(&format!("Steps: {:?}\n", explanation.critical_path));
635 output.push_str(&format!(
636 "Total Duration: {}ms\n\n",
637 explanation.estimated_duration_ms
638 ));
639 }
640
641 if !explanation.suggestions.is_empty() {
642 output.push_str("--- OPTIMIZATION SUGGESTIONS ---\n\n");
643 for (idx, suggestion) in explanation.suggestions.iter().enumerate() {
644 output.push_str(&format!(
645 "{}. [{:?}] {:?}: {}\n",
646 idx + 1,
647 suggestion.severity,
648 suggestion.category,
649 suggestion.description
650 ));
651 output.push_str(&format!(
652 " Potential: {}\n",
653 suggestion.potential_improvement
654 ));
655 output.push_str(&format!(
656 " Affects steps: {:?}\n\n",
657 suggestion.affected_steps
658 ));
659 }
660 }
661
662 output
663 }
664
665 fn format_tree(&self, explanation: &PlanExplanation) -> String {
667 let mut output = String::new();
668 output.push_str("Query Execution Tree:\n\n");
669
670 let mut printed = HashSet::new();
671
672 for step in &explanation.steps {
674 if step.dependencies.is_empty() && !printed.contains(&step.step_id) {
675 self.format_tree_recursive(
676 &explanation.steps,
677 &step.step_id,
678 "",
679 &mut output,
680 &mut printed,
681 0,
682 );
683 }
684 }
685
686 output
687 }
688
689 fn format_tree_recursive(
691 &self,
692 steps: &[StepExplanation],
693 step_id: &str,
694 prefix: &str,
695 output: &mut String,
696 printed: &mut HashSet<String>,
697 depth: usize,
698 ) {
699 if depth >= self.config.max_tree_depth || printed.contains(step_id) {
700 return;
701 }
702
703 if let Some(step) = steps.iter().find(|s| s.step_id == step_id) {
704 printed.insert(step_id.to_string());
705
706 output.push_str(&format!(
707 "{}├─ [{}] {} (cost: {:.2}, {}ms)\n",
708 prefix,
709 step.step_id,
710 step.step_type,
711 step.estimated_cost,
712 step.estimated_duration_ms
713 ));
714
715 if let Some(service_id) = &step.service_id {
716 output.push_str(&format!("{}│ Service: {}\n", prefix, service_id));
717 }
718
719 let dependents: Vec<String> = steps
721 .iter()
722 .filter(|s| s.dependencies.contains(&step.step_id))
723 .map(|s| s.step_id.clone())
724 .collect();
725
726 for (idx, dependent) in dependents.iter().enumerate() {
727 let is_last = idx == dependents.len() - 1;
728 let new_prefix = if is_last {
729 format!("{} ", prefix)
730 } else {
731 format!("{}│ ", prefix)
732 };
733 self.format_tree_recursive(
734 steps,
735 dependent,
736 &new_prefix,
737 output,
738 printed,
739 depth + 1,
740 );
741 }
742 }
743 }
744
745 fn format_cost_analysis(&self, explanation: &PlanExplanation) -> String {
747 let mut output = String::new();
748
749 output.push_str("=== COST ANALYSIS ===\n\n");
750 output.push_str(&format!(
751 "Total Estimated Cost: {}\n\n",
752 explanation.total_cost
753 ));
754
755 let mut cost_by_type: HashMap<String, f64> = HashMap::new();
757 for step in &explanation.steps {
758 *cost_by_type.entry(step.step_type.clone()).or_insert(0.0) += step.estimated_cost;
759 }
760
761 output.push_str("Cost Breakdown by Operation Type:\n");
762 let mut sorted_types: Vec<_> = cost_by_type.iter().collect();
763 sorted_types.sort_by(|a, b| b.1.partial_cmp(a.1).unwrap_or(std::cmp::Ordering::Equal));
764
765 for (step_type, cost) in sorted_types {
766 let percentage = (*cost / explanation.total_cost as f64) * 100.0;
767 output.push_str(&format!(
768 " {}: {:.2} ({:.1}%)\n",
769 step_type, cost, percentage
770 ));
771 }
772
773 output.push('\n');
774
775 let mut cost_by_service: HashMap<String, f64> = HashMap::new();
777 for step in &explanation.steps {
778 if let Some(service_id) = &step.service_id {
779 *cost_by_service.entry(service_id.clone()).or_insert(0.0) += step.estimated_cost;
780 }
781 }
782
783 if !cost_by_service.is_empty() {
784 output.push_str("Cost Breakdown by Service:\n");
785 let mut sorted_services: Vec<_> = cost_by_service.iter().collect();
786 sorted_services
787 .sort_by(|a, b| b.1.partial_cmp(a.1).unwrap_or(std::cmp::Ordering::Equal));
788
789 for (service_id, cost) in sorted_services {
790 let percentage = (*cost / explanation.total_cost as f64) * 100.0;
791 output.push_str(&format!(
792 " {}: {:.2} ({:.1}%)\n",
793 service_id, cost, percentage
794 ));
795 }
796 }
797
798 output
799 }
800}
801
802impl Default for QueryPlanExplainer {
803 fn default() -> Self {
804 Self::new()
805 }
806}
807
808impl fmt::Display for SuggestionSeverity {
809 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
810 match self {
811 Self::Low => write!(f, "LOW"),
812 Self::Medium => write!(f, "MEDIUM"),
813 Self::High => write!(f, "HIGH"),
814 Self::Critical => write!(f, "CRITICAL"),
815 }
816 }
817}
818
819#[cfg(test)]
820mod tests {
821 use super::*;
822 use crate::planner::planning::types::ExecutionStep;
823 use crate::planner::planning::types::StepType;
824
825 fn create_test_plan() -> ExecutionPlan {
826 use std::collections::HashMap;
827 use std::time::Duration;
828
829 ExecutionPlan {
830 query_id: "test_query_1".to_string(),
831 steps: vec![
832 ExecutionStep {
833 step_id: "step_0".to_string(),
834 step_type: StepType::ServiceQuery,
835 service_id: Some("service1".to_string()),
836 service_url: Some("http://service1.example.com/sparql".to_string()),
837 auth_config: None,
838 query_fragment: "SELECT ?s WHERE { ?s rdf:type foaf:Person }".to_string(),
839 dependencies: vec![],
840 estimated_cost: 100.0,
841 timeout: Duration::from_secs(30),
842 retry_config: None,
843 },
844 ExecutionStep {
845 step_id: "step_1".to_string(),
846 step_type: StepType::ServiceQuery,
847 service_id: Some("service2".to_string()),
848 service_url: Some("http://service2.example.com/sparql".to_string()),
849 auth_config: None,
850 query_fragment: "SELECT ?name WHERE { ?s foaf:name ?name }".to_string(),
851 dependencies: vec![],
852 estimated_cost: 80.0,
853 timeout: Duration::from_secs(30),
854 retry_config: None,
855 },
856 ExecutionStep {
857 step_id: "step_2".to_string(),
858 step_type: StepType::Join,
859 service_id: None,
860 service_url: None,
861 auth_config: None,
862 query_fragment: String::new(),
863 dependencies: vec!["step_0".to_string(), "step_1".to_string()],
864 estimated_cost: 50.0,
865 timeout: Duration::from_secs(30),
866 retry_config: None,
867 },
868 ],
869 estimated_total_cost: 230.0,
870 max_parallelism: 2,
871 planning_time: Duration::from_millis(100),
872 cache_key: Some("test_cache_key".to_string()),
873 metadata: HashMap::new(),
874 parallelizable_steps: vec![vec!["step_0".to_string(), "step_1".to_string()]],
875 }
876 }
877
878 #[test]
879 fn test_explainer_creation() {
880 let explainer = QueryPlanExplainer::new();
881 assert!(explainer.config.include_costs);
882 assert!(explainer.config.include_services);
883 }
884
885 #[test]
886 fn test_explain_plan_brief() {
887 let explainer = QueryPlanExplainer::new();
888 let plan = create_test_plan();
889
890 let explanation = explainer.explain_plan(&plan, ExplainFormat::Brief);
891 assert!(explanation.is_ok());
892
893 let output = explanation.unwrap();
894 assert!(output.contains("Total Cost"));
895 assert!(output.contains("Steps: 3"));
896 assert!(output.contains("Services: 2"));
897 }
898
899 #[test]
900 fn test_explain_plan_detailed() {
901 let explainer = QueryPlanExplainer::new();
902 let plan = create_test_plan();
903
904 let explanation = explainer.explain_plan(&plan, ExplainFormat::Detailed);
905 assert!(explanation.is_ok());
906
907 let output = explanation.unwrap();
908 assert!(output.contains("EXECUTION STEPS"));
909 assert!(output.contains("ServiceQuery"));
910 assert!(output.contains("Join"));
911 }
912
913 #[test]
914 fn test_explain_plan_json() {
915 let explainer = QueryPlanExplainer::new();
916 let plan = create_test_plan();
917
918 let explanation = explainer.explain_plan(&plan, ExplainFormat::Json);
919 assert!(explanation.is_ok());
920
921 let output = explanation.unwrap();
922 let parsed: Result<PlanExplanation, _> = serde_json::from_str(&output);
923 assert!(parsed.is_ok());
924 }
925
926 #[test]
927 fn test_explain_plan_tree() {
928 let explainer = QueryPlanExplainer::new();
929 let plan = create_test_plan();
930
931 let explanation = explainer.explain_plan(&plan, ExplainFormat::Tree);
932 assert!(explanation.is_ok());
933
934 let output = explanation.unwrap();
935 assert!(output.contains("Query Execution Tree"));
936 assert!(output.contains("├─"));
937 }
938
939 #[test]
940 fn test_explain_plan_cost_analysis() {
941 let explainer = QueryPlanExplainer::new();
942 let plan = create_test_plan();
943
944 let explanation = explainer.explain_plan(&plan, ExplainFormat::CostAnalysis);
945 assert!(explanation.is_ok());
946
947 let output = explanation.unwrap();
948 assert!(output.contains("COST ANALYSIS"));
949 assert!(output.contains("Cost Breakdown"));
950 }
951
952 #[test]
953 fn test_parallel_step_identification() {
954 let explainer = QueryPlanExplainer::new();
955 let plan = create_test_plan();
956
957 let explanation = explainer.analyze_plan(&plan).unwrap();
958 assert!(!explanation.parallel_steps.is_empty());
959 assert_eq!(
960 explanation.parallel_steps[0],
961 vec!["step_0".to_string(), "step_1".to_string()]
962 );
963 }
964
965 #[test]
966 fn test_optimization_suggestions() {
967 let explainer = QueryPlanExplainer::new();
968 let plan = create_test_plan();
969
970 let explanation = explainer.analyze_plan(&plan).unwrap();
971 assert!(!explanation.suggestions.is_empty());
972
973 let has_parallel_suggestion = explanation
975 .suggestions
976 .iter()
977 .any(|s| s.category == SuggestionCategory::Parallelization);
978 assert!(has_parallel_suggestion);
979 }
980
981 #[test]
982 fn test_critical_path() {
983 let explainer = QueryPlanExplainer::new();
984 let plan = create_test_plan();
985
986 let explanation = explainer.analyze_plan(&plan).unwrap();
987 assert!(!explanation.critical_path.is_empty());
988
989 assert!(explanation.critical_path.contains(&"step_2".to_string()));
991 }
992
993 #[test]
994 fn test_service_dependencies() {
995 let explainer = QueryPlanExplainer::new();
996 let plan = create_test_plan();
997
998 let explanation = explainer.analyze_plan(&plan).unwrap();
999 assert_eq!(explanation.service_count, 2);
1001 }
1002
1003 #[test]
1004 fn test_estimated_duration() {
1005 let explainer = QueryPlanExplainer::new();
1006 let plan = create_test_plan();
1007
1008 let explanation = explainer.analyze_plan(&plan).unwrap();
1009 assert!(explanation.estimated_duration_ms > 0);
1010 }
1011}