1use crate::error::OxirsError;
25use crate::Result;
26use serde::{Deserialize, Serialize};
27use std::fmt;
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct QueryPlanNode {
32 pub node_type: String,
34 pub description: String,
36 pub estimated_cardinality: Option<usize>,
38 pub actual_cardinality: Option<usize>,
40 pub estimated_cost: Option<f64>,
42 pub execution_time_us: Option<u64>,
44 pub index_used: Option<String>,
46 pub children: Vec<QueryPlanNode>,
48 pub metadata: std::collections::HashMap<String, String>,
50}
51
52impl QueryPlanNode {
53 pub fn new(node_type: impl Into<String>, description: impl Into<String>) -> Self {
55 Self {
56 node_type: node_type.into(),
57 description: description.into(),
58 estimated_cardinality: None,
59 actual_cardinality: None,
60 estimated_cost: None,
61 execution_time_us: None,
62 index_used: None,
63 children: Vec::new(),
64 metadata: std::collections::HashMap::new(),
65 }
66 }
67
68 pub fn add_child(&mut self, child: QueryPlanNode) {
70 self.children.push(child);
71 }
72
73 pub fn with_estimated_cardinality(mut self, cardinality: usize) -> Self {
75 self.estimated_cardinality = Some(cardinality);
76 self
77 }
78
79 pub fn with_actual_cardinality(mut self, cardinality: usize) -> Self {
81 self.actual_cardinality = Some(cardinality);
82 self
83 }
84
85 pub fn with_estimated_cost(mut self, cost: f64) -> Self {
87 self.estimated_cost = Some(cost);
88 self
89 }
90
91 pub fn with_execution_time(mut self, time_us: u64) -> Self {
93 self.execution_time_us = Some(time_us);
94 self
95 }
96
97 pub fn with_index(mut self, index: impl Into<String>) -> Self {
99 self.index_used = Some(index.into());
100 self
101 }
102
103 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
105 self.metadata.insert(key.into(), value.into());
106 self
107 }
108}
109
110pub struct QueryPlanVisualizer {
112 show_stats: bool,
114 show_costs: bool,
116 show_indexes: bool,
118 show_cardinality: bool,
120}
121
122impl Default for QueryPlanVisualizer {
123 fn default() -> Self {
124 Self::new()
125 }
126}
127
128impl QueryPlanVisualizer {
129 pub fn new() -> Self {
131 Self {
132 show_stats: true,
133 show_costs: true,
134 show_indexes: true,
135 show_cardinality: true,
136 }
137 }
138
139 pub fn with_stats(mut self, show: bool) -> Self {
141 self.show_stats = show;
142 self
143 }
144
145 pub fn with_costs(mut self, show: bool) -> Self {
147 self.show_costs = show;
148 self
149 }
150
151 pub fn with_indexes(mut self, show: bool) -> Self {
153 self.show_indexes = show;
154 self
155 }
156
157 pub fn with_cardinality(mut self, show: bool) -> Self {
159 self.show_cardinality = show;
160 self
161 }
162
163 pub fn visualize_as_tree(&self, plan: &QueryPlanNode) -> String {
165 let mut output = String::new();
166 self.render_node(plan, &mut output, "", true);
167 output
168 }
169
170 fn render_node(&self, node: &QueryPlanNode, output: &mut String, prefix: &str, is_last: bool) {
172 output.push_str(prefix);
174 if is_last {
175 output.push_str("└─ ");
176 } else {
177 output.push_str("├─ ");
178 }
179
180 output.push_str(&format!("[{}] {}", node.node_type, node.description));
182
183 let mut stats = Vec::new();
185
186 if self.show_cardinality {
187 if let Some(est) = node.estimated_cardinality {
188 if let Some(act) = node.actual_cardinality {
189 stats.push(format!("card: {} → {}", est, act));
190 } else {
191 stats.push(format!("card: ~{}", est));
192 }
193 }
194 }
195
196 if self.show_costs {
197 if let Some(cost) = node.estimated_cost {
198 stats.push(format!("cost: {:.2}", cost));
199 }
200 }
201
202 if self.show_stats {
203 if let Some(time) = node.execution_time_us {
204 if time >= 1000 {
205 stats.push(format!("time: {:.2}ms", time as f64 / 1000.0));
206 } else {
207 stats.push(format!("time: {}μs", time));
208 }
209 }
210 }
211
212 if self.show_indexes {
213 if let Some(idx) = &node.index_used {
214 stats.push(format!("index: {}", idx));
215 }
216 }
217
218 if !stats.is_empty() {
219 output.push_str(&format!(" ({})", stats.join(", ")));
220 }
221
222 output.push('\n');
223
224 let child_prefix = if is_last {
226 format!("{} ", prefix)
227 } else {
228 format!("{}│ ", prefix)
229 };
230
231 for (i, child) in node.children.iter().enumerate() {
232 let is_last_child = i == node.children.len() - 1;
233 self.render_node(child, output, &child_prefix, is_last_child);
234 }
235 }
236
237 pub fn export_as_json(&self, plan: &QueryPlanNode) -> Result<String> {
239 serde_json::to_string_pretty(plan)
240 .map_err(|e| OxirsError::Query(format!("Failed to serialize query plan: {}", e)))
241 }
242
243 pub fn generate_summary(&self, plan: &QueryPlanNode) -> QueryPlanSummary {
245 let mut summary = QueryPlanSummary::default();
246 Self::collect_stats(plan, &mut summary);
247 summary
248 }
249
250 fn collect_stats(node: &QueryPlanNode, summary: &mut QueryPlanSummary) {
252 summary.total_nodes += 1;
253
254 if let Some(time) = node.execution_time_us {
255 summary.total_execution_time_us += time;
256 }
257
258 if let Some(cost) = node.estimated_cost {
259 summary.total_estimated_cost += cost;
260 }
261
262 if let Some(est) = node.estimated_cardinality {
263 if let Some(act) = node.actual_cardinality {
264 let error = (est as f64 - act as f64).abs() / act.max(1) as f64;
265 summary.cardinality_errors.push(error);
266 }
267 }
268
269 if node.index_used.is_some() {
270 summary.index_operations += 1;
271 }
272
273 match node.node_type.as_str() {
274 "TriplePattern" => summary.triple_patterns += 1,
275 "Join" => summary.joins += 1,
276 "Filter" => summary.filters += 1,
277 "Union" => summary.unions += 1,
278 "Optional" => summary.optionals += 1,
279 _ => {}
280 }
281
282 for child in &node.children {
283 Self::collect_stats(child, summary);
284 }
285 }
286
287 pub fn suggest_optimizations(&self, plan: &QueryPlanNode) -> Vec<OptimizationHint> {
289 let mut hints = Vec::new();
290 Self::analyze_node(plan, &mut hints);
291 hints
292 }
293
294 fn analyze_node(node: &QueryPlanNode, hints: &mut Vec<OptimizationHint>) {
296 if let (Some(est), Some(act)) = (node.estimated_cardinality, node.actual_cardinality) {
298 if est > 0 && act > 0 {
299 let ratio = est as f64 / act as f64;
300 if !(0.1..=10.0).contains(&ratio) {
301 hints.push(OptimizationHint {
302 severity: HintSeverity::Warning,
303 node_type: node.node_type.clone(),
304 description: node.description.clone(),
305 message: format!(
306 "Cardinality misestimation: estimated {} but got {} ({}x off)",
307 est,
308 act,
309 if ratio > 1.0 { ratio } else { 1.0 / ratio }
310 ),
311 suggestion: "Consider updating statistics or adding histogram data"
312 .to_string(),
313 });
314 }
315 }
316 }
317
318 if node.node_type == "TriplePattern" && node.index_used.is_none() {
320 if let Some(card) = node.actual_cardinality {
321 if card > 1000 {
322 hints.push(OptimizationHint {
323 severity: HintSeverity::Info,
324 node_type: node.node_type.clone(),
325 description: node.description.clone(),
326 message: format!("Full scan on {} results without index", card),
327 suggestion: "Consider adding an index for this pattern".to_string(),
328 });
329 }
330 }
331 }
332
333 if let Some(time) = node.execution_time_us {
335 if time > 100_000 {
336 hints.push(OptimizationHint {
338 severity: HintSeverity::Warning,
339 node_type: node.node_type.clone(),
340 description: node.description.clone(),
341 message: format!("Slow operation: {:.2}ms", time as f64 / 1000.0),
342 suggestion: "Consider optimizing this operation or adding caching".to_string(),
343 });
344 }
345 }
346
347 for child in &node.children {
349 Self::analyze_node(child, hints);
350 }
351 }
352}
353
354#[derive(Debug, Default, Serialize, Deserialize)]
356pub struct QueryPlanSummary {
357 pub total_nodes: usize,
359 pub total_execution_time_us: u64,
361 pub total_estimated_cost: f64,
363 pub triple_patterns: usize,
365 pub joins: usize,
367 pub filters: usize,
369 pub unions: usize,
371 pub optionals: usize,
373 pub index_operations: usize,
375 pub cardinality_errors: Vec<f64>,
377}
378
379impl QueryPlanSummary {
380 pub fn avg_cardinality_error(&self) -> f64 {
382 if self.cardinality_errors.is_empty() {
383 0.0
384 } else {
385 self.cardinality_errors.iter().sum::<f64>() / self.cardinality_errors.len() as f64
386 }
387 }
388
389 pub fn execution_time_ms(&self) -> f64 {
391 self.total_execution_time_us as f64 / 1000.0
392 }
393}
394
395impl fmt::Display for QueryPlanSummary {
396 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
397 writeln!(f, "Query Plan Summary:")?;
398 writeln!(f, " Nodes: {}", self.total_nodes)?;
399 writeln!(f, " Triple Patterns: {}", self.triple_patterns)?;
400 writeln!(f, " Joins: {}", self.joins)?;
401 writeln!(f, " Filters: {}", self.filters)?;
402 writeln!(f, " Unions: {}", self.unions)?;
403 writeln!(f, " Optionals: {}", self.optionals)?;
404 writeln!(f, " Index Ops: {}", self.index_operations)?;
405 writeln!(f, " Estimated Cost: {:.2}", self.total_estimated_cost)?;
406 writeln!(f, " Execution Time: {:.2}ms", self.execution_time_ms())?;
407 if !self.cardinality_errors.is_empty() {
408 writeln!(
409 f,
410 " Avg Card Error: {:.2}%",
411 self.avg_cardinality_error() * 100.0
412 )?;
413 }
414 Ok(())
415 }
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct OptimizationHint {
421 pub severity: HintSeverity,
423 pub node_type: String,
425 pub description: String,
427 pub message: String,
429 pub suggestion: String,
431}
432
433impl fmt::Display for OptimizationHint {
434 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435 write!(
436 f,
437 "[{:?}] {} - {}: {} → {}",
438 self.severity, self.node_type, self.description, self.message, self.suggestion
439 )
440 }
441}
442
443#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
445pub enum HintSeverity {
446 Info,
448 Warning,
450 Critical,
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 fn create_sample_plan() -> QueryPlanNode {
459 let mut root = QueryPlanNode::new("Join", "HashJoin on ?person")
460 .with_estimated_cardinality(100)
461 .with_actual_cardinality(95)
462 .with_estimated_cost(150.0)
463 .with_execution_time(5000);
464
465 let child1 = QueryPlanNode::new("TriplePattern", "?person rdf:type foaf:Person")
466 .with_estimated_cardinality(1000)
467 .with_actual_cardinality(1000)
468 .with_index("SPO".to_string())
469 .with_execution_time(1000);
470
471 let child2 = QueryPlanNode::new("TriplePattern", "?person foaf:name ?name")
472 .with_estimated_cardinality(100)
473 .with_actual_cardinality(95)
474 .with_index("SPO".to_string())
475 .with_execution_time(500);
476
477 root.add_child(child1);
478 root.add_child(child2);
479 root
480 }
481
482 #[test]
483 fn test_query_plan_node_creation() {
484 let node = QueryPlanNode::new("TriplePattern", "?s ?p ?o")
485 .with_estimated_cardinality(1000)
486 .with_estimated_cost(100.0);
487
488 assert_eq!(node.node_type, "TriplePattern");
489 assert_eq!(node.description, "?s ?p ?o");
490 assert_eq!(node.estimated_cardinality, Some(1000));
491 assert_eq!(node.estimated_cost, Some(100.0));
492 }
493
494 #[test]
495 fn test_visualizer_tree_output() {
496 let plan = create_sample_plan();
497 let visualizer = QueryPlanVisualizer::new();
498 let tree = visualizer.visualize_as_tree(&plan);
499
500 assert!(tree.contains("[Join] HashJoin on ?person"));
501 assert!(tree.contains("TriplePattern"));
502 assert!(tree.contains("foaf:Person"));
503 assert!(tree.contains("foaf:name"));
504 }
505
506 #[test]
507 fn test_json_export() {
508 let plan = create_sample_plan();
509 let visualizer = QueryPlanVisualizer::new();
510 let json = visualizer
511 .export_as_json(&plan)
512 .expect("operation should succeed");
513
514 assert!(json.contains("\"node_type\": \"Join\""));
515 assert!(json.contains("\"estimated_cardinality\": 100"));
516 assert!(json.contains("\"children\""));
517 }
518
519 #[test]
520 fn test_summary_generation() {
521 let plan = create_sample_plan();
522 let visualizer = QueryPlanVisualizer::new();
523 let summary = visualizer.generate_summary(&plan);
524
525 assert_eq!(summary.total_nodes, 3); assert_eq!(summary.joins, 1);
527 assert_eq!(summary.triple_patterns, 2);
528 assert_eq!(summary.index_operations, 2);
529 assert!(summary.total_execution_time_us > 0);
530 }
531
532 #[test]
533 fn test_optimization_hints() {
534 let plan = QueryPlanNode::new("TriplePattern", "?s ?p ?o")
535 .with_estimated_cardinality(100)
536 .with_actual_cardinality(10000); let visualizer = QueryPlanVisualizer::new();
539 let hints = visualizer.suggest_optimizations(&plan);
540
541 assert!(!hints.is_empty());
542 assert!(hints
543 .iter()
544 .any(|h| h.message.contains("Cardinality misestimation")));
545 }
546
547 #[test]
548 fn test_slow_operation_detection() {
549 let plan = QueryPlanNode::new("Join", "Complex join").with_execution_time(200_000); let visualizer = QueryPlanVisualizer::new();
552 let hints = visualizer.suggest_optimizations(&plan);
553
554 assert!(hints.iter().any(|h| h.message.contains("Slow operation")));
555 }
556
557 #[test]
558 fn test_visualizer_options() {
559 let plan = create_sample_plan();
560 let visualizer = QueryPlanVisualizer::new()
561 .with_costs(false)
562 .with_indexes(false);
563
564 let tree = visualizer.visualize_as_tree(&plan);
565
566 assert!(!tree.contains("cost:"));
568 assert!(!tree.contains("index:"));
569 assert!(tree.contains("card:"));
571 assert!(tree.contains("time:") || tree.contains("μs"));
572 }
573
574 #[test]
575 fn test_summary_display() {
576 let plan = create_sample_plan();
577 let visualizer = QueryPlanVisualizer::new();
578 let summary = visualizer.generate_summary(&plan);
579
580 let display = format!("{}", summary);
581 assert!(display.contains("Query Plan Summary:"));
582 assert!(display.contains("Triple Patterns:"));
583 assert!(display.contains("Joins:"));
584 }
585}