Skip to main content

oxirs_gql/
query_plan_visualizer.rs

1//! Query Plan Visualization
2//!
3//! This module provides visualization and analysis tools for GraphQL query execution plans.
4//! It helps developers understand how queries are executed, identify bottlenecks, and optimize performance.
5//!
6//! # Features
7//!
8//! - **Visual Query Plans**: Generate visual representations of execution plans
9//! - **Multiple Formats**: DOT (Graphviz), Mermaid, ASCII tree, JSON
10//! - **Execution Timeline**: Visualize execution order and timing
11//! - **Cost Analysis**: Display estimated costs for each operation
12//! - **Dependency Graphs**: Show field dependencies and parallelization opportunities
13//! - **Performance Metrics**: Integration with actual execution metrics
14//!
15//! # Example
16//!
17//! ```rust,ignore
18//! use oxirs_gql::query_plan_visualizer::{QueryPlanVisualizer, VisualizationFormat};
19//!
20//! let visualizer = QueryPlanVisualizer::new();
21//! let plan = /* ... execution plan ... */;
22//!
23//! // Generate Graphviz DOT format
24//! let dot = visualizer.visualize(&plan, VisualizationFormat::Dot)?;
25//!
26//! // Generate Mermaid diagram
27//! let mermaid = visualizer.visualize(&plan, VisualizationFormat::Mermaid)?;
28//!
29//! // Generate ASCII tree
30//! let tree = visualizer.visualize(&plan, VisualizationFormat::AsciiTree)?;
31//! ```
32
33use serde::{Deserialize, Serialize};
34use std::collections::HashMap;
35use std::fmt::Write as FmtWrite;
36
37/// Visualization format
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39pub enum VisualizationFormat {
40    /// Graphviz DOT format
41    Dot,
42    /// Mermaid diagram
43    Mermaid,
44    /// ASCII tree
45    AsciiTree,
46    /// JSON representation
47    Json,
48    /// HTML with interactive visualization
49    Html,
50}
51
52/// Query plan node representing a single operation
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct PlanNode {
55    /// Unique node ID
56    pub id: String,
57    /// Node type (e.g., "Field", "Object", "List", "Defer", "Stream")
58    pub node_type: String,
59    /// Display name
60    pub name: String,
61    /// Parent node ID
62    pub parent_id: Option<String>,
63    /// Child node IDs
64    pub children: Vec<String>,
65    /// Estimated cost
66    pub estimated_cost: f64,
67    /// Actual execution time (if available)
68    pub execution_time_ms: Option<u64>,
69    /// Additional metadata
70    pub metadata: HashMap<String, serde_json::Value>,
71    /// Whether this can be executed in parallel with siblings
72    pub parallel_execution: bool,
73    /// Dependencies (node IDs that must complete first)
74    pub dependencies: Vec<String>,
75}
76
77impl PlanNode {
78    /// Create a new plan node
79    pub fn new(id: String, node_type: String, name: String) -> Self {
80        Self {
81            id,
82            node_type,
83            name,
84            parent_id: None,
85            children: Vec::new(),
86            estimated_cost: 1.0,
87            execution_time_ms: None,
88            metadata: HashMap::new(),
89            parallel_execution: true,
90            dependencies: Vec::new(),
91        }
92    }
93
94    /// Set parent
95    pub fn with_parent(mut self, parent_id: String) -> Self {
96        self.parent_id = Some(parent_id);
97        self
98    }
99
100    /// Add child
101    pub fn with_child(mut self, child_id: String) -> Self {
102        self.children.push(child_id);
103        self
104    }
105
106    /// Set estimated cost
107    pub fn with_cost(mut self, cost: f64) -> Self {
108        self.estimated_cost = cost;
109        self
110    }
111
112    /// Set execution time
113    pub fn with_execution_time(mut self, time_ms: u64) -> Self {
114        self.execution_time_ms = Some(time_ms);
115        self
116    }
117
118    /// Add metadata
119    pub fn with_metadata(mut self, key: String, value: serde_json::Value) -> Self {
120        self.metadata.insert(key, value);
121        self
122    }
123
124    /// Set parallel execution capability
125    pub fn with_parallel(mut self, parallel: bool) -> Self {
126        self.parallel_execution = parallel;
127        self
128    }
129
130    /// Add dependency
131    pub fn with_dependency(mut self, dep_id: String) -> Self {
132        self.dependencies.push(dep_id);
133        self
134    }
135}
136
137/// Query execution plan
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct QueryPlan {
140    /// Root node ID
141    pub root_id: String,
142    /// All nodes in the plan
143    pub nodes: HashMap<String, PlanNode>,
144    /// Query text
145    pub query: String,
146    /// Total estimated cost
147    pub total_cost: f64,
148    /// Total execution time (if available)
149    pub total_execution_time_ms: Option<u64>,
150}
151
152impl QueryPlan {
153    /// Create a new query plan
154    pub fn new(root_id: String, query: String) -> Self {
155        Self {
156            root_id,
157            nodes: HashMap::new(),
158            query,
159            total_cost: 0.0,
160            total_execution_time_ms: None,
161        }
162    }
163
164    /// Add a node to the plan
165    pub fn add_node(&mut self, node: PlanNode) {
166        self.total_cost += node.estimated_cost;
167        self.nodes.insert(node.id.clone(), node);
168    }
169
170    /// Get root node
171    pub fn root_node(&self) -> Option<&PlanNode> {
172        self.nodes.get(&self.root_id)
173    }
174
175    /// Get node by ID
176    pub fn get_node(&self, id: &str) -> Option<&PlanNode> {
177        self.nodes.get(id)
178    }
179
180    /// Get all leaf nodes (nodes with no children)
181    pub fn leaf_nodes(&self) -> Vec<&PlanNode> {
182        self.nodes
183            .values()
184            .filter(|node| node.children.is_empty())
185            .collect()
186    }
187
188    /// Calculate execution levels (depth from root)
189    pub fn execution_levels(&self) -> HashMap<String, usize> {
190        let mut levels = HashMap::new();
191        let mut queue = vec![(self.root_id.clone(), 0)];
192
193        while let Some((node_id, level)) = queue.pop() {
194            levels.insert(node_id.clone(), level);
195
196            if let Some(node) = self.nodes.get(&node_id) {
197                for child_id in &node.children {
198                    queue.push((child_id.clone(), level + 1));
199                }
200            }
201        }
202
203        levels
204    }
205
206    /// Get nodes that can be executed in parallel at each level
207    pub fn parallel_execution_groups(&self) -> Vec<Vec<String>> {
208        let levels = self.execution_levels();
209        let max_level = levels.values().max().copied().unwrap_or(0);
210
211        let mut groups = vec![Vec::new(); max_level + 1];
212
213        for (node_id, level) in levels {
214            if let Some(node) = self.nodes.get(&node_id) {
215                if node.parallel_execution && node.dependencies.is_empty() {
216                    groups[level].push(node_id);
217                }
218            }
219        }
220
221        groups
222    }
223
224    /// Set total execution time
225    pub fn set_total_execution_time(&mut self, time_ms: u64) {
226        self.total_execution_time_ms = Some(time_ms);
227    }
228}
229
230/// Query plan visualizer
231pub struct QueryPlanVisualizer {
232    /// Show costs in visualization
233    pub show_costs: bool,
234    /// Show execution times in visualization
235    pub show_execution_times: bool,
236    /// Show metadata in visualization
237    pub show_metadata: bool,
238    /// Color scheme for different node types
239    pub colors: HashMap<String, String>,
240}
241
242impl QueryPlanVisualizer {
243    /// Create a new visualizer with default settings
244    pub fn new() -> Self {
245        let mut colors = HashMap::new();
246        colors.insert("Field".to_string(), "#87CEEB".to_string()); // Sky blue
247        colors.insert("Object".to_string(), "#90EE90".to_string()); // Light green
248        colors.insert("List".to_string(), "#FFB6C1".to_string()); // Light pink
249        colors.insert("Defer".to_string(), "#FFD700".to_string()); // Gold
250        colors.insert("Stream".to_string(), "#FF6347".to_string()); // Tomato
251
252        Self {
253            show_costs: true,
254            show_execution_times: true,
255            show_metadata: false,
256            colors,
257        }
258    }
259
260    /// Enable cost display
261    pub fn with_costs(mut self, show: bool) -> Self {
262        self.show_costs = show;
263        self
264    }
265
266    /// Enable execution time display
267    pub fn with_execution_times(mut self, show: bool) -> Self {
268        self.show_execution_times = show;
269        self
270    }
271
272    /// Enable metadata display
273    pub fn with_metadata(mut self, show: bool) -> Self {
274        self.show_metadata = show;
275        self
276    }
277
278    /// Visualize a query plan
279    pub fn visualize(
280        &self,
281        plan: &QueryPlan,
282        format: VisualizationFormat,
283    ) -> Result<String, VisualizationError> {
284        match format {
285            VisualizationFormat::Dot => self.visualize_dot(plan),
286            VisualizationFormat::Mermaid => self.visualize_mermaid(plan),
287            VisualizationFormat::AsciiTree => self.visualize_ascii_tree(plan),
288            VisualizationFormat::Json => self.visualize_json(plan),
289            VisualizationFormat::Html => self.visualize_html(plan),
290        }
291    }
292
293    /// Generate Graphviz DOT format
294    fn visualize_dot(&self, plan: &QueryPlan) -> Result<String, VisualizationError> {
295        let mut output = String::new();
296        writeln!(output, "digraph QueryPlan {{")
297            .map_err(|e| VisualizationError::FormatError(e.to_string()))?;
298        writeln!(output, "  rankdir=TB;")
299            .map_err(|e| VisualizationError::FormatError(e.to_string()))?;
300        writeln!(output, "  node [shape=box, style=filled];")
301            .map_err(|e| VisualizationError::FormatError(e.to_string()))?;
302
303        // Add nodes
304        for (id, node) in &plan.nodes {
305            let default_color = "#FFFFFF".to_string();
306            let color = self.colors.get(&node.node_type).unwrap_or(&default_color);
307
308            let mut label = node.name.clone();
309            if self.show_costs {
310                label.push_str(&format!("\\nCost: {:.2}", node.estimated_cost));
311            }
312            if self.show_execution_times {
313                if let Some(time) = node.execution_time_ms {
314                    label.push_str(&format!("\\nTime: {}ms", time));
315                }
316            }
317
318            writeln!(
319                output,
320                "  \"{}\" [label=\"{}\", fillcolor=\"{}\"];",
321                id, label, color
322            )
323            .map_err(|e| VisualizationError::FormatError(e.to_string()))?;
324        }
325
326        // Add edges
327        for node in plan.nodes.values() {
328            for child_id in &node.children {
329                writeln!(output, "  \"{}\" -> \"{}\";", node.id, child_id)
330                    .map_err(|e| VisualizationError::FormatError(e.to_string()))?;
331            }
332
333            // Add dependency edges (dashed)
334            for dep_id in &node.dependencies {
335                writeln!(
336                    output,
337                    "  \"{}\" -> \"{}\" [style=dashed, color=red];",
338                    dep_id, node.id
339                )
340                .map_err(|e| VisualizationError::FormatError(e.to_string()))?;
341            }
342        }
343
344        writeln!(output, "}}").map_err(|e| VisualizationError::FormatError(e.to_string()))?;
345        Ok(output)
346    }
347
348    /// Generate Mermaid diagram
349    fn visualize_mermaid(&self, plan: &QueryPlan) -> Result<String, VisualizationError> {
350        let mut output = String::new();
351        writeln!(output, "graph TD").map_err(|e| VisualizationError::FormatError(e.to_string()))?;
352
353        // Add nodes
354        for (id, node) in &plan.nodes {
355            let mut label = node.name.clone();
356            if self.show_costs {
357                label.push_str(&format!("<br/>Cost: {:.2}", node.estimated_cost));
358            }
359            if self.show_execution_times {
360                if let Some(time) = node.execution_time_ms {
361                    label.push_str(&format!("<br/>Time: {}ms", time));
362                }
363            }
364
365            writeln!(output, "  {}[\"{}\"]", id.replace('-', "_"), label)
366                .map_err(|e| VisualizationError::FormatError(e.to_string()))?;
367        }
368
369        // Add edges
370        for node in plan.nodes.values() {
371            for child_id in &node.children {
372                writeln!(
373                    output,
374                    "  {} --> {}",
375                    node.id.replace('-', "_"),
376                    child_id.replace('-', "_")
377                )
378                .map_err(|e| VisualizationError::FormatError(e.to_string()))?;
379            }
380
381            // Add dependency edges
382            for dep_id in &node.dependencies {
383                writeln!(
384                    output,
385                    "  {} -.-> {}",
386                    dep_id.replace('-', "_"),
387                    node.id.replace('-', "_")
388                )
389                .map_err(|e| VisualizationError::FormatError(e.to_string()))?;
390            }
391        }
392
393        Ok(output)
394    }
395
396    /// Generate ASCII tree
397    fn visualize_ascii_tree(&self, plan: &QueryPlan) -> Result<String, VisualizationError> {
398        let mut output = String::new();
399        writeln!(output, "Query Plan:")
400            .map_err(|e| VisualizationError::FormatError(e.to_string()))?;
401        writeln!(output, "Total Cost: {:.2}", plan.total_cost)
402            .map_err(|e| VisualizationError::FormatError(e.to_string()))?;
403
404        if let Some(time) = plan.total_execution_time_ms {
405            writeln!(output, "Total Time: {}ms", time)
406                .map_err(|e| VisualizationError::FormatError(e.to_string()))?;
407        }
408
409        writeln!(output).map_err(|e| VisualizationError::FormatError(e.to_string()))?;
410
411        if let Some(root) = plan.root_node() {
412            self.print_tree_node(plan, root, "", true, &mut output)?;
413        }
414
415        Ok(output)
416    }
417
418    fn print_tree_node(
419        &self,
420        plan: &QueryPlan,
421        node: &PlanNode,
422        prefix: &str,
423        is_last: bool,
424        output: &mut String,
425    ) -> Result<(), VisualizationError> {
426        let connector = if is_last { "└── " } else { "├── " };
427        let mut line = format!("{}{}{}", prefix, connector, node.name);
428
429        if self.show_costs {
430            line.push_str(&format!(" (cost: {:.2}", node.estimated_cost));
431            if self.show_execution_times {
432                if let Some(time) = node.execution_time_ms {
433                    line.push_str(&format!(", time: {}ms", time));
434                }
435            }
436            line.push(')');
437        } else if self.show_execution_times {
438            if let Some(time) = node.execution_time_ms {
439                line.push_str(&format!(" (time: {}ms)", time));
440            }
441        }
442
443        writeln!(output, "{}", line).map_err(|e| VisualizationError::FormatError(e.to_string()))?;
444
445        let child_prefix = format!("{}{}   ", prefix, if is_last { " " } else { "│" });
446
447        for (i, child_id) in node.children.iter().enumerate() {
448            if let Some(child) = plan.get_node(child_id) {
449                let is_last_child = i == node.children.len() - 1;
450                self.print_tree_node(plan, child, &child_prefix, is_last_child, output)?;
451            }
452        }
453
454        Ok(())
455    }
456
457    /// Generate JSON representation
458    fn visualize_json(&self, plan: &QueryPlan) -> Result<String, VisualizationError> {
459        serde_json::to_string_pretty(plan)
460            .map_err(|e| VisualizationError::FormatError(e.to_string()))
461    }
462
463    /// Generate HTML with interactive visualization
464    fn visualize_html(&self, plan: &QueryPlan) -> Result<String, VisualizationError> {
465        let json = self.visualize_json(plan)?;
466
467        Ok(format!(
468            r#"<!DOCTYPE html>
469<html>
470<head>
471    <title>Query Plan Visualization</title>
472    <style>
473        body {{ font-family: Arial, sans-serif; margin: 20px; }}
474        .node {{ border: 1px solid #ccc; padding: 10px; margin: 5px; border-radius: 5px; }}
475        .node-Field {{ background-color: #87CEEB; }}
476        .node-Object {{ background-color: #90EE90; }}
477        .node-List {{ background-color: #FFB6C1; }}
478        .node-Defer {{ background-color: #FFD700; }}
479        .node-Stream {{ background-color: #FF6347; }}
480        .metadata {{ font-size: 0.9em; color: #666; }}
481        pre {{ background-color: #f4f4f4; padding: 10px; border-radius: 5px; }}
482    </style>
483</head>
484<body>
485    <h1>Query Plan Visualization</h1>
486    <div id="visualization"></div>
487    <h2>JSON Representation</h2>
488    <pre>{}</pre>
489    <script>
490        const plan = {};
491        // Interactive visualization would go here
492        console.log('Query plan:', plan);
493    </script>
494</body>
495</html>"#,
496            json, json
497        ))
498    }
499
500    /// Generate execution timeline
501    pub fn generate_timeline(&self, plan: &QueryPlan) -> String {
502        let mut output = String::new();
503        writeln!(&mut output, "Execution Timeline:").expect("writing to String should not fail");
504        writeln!(&mut output, "==================").expect("writing to String should not fail");
505
506        let levels = plan.execution_levels();
507        let max_level = levels.values().max().copied().unwrap_or(0);
508
509        for level in 0..=max_level {
510            let nodes_at_level: Vec<_> = plan
511                .nodes
512                .iter()
513                .filter(|(id, _)| levels.get(*id).copied() == Some(level))
514                .collect();
515
516            if !nodes_at_level.is_empty() {
517                writeln!(&mut output, "\nLevel {}:", level)
518                    .expect("writing to String should not fail");
519                for (_id, node) in nodes_at_level {
520                    let time_str = node
521                        .execution_time_ms
522                        .map(|t| format!(" ({}ms)", t))
523                        .unwrap_or_default();
524                    let parallel = if node.parallel_execution {
525                        " [parallel]"
526                    } else {
527                        ""
528                    };
529                    writeln!(
530                        &mut output,
531                        "  - {} [{}]{}{}",
532                        node.name, node.node_type, time_str, parallel
533                    )
534                    .expect("writing to String should not fail");
535                }
536            }
537        }
538
539        output
540    }
541
542    /// Generate cost breakdown
543    pub fn generate_cost_breakdown(&self, plan: &QueryPlan) -> String {
544        let mut output = String::new();
545        writeln!(&mut output, "Cost Breakdown:").expect("writing to String should not fail");
546        writeln!(&mut output, "===============").expect("writing to String should not fail");
547        writeln!(&mut output, "Total Cost: {:.2}\n", plan.total_cost)
548            .expect("writing to String should not fail");
549
550        let mut nodes: Vec<_> = plan.nodes.values().collect();
551        nodes.sort_by(|a, b| {
552            b.estimated_cost
553                .partial_cmp(&a.estimated_cost)
554                .unwrap_or(std::cmp::Ordering::Equal)
555        });
556
557        for node in nodes {
558            let percentage = (node.estimated_cost / plan.total_cost) * 100.0;
559            writeln!(
560                &mut output,
561                "{:<30} {:>8.2} ({:>5.1}%)",
562                node.name, node.estimated_cost, percentage
563            )
564            .expect("writing to String should not fail");
565        }
566
567        output
568    }
569}
570
571impl Default for QueryPlanVisualizer {
572    fn default() -> Self {
573        Self::new()
574    }
575}
576
577/// Errors that can occur during visualization
578#[derive(Debug, thiserror::Error)]
579pub enum VisualizationError {
580    /// Format error
581    #[error("Formatting error: {0}")]
582    FormatError(String),
583
584    /// Invalid plan structure
585    #[error("Invalid plan structure: {0}")]
586    InvalidPlan(String),
587
588    /// Serialization error
589    #[error("Serialization error: {0}")]
590    SerializationError(String),
591}
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596
597    fn create_sample_plan() -> QueryPlan {
598        let mut plan = QueryPlan::new("root".to_string(), "{ user { posts } }".to_string());
599
600        let root = PlanNode::new(
601            "root".to_string(),
602            "Object".to_string(),
603            "Query".to_string(),
604        )
605        .with_cost(10.0)
606        .with_child("user".to_string());
607
608        let user = PlanNode::new("user".to_string(), "Field".to_string(), "user".to_string())
609            .with_parent("root".to_string())
610            .with_cost(5.0)
611            .with_child("posts".to_string())
612            .with_execution_time(15);
613
614        let posts = PlanNode::new("posts".to_string(), "List".to_string(), "posts".to_string())
615            .with_parent("user".to_string())
616            .with_cost(20.0)
617            .with_execution_time(45);
618
619        plan.add_node(root);
620        plan.add_node(user);
621        plan.add_node(posts);
622        plan.set_total_execution_time(60);
623
624        plan
625    }
626
627    #[test]
628    fn test_plan_node_creation() {
629        let node = PlanNode::new(
630            "test".to_string(),
631            "Field".to_string(),
632            "testField".to_string(),
633        )
634        .with_cost(5.0)
635        .with_execution_time(10)
636        .with_parallel(true)
637        .with_metadata("key".to_string(), serde_json::json!("value"));
638
639        assert_eq!(node.id, "test");
640        assert_eq!(node.node_type, "Field");
641        assert_eq!(node.name, "testField");
642        assert_eq!(node.estimated_cost, 5.0);
643        assert_eq!(node.execution_time_ms, Some(10));
644        assert!(node.parallel_execution);
645        assert_eq!(node.metadata.len(), 1);
646    }
647
648    #[test]
649    fn test_query_plan_creation() {
650        let plan = QueryPlan::new("root".to_string(), "{ test }".to_string());
651
652        assert_eq!(plan.root_id, "root");
653        assert_eq!(plan.query, "{ test }");
654        assert_eq!(plan.total_cost, 0.0);
655        assert!(plan.nodes.is_empty());
656    }
657
658    #[test]
659    fn test_add_node_to_plan() {
660        let mut plan = QueryPlan::new("root".to_string(), "{ test }".to_string());
661        let node = PlanNode::new("test".to_string(), "Field".to_string(), "test".to_string())
662            .with_cost(5.0);
663
664        plan.add_node(node);
665
666        assert_eq!(plan.nodes.len(), 1);
667        assert_eq!(plan.total_cost, 5.0);
668    }
669
670    #[test]
671    fn test_root_node() {
672        let plan = create_sample_plan();
673        let root = plan.root_node();
674
675        assert!(root.is_some());
676        assert_eq!(root.expect("should succeed").id, "root");
677    }
678
679    #[test]
680    fn test_leaf_nodes() {
681        let plan = create_sample_plan();
682        let leaves = plan.leaf_nodes();
683
684        assert_eq!(leaves.len(), 1);
685        assert_eq!(leaves[0].id, "posts");
686    }
687
688    #[test]
689    fn test_execution_levels() {
690        let plan = create_sample_plan();
691        let levels = plan.execution_levels();
692
693        assert_eq!(levels.get("root"), Some(&0));
694        assert_eq!(levels.get("user"), Some(&1));
695        assert_eq!(levels.get("posts"), Some(&2));
696    }
697
698    #[test]
699    fn test_parallel_execution_groups() {
700        let mut plan = QueryPlan::new("root".to_string(), "{ test }".to_string());
701
702        let root = PlanNode::new(
703            "root".to_string(),
704            "Object".to_string(),
705            "Query".to_string(),
706        )
707        .with_child("field1".to_string())
708        .with_child("field2".to_string());
709
710        let field1 = PlanNode::new(
711            "field1".to_string(),
712            "Field".to_string(),
713            "field1".to_string(),
714        )
715        .with_parent("root".to_string())
716        .with_parallel(true);
717
718        let field2 = PlanNode::new(
719            "field2".to_string(),
720            "Field".to_string(),
721            "field2".to_string(),
722        )
723        .with_parent("root".to_string())
724        .with_parallel(true);
725
726        plan.add_node(root);
727        plan.add_node(field1);
728        plan.add_node(field2);
729
730        let groups = plan.parallel_execution_groups();
731        assert!(groups[1].len() == 2); // Both fields at level 1 can execute in parallel
732    }
733
734    #[test]
735    fn test_visualizer_creation() {
736        let visualizer = QueryPlanVisualizer::new();
737
738        assert!(visualizer.show_costs);
739        assert!(visualizer.show_execution_times);
740        assert!(!visualizer.show_metadata);
741        assert!(!visualizer.colors.is_empty());
742    }
743
744    #[test]
745    fn test_visualizer_configuration() {
746        let visualizer = QueryPlanVisualizer::new()
747            .with_costs(false)
748            .with_execution_times(false)
749            .with_metadata(true);
750
751        assert!(!visualizer.show_costs);
752        assert!(!visualizer.show_execution_times);
753        assert!(visualizer.show_metadata);
754    }
755
756    #[test]
757    fn test_dot_visualization() {
758        let plan = create_sample_plan();
759        let visualizer = QueryPlanVisualizer::new();
760
761        let dot = visualizer.visualize(&plan, VisualizationFormat::Dot);
762        assert!(dot.is_ok());
763
764        let output = dot.expect("should succeed");
765        assert!(output.contains("digraph QueryPlan"));
766        assert!(output.contains("root"));
767        assert!(output.contains("user"));
768        assert!(output.contains("posts"));
769    }
770
771    #[test]
772    fn test_mermaid_visualization() {
773        let plan = create_sample_plan();
774        let visualizer = QueryPlanVisualizer::new();
775
776        let mermaid = visualizer.visualize(&plan, VisualizationFormat::Mermaid);
777        assert!(mermaid.is_ok());
778
779        let output = mermaid.expect("should succeed");
780        assert!(output.contains("graph TD"));
781        assert!(output.contains("root"));
782        assert!(output.contains("user"));
783    }
784
785    #[test]
786    fn test_ascii_tree_visualization() {
787        let plan = create_sample_plan();
788        let visualizer = QueryPlanVisualizer::new();
789
790        let tree = visualizer.visualize(&plan, VisualizationFormat::AsciiTree);
791        assert!(tree.is_ok());
792
793        let output = tree.expect("should succeed");
794        assert!(output.contains("Query Plan:"));
795        assert!(output.contains("Total Cost:"));
796        assert!(output.contains("Query"));
797        assert!(output.contains("user"));
798        assert!(output.contains("posts"));
799    }
800
801    #[test]
802    fn test_json_visualization() {
803        let plan = create_sample_plan();
804        let visualizer = QueryPlanVisualizer::new();
805
806        let json = visualizer.visualize(&plan, VisualizationFormat::Json);
807        assert!(json.is_ok());
808
809        let output = json.expect("should succeed");
810        assert!(output.contains("root_id"));
811        assert!(output.contains("nodes"));
812        assert!(output.contains("total_cost"));
813    }
814
815    #[test]
816    fn test_html_visualization() {
817        let plan = create_sample_plan();
818        let visualizer = QueryPlanVisualizer::new();
819
820        let html = visualizer.visualize(&plan, VisualizationFormat::Html);
821        assert!(html.is_ok());
822
823        let output = html.expect("should succeed");
824        assert!(output.contains("<!DOCTYPE html>"));
825        assert!(output.contains("Query Plan Visualization"));
826        assert!(output.contains("<script>"));
827    }
828
829    #[test]
830    fn test_timeline_generation() {
831        let plan = create_sample_plan();
832        let visualizer = QueryPlanVisualizer::new();
833
834        let timeline = visualizer.generate_timeline(&plan);
835        assert!(timeline.contains("Execution Timeline"));
836        assert!(timeline.contains("Level 0"));
837        assert!(timeline.contains("Level 1"));
838        assert!(timeline.contains("Level 2"));
839    }
840
841    #[test]
842    fn test_cost_breakdown() {
843        let plan = create_sample_plan();
844        let visualizer = QueryPlanVisualizer::new();
845
846        let breakdown = visualizer.generate_cost_breakdown(&plan);
847        assert!(breakdown.contains("Cost Breakdown"));
848        assert!(breakdown.contains("Total Cost:"));
849        assert!(breakdown.contains("posts")); // Highest cost item
850    }
851
852    #[test]
853    fn test_node_with_dependencies() {
854        let node = PlanNode::new("test".to_string(), "Field".to_string(), "test".to_string())
855            .with_dependency("dep1".to_string())
856            .with_dependency("dep2".to_string());
857
858        assert_eq!(node.dependencies.len(), 2);
859    }
860
861    #[test]
862    fn test_plan_with_dependencies() {
863        let mut plan = QueryPlan::new("root".to_string(), "{ test }".to_string());
864
865        let node1 = PlanNode::new(
866            "node1".to_string(),
867            "Field".to_string(),
868            "field1".to_string(),
869        );
870        let node2 = PlanNode::new(
871            "node2".to_string(),
872            "Field".to_string(),
873            "field2".to_string(),
874        )
875        .with_dependency("node1".to_string());
876
877        plan.add_node(node1);
878        plan.add_node(node2);
879
880        let node2_ref = plan.get_node("node2").expect("should succeed");
881        assert_eq!(node2_ref.dependencies.len(), 1);
882        assert_eq!(node2_ref.dependencies[0], "node1");
883    }
884
885    #[test]
886    fn test_dot_with_dependencies() {
887        let mut plan = QueryPlan::new("root".to_string(), "{ test }".to_string());
888
889        let node1 = PlanNode::new(
890            "node1".to_string(),
891            "Field".to_string(),
892            "field1".to_string(),
893        )
894        .with_cost(1.0);
895        let node2 = PlanNode::new(
896            "node2".to_string(),
897            "Field".to_string(),
898            "field2".to_string(),
899        )
900        .with_dependency("node1".to_string())
901        .with_cost(2.0);
902
903        plan.add_node(node1);
904        plan.add_node(node2);
905
906        let visualizer = QueryPlanVisualizer::new();
907        let dot = visualizer
908            .visualize(&plan, VisualizationFormat::Dot)
909            .expect("should succeed");
910
911        assert!(dot.contains("style=dashed"));
912        assert!(dot.contains("color=red"));
913    }
914
915    #[test]
916    fn test_complex_plan() {
917        let mut plan = QueryPlan::new("root".to_string(), "complex query".to_string());
918
919        // Build a more complex plan with multiple levels
920        let root = PlanNode::new(
921            "root".to_string(),
922            "Object".to_string(),
923            "Query".to_string(),
924        )
925        .with_child("user".to_string())
926        .with_child("posts".to_string());
927
928        let user = PlanNode::new("user".to_string(), "Object".to_string(), "User".to_string())
929            .with_cost(5.0)
930            .with_child("name".to_string())
931            .with_child("email".to_string());
932
933        let name = PlanNode::new("name".to_string(), "Field".to_string(), "name".to_string())
934            .with_cost(1.0);
935
936        let email = PlanNode::new(
937            "email".to_string(),
938            "Field".to_string(),
939            "email".to_string(),
940        )
941        .with_cost(1.0);
942
943        let posts = PlanNode::new("posts".to_string(), "List".to_string(), "posts".to_string())
944            .with_cost(10.0)
945            .with_child("title".to_string());
946
947        let title = PlanNode::new(
948            "title".to_string(),
949            "Field".to_string(),
950            "title".to_string(),
951        )
952        .with_cost(2.0);
953
954        plan.add_node(root);
955        plan.add_node(user);
956        plan.add_node(name);
957        plan.add_node(email);
958        plan.add_node(posts);
959        plan.add_node(title);
960
961        assert_eq!(plan.nodes.len(), 6);
962        assert!(plan.total_cost > 0.0);
963
964        let levels = plan.execution_levels();
965        assert_eq!(levels.len(), 6);
966    }
967
968    #[test]
969    fn test_visualizer_without_costs() {
970        let plan = create_sample_plan();
971        let visualizer = QueryPlanVisualizer::new().with_costs(false);
972
973        let tree = visualizer
974            .visualize(&plan, VisualizationFormat::AsciiTree)
975            .expect("should succeed");
976        assert!(!tree.contains("cost:"));
977    }
978
979    #[test]
980    fn test_visualizer_without_execution_times() {
981        let plan = create_sample_plan();
982        let visualizer = QueryPlanVisualizer::new().with_execution_times(false);
983
984        let tree = visualizer
985            .visualize(&plan, VisualizationFormat::AsciiTree)
986            .expect("should succeed");
987        assert!(!tree.contains("time:"));
988    }
989}