1use crate::dictionary::NodeId;
15use crate::error::{Result, TdbError};
16use crate::query_hints::{IndexType, QueryHints, QueryStats};
17use crate::statistics::TripleStatistics;
18use serde::{Deserialize, Serialize};
19use std::sync::Arc;
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
23pub struct QueryPattern {
24 pub subject: Option<NodeId>,
26 pub predicate: Option<NodeId>,
28 pub object: Option<NodeId>,
30}
31
32impl QueryPattern {
33 pub fn new(s: Option<NodeId>, p: Option<NodeId>, o: Option<NodeId>) -> Self {
35 Self {
36 subject: s,
37 predicate: p,
38 object: o,
39 }
40 }
41
42 pub fn is_exact(&self) -> bool {
44 self.subject.is_some() && self.predicate.is_some() && self.object.is_some()
45 }
46
47 pub fn is_full_scan(&self) -> bool {
49 self.subject.is_none() && self.predicate.is_none() && self.object.is_none()
50 }
51
52 pub fn bound_count(&self) -> usize {
54 let mut count = 0;
55 if self.subject.is_some() {
56 count += 1;
57 }
58 if self.predicate.is_some() {
59 count += 1;
60 }
61 if self.object.is_some() {
62 count += 1;
63 }
64 count
65 }
66
67 pub fn selectivity(&self) -> f64 {
71 let bound = self.bound_count();
72 match bound {
73 3 => 0.01, 2 => 0.1, 1 => 0.5, _ => 1.0, }
78 }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct QueryPlan {
84 #[serde(skip)]
86 pub pattern: QueryPattern,
87 pub index: IndexType,
89 pub estimated_results: usize,
91 pub estimated_io_cost: f64,
93 pub estimated_cpu_cost: f64,
95 pub total_cost: f64,
97 pub confidence: f64,
99 pub explanation: String,
101}
102
103impl QueryPlan {
104 pub fn new(pattern: QueryPattern, index: IndexType, estimated_results: usize) -> Self {
106 let io_cost = (estimated_results as f64 + 1.0).ln();
110 let cpu_cost = estimated_results as f64 * 0.1;
111 let total_cost = io_cost + cpu_cost;
112
113 Self {
114 pattern,
115 index,
116 estimated_results,
117 estimated_io_cost: io_cost,
118 estimated_cpu_cost: cpu_cost,
119 total_cost,
120 confidence: 0.7, explanation: String::new(),
122 }
123 }
124
125 pub fn with_explanation(mut self, explanation: String) -> Self {
127 self.explanation = explanation;
128 self
129 }
130
131 pub fn with_confidence(mut self, confidence: f64) -> Self {
133 self.confidence = confidence.clamp(0.0, 1.0);
134 self
135 }
136
137 pub fn to_ascii_tree(&self) -> String {
139 let mut output = String::new();
140
141 output.push_str("Query Plan\n");
142 output.push_str("══════════\n\n");
143
144 output.push_str("├─ Pattern\n");
146 output.push_str(&format!("│ ├─ Subject: {:?}\n", self.pattern.subject));
147 output.push_str(&format!("│ ├─ Predicate: {:?}\n", self.pattern.predicate));
148 output.push_str(&format!("│ └─ Object: {:?}\n", self.pattern.object));
149 output.push_str("│\n");
150
151 output.push_str(&format!("├─ Index: {:?}\n", self.index));
153 output.push_str("│\n");
154
155 output.push_str("├─ Cost Estimates\n");
157 output.push_str(&format!(
158 "│ ├─ I/O Cost: {:.2}\n",
159 self.estimated_io_cost
160 ));
161 output.push_str(&format!(
162 "│ ├─ CPU Cost: {:.2}\n",
163 self.estimated_cpu_cost
164 ));
165 output.push_str(&format!("│ ├─ Total Cost: {:.2}\n", self.total_cost));
166 output.push_str(&format!("│ ├─ Est. Results: {}\n", self.estimated_results));
167 output.push_str(&format!(
168 "│ └─ Confidence: {:.0}%\n",
169 self.confidence * 100.0
170 ));
171 output.push_str("│\n");
172
173 if !self.explanation.is_empty() {
175 output.push_str("└─ Explanation\n");
176 for line in self.explanation.lines() {
177 output.push_str(&format!(" {}\n", line));
178 }
179 }
180
181 output
182 }
183
184 pub fn to_dot(&self) -> String {
186 let mut output = String::new();
187
188 output.push_str("digraph QueryPlan {\n");
189 output.push_str(" rankdir=TB;\n");
190 output.push_str(" node [shape=box, style=rounded];\n\n");
191
192 output.push_str(" query [label=\"Query Pattern\"];\n");
194
195 let pattern_label = format!(
197 "S: {:?}\\nP: {:?}\\nO: {:?}",
198 self.pattern.subject, self.pattern.predicate, self.pattern.object
199 );
200 output.push_str(&format!(" pattern [label=\"{}\"];\n", pattern_label));
201 output.push_str(" query -> pattern;\n\n");
202
203 let index_label = format!("Index: {:?}", self.index);
205 output.push_str(&format!(" index [label=\"{}\"];\n", index_label));
206 output.push_str(" pattern -> index;\n\n");
207
208 let cost_label = format!(
210 "I/O: {:.2}\\nCPU: {:.2}\\nTotal: {:.2}\\nResults: {}",
211 self.estimated_io_cost,
212 self.estimated_cpu_cost,
213 self.total_cost,
214 self.estimated_results
215 );
216 output.push_str(&format!(
217 " cost [label=\"{}\", shape=ellipse];\n",
218 cost_label
219 ));
220 output.push_str(" index -> cost;\n\n");
221
222 output.push_str("}\n");
223 output
224 }
225
226 pub fn to_json(&self) -> Result<String> {
228 serde_json::to_string_pretty(self)
229 .map_err(|e| TdbError::Other(format!("JSON serialization error: {}", e)))
230 }
231
232 pub fn to_summary(&self) -> String {
234 format!(
235 "Plan: {:?} index | Cost: {:.2} | Results: {} | Confidence: {:.0}%",
236 self.index,
237 self.total_cost,
238 self.estimated_results,
239 self.confidence * 100.0
240 )
241 }
242}
243
244pub struct QueryOptimizer {
246 statistics: Arc<TripleStatistics>,
248 plan_cache: parking_lot::RwLock<std::collections::HashMap<QueryPattern, QueryPlan>>,
250 optimization_level: u8,
252}
253
254impl QueryOptimizer {
255 pub fn new(statistics: Arc<TripleStatistics>) -> Self {
257 Self {
258 statistics,
259 plan_cache: parking_lot::RwLock::new(std::collections::HashMap::new()),
260 optimization_level: 2, }
262 }
263
264 pub fn with_optimization_level(mut self, level: u8) -> Self {
266 self.optimization_level = level.min(2);
267 self
268 }
269
270 pub fn optimize(&self, pattern: QueryPattern, hints: &QueryHints) -> Result<QueryPlan> {
278 if self.optimization_level == 0 {
280 return Ok(self.simple_optimization(pattern, hints));
281 }
282
283 {
285 let cache = self.plan_cache.read();
286 if let Some(cached_plan) = cache.get(&pattern) {
287 if let Some(preferred_index) = hints.preferred_index {
289 let mut plan = cached_plan.clone();
290 plan.index = preferred_index;
291 plan.explanation = format!(
292 "Using hint-specified index {:?} (overriding cached plan)",
293 preferred_index
294 );
295 return Ok(plan);
296 }
297 return Ok(cached_plan.clone());
298 }
299 }
300
301 let plan = if self.optimization_level >= 2 {
303 self.advanced_optimization(pattern.clone(), hints)?
304 } else {
305 self.basic_optimization(pattern.clone(), hints)
306 };
307
308 {
310 let mut cache = self.plan_cache.write();
311 cache.insert(pattern.clone(), plan.clone());
312 }
313
314 Ok(plan)
315 }
316
317 fn simple_optimization(&self, pattern: QueryPattern, hints: &QueryHints) -> QueryPlan {
319 if let Some(preferred_index) = hints.preferred_index {
321 return QueryPlan::new(pattern.clone(), preferred_index, 1000)
322 .with_explanation(format!("Using hint-specified index {:?}", preferred_index));
323 }
324
325 let index = self.select_index_by_pattern(&pattern);
327 let estimated_results = self.estimate_results_simple(&pattern);
328
329 QueryPlan::new(pattern, index, estimated_results)
330 .with_explanation(format!("Simple heuristic selection: {:?}", index))
331 .with_confidence(0.5)
332 }
333
334 fn basic_optimization(&self, pattern: QueryPattern, hints: &QueryHints) -> QueryPlan {
336 if let Some(preferred_index) = hints.preferred_index {
338 return QueryPlan::new(pattern.clone(), preferred_index, 1000)
339 .with_explanation(format!("Using hint-specified index {:?}", preferred_index));
340 }
341
342 let index = self.select_index_by_pattern(&pattern);
344 let estimated_results = self.estimate_results_basic(&pattern);
345
346 QueryPlan::new(pattern, index, estimated_results)
347 .with_explanation(format!("Basic optimization with statistics: {:?}", index))
348 .with_confidence(0.7)
349 }
350
351 fn advanced_optimization(
353 &self,
354 pattern: QueryPattern,
355 hints: &QueryHints,
356 ) -> Result<QueryPlan> {
357 let mut candidates = Vec::new();
359
360 if self.is_index_viable(&pattern, IndexType::SPO) {
362 let plan = self.generate_plan(&pattern, IndexType::SPO)?;
363 candidates.push(plan);
364 }
365
366 if self.is_index_viable(&pattern, IndexType::POS) {
368 let plan = self.generate_plan(&pattern, IndexType::POS)?;
369 candidates.push(plan);
370 }
371
372 if self.is_index_viable(&pattern, IndexType::OSP) {
374 let plan = self.generate_plan(&pattern, IndexType::OSP)?;
375 candidates.push(plan);
376 }
377
378 if let Some(preferred_index) = hints.preferred_index {
380 if let Some(plan) = candidates.iter().find(|p| p.index == preferred_index) {
381 return Ok(plan.clone());
382 }
383 }
384
385 candidates
387 .into_iter()
388 .min_by(|a, b| {
389 a.total_cost
390 .partial_cmp(&b.total_cost)
391 .unwrap_or(std::cmp::Ordering::Equal)
392 })
393 .ok_or_else(|| TdbError::Other("No viable query plan found".to_string()))
394 }
395
396 fn generate_plan(&self, pattern: &QueryPattern, index: IndexType) -> Result<QueryPlan> {
398 let stats = self.statistics.export();
399
400 let base_estimate = match index {
402 IndexType::SPO => {
403 if pattern.subject.is_some() {
404 stats.avg_properties_per_subject as usize
406 } else {
407 stats.total_triples as usize
408 }
409 }
410 IndexType::POS => {
411 if pattern.predicate.is_some() {
412 stats.avg_objects_per_predicate as usize
414 } else {
415 stats.total_triples as usize
416 }
417 }
418 IndexType::OSP => {
419 if pattern.object.is_some() {
420 stats.avg_subjects_per_object as usize
422 } else {
423 stats.total_triples as usize
424 }
425 }
426 };
427
428 let refined_estimate = match pattern.bound_count() {
430 3 => 1, 2 => {
432 let factor = if base_estimate > 10 {
435 (base_estimate as f64).sqrt() as usize
436 } else {
437 base_estimate
438 };
439 factor.max(1)
440 }
441 1 => base_estimate.max(1),
442 _ => stats.total_triples as usize,
443 };
444
445 let confidence = if stats.total_triples > 0 {
447 match pattern.bound_count() {
448 3 => 0.95, 2 => 0.85, 1 => 0.70, _ => 0.50, }
453 } else {
454 0.30 };
456
457 let explanation = format!(
458 "Cost-based selection: {:?} index with estimated {} results (avg={:.2}, confidence={:.0}%)",
459 index,
460 refined_estimate,
461 match index {
462 IndexType::SPO => stats.avg_properties_per_subject,
463 IndexType::POS => stats.avg_objects_per_predicate,
464 IndexType::OSP => stats.avg_subjects_per_object,
465 },
466 confidence * 100.0
467 );
468
469 Ok(QueryPlan::new(pattern.clone(), index, refined_estimate)
470 .with_explanation(explanation)
471 .with_confidence(confidence))
472 }
473
474 fn is_index_viable(&self, pattern: &QueryPattern, index: IndexType) -> bool {
476 match index {
477 IndexType::SPO => true, IndexType::POS => pattern.predicate.is_some(),
479 IndexType::OSP => pattern.object.is_some(),
480 }
481 }
482
483 fn select_index_by_pattern(&self, pattern: &QueryPattern) -> IndexType {
485 match (
486 pattern.subject.is_some(),
487 pattern.predicate.is_some(),
488 pattern.object.is_some(),
489 ) {
490 (true, _, _) => IndexType::SPO, (false, true, _) => IndexType::POS, (false, false, true) => IndexType::OSP, (false, false, false) => IndexType::SPO, }
495 }
496
497 fn estimate_results_simple(&self, pattern: &QueryPattern) -> usize {
499 match pattern.bound_count() {
500 3 => 1, 2 => 100, 1 => 1000, _ => 10000, }
505 }
506
507 fn estimate_results_basic(&self, pattern: &QueryPattern) -> usize {
509 let stats = self.statistics.export();
510 let total = stats.total_triples.max(1) as usize;
511
512 match pattern.bound_count() {
513 3 => 1, 2 => (total as f64 * 0.01).ceil() as usize, 1 => (total as f64 * 0.1).ceil() as usize, _ => total, }
518 }
519
520 pub fn clear_cache(&self) {
522 let mut cache = self.plan_cache.write();
523 cache.clear();
524 }
525
526 pub fn cache_stats(&self) -> QueryOptimizerStats {
528 let cache = self.plan_cache.read();
529 QueryOptimizerStats {
530 cached_plans: cache.len(),
531 optimization_level: self.optimization_level,
532 }
533 }
534}
535
536#[derive(Debug, Clone, Serialize, Deserialize)]
538pub struct QueryOptimizerStats {
539 pub cached_plans: usize,
541 pub optimization_level: u8,
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548 use crate::statistics::StatisticsConfig;
549
550 fn create_test_optimizer() -> QueryOptimizer {
551 let stats = Arc::new(TripleStatistics::new(StatisticsConfig::default()));
552 QueryOptimizer::new(stats)
553 }
554
555 #[test]
556 fn test_query_pattern_selectivity() {
557 let pattern_exact = QueryPattern::new(
558 Some(NodeId::new(1)),
559 Some(NodeId::new(2)),
560 Some(NodeId::new(3)),
561 );
562 assert_eq!(pattern_exact.selectivity(), 0.01);
563 assert!(pattern_exact.is_exact());
564
565 let pattern_two = QueryPattern::new(Some(NodeId::new(1)), Some(NodeId::new(2)), None);
566 assert_eq!(pattern_two.selectivity(), 0.1);
567 assert_eq!(pattern_two.bound_count(), 2);
568
569 let pattern_one = QueryPattern::new(Some(NodeId::new(1)), None, None);
570 assert_eq!(pattern_one.selectivity(), 0.5);
571
572 let pattern_none = QueryPattern::new(None, None, None);
573 assert_eq!(pattern_none.selectivity(), 1.0);
574 assert!(pattern_none.is_full_scan());
575 }
576
577 #[test]
578 fn test_simple_optimization() {
579 let optimizer = create_test_optimizer();
580 let hints = QueryHints::new();
581
582 let pattern = QueryPattern::new(Some(NodeId::new(1)), None, None);
584 let plan = optimizer.simple_optimization(pattern, &hints);
585 assert_eq!(plan.index, IndexType::SPO);
586
587 let pattern = QueryPattern::new(None, Some(NodeId::new(2)), None);
589 let plan = optimizer.simple_optimization(pattern, &hints);
590 assert_eq!(plan.index, IndexType::POS);
591
592 let pattern = QueryPattern::new(None, None, Some(NodeId::new(3)));
594 let plan = optimizer.simple_optimization(pattern, &hints);
595 assert_eq!(plan.index, IndexType::OSP);
596 }
597
598 #[test]
599 fn test_hint_override() {
600 let optimizer = create_test_optimizer();
601
602 let pattern = QueryPattern::new(Some(NodeId::new(1)), None, None);
604 let hints = QueryHints::new().with_index(IndexType::POS);
605
606 let plan = optimizer.simple_optimization(pattern, &hints);
607 assert_eq!(plan.index, IndexType::POS);
608 }
609
610 #[test]
611 fn test_plan_cost_estimation() {
612 let pattern = QueryPattern::new(Some(NodeId::new(1)), Some(NodeId::new(2)), None);
613 let plan = QueryPlan::new(pattern, IndexType::SPO, 100);
614
615 assert!(plan.estimated_io_cost > 0.0);
616 assert!(plan.estimated_cpu_cost > 0.0);
617 assert_eq!(
618 plan.total_cost,
619 plan.estimated_io_cost + plan.estimated_cpu_cost
620 );
621 }
622
623 #[test]
624 fn test_optimization_levels() {
625 let stats = Arc::new(TripleStatistics::new(StatisticsConfig::default()));
626
627 let optimizer_disabled = QueryOptimizer::new(stats.clone()).with_optimization_level(0);
628 assert_eq!(optimizer_disabled.optimization_level, 0);
629
630 let optimizer_basic = QueryOptimizer::new(stats.clone()).with_optimization_level(1);
631 assert_eq!(optimizer_basic.optimization_level, 1);
632
633 let optimizer_advanced = QueryOptimizer::new(stats.clone()).with_optimization_level(2);
634 assert_eq!(optimizer_advanced.optimization_level, 2);
635 }
636
637 #[test]
638 fn test_index_viability() {
639 let optimizer = create_test_optimizer();
640
641 let pattern = QueryPattern::new(None, None, None);
643 assert!(optimizer.is_index_viable(&pattern, IndexType::SPO));
644
645 let pattern_no_p = QueryPattern::new(Some(NodeId::new(1)), None, None);
647 assert!(!optimizer.is_index_viable(&pattern_no_p, IndexType::POS));
648
649 let pattern_with_p = QueryPattern::new(None, Some(NodeId::new(2)), None);
650 assert!(optimizer.is_index_viable(&pattern_with_p, IndexType::POS));
651
652 let pattern_no_o = QueryPattern::new(Some(NodeId::new(1)), None, None);
654 assert!(!optimizer.is_index_viable(&pattern_no_o, IndexType::OSP));
655
656 let pattern_with_o = QueryPattern::new(None, None, Some(NodeId::new(3)));
657 assert!(optimizer.is_index_viable(&pattern_with_o, IndexType::OSP));
658 }
659
660 #[test]
661 fn test_cache_functionality() {
662 let optimizer = create_test_optimizer();
663 let pattern = QueryPattern::new(Some(NodeId::new(1)), None, None);
664 let hints = QueryHints::new();
665
666 let plan1 = optimizer.optimize(pattern.clone(), &hints).unwrap();
668
669 let plan2 = optimizer.optimize(pattern.clone(), &hints).unwrap();
671
672 assert_eq!(plan1.index, plan2.index);
673
674 let cache_stats = optimizer.cache_stats();
675 assert_eq!(cache_stats.cached_plans, 1);
676
677 optimizer.clear_cache();
678 let cache_stats = optimizer.cache_stats();
679 assert_eq!(cache_stats.cached_plans, 0);
680 }
681
682 #[test]
683 fn test_result_estimation() {
684 let optimizer = create_test_optimizer();
685
686 let pattern_exact = QueryPattern::new(
688 Some(NodeId::new(1)),
689 Some(NodeId::new(2)),
690 Some(NodeId::new(3)),
691 );
692 assert_eq!(optimizer.estimate_results_simple(&pattern_exact), 1);
693
694 let pattern_two = QueryPattern::new(Some(NodeId::new(1)), Some(NodeId::new(2)), None);
696 assert_eq!(optimizer.estimate_results_simple(&pattern_two), 100);
697
698 let pattern_one = QueryPattern::new(Some(NodeId::new(1)), None, None);
700 assert_eq!(optimizer.estimate_results_simple(&pattern_one), 1000);
701
702 let pattern_none = QueryPattern::new(None, None, None);
704 assert_eq!(optimizer.estimate_results_simple(&pattern_none), 10000);
705 }
706
707 #[test]
708 fn test_query_plan_ascii_visualization() {
709 let pattern = QueryPattern::new(Some(NodeId::new(1)), None, Some(NodeId::new(3)));
710 let plan = QueryPlan::new(pattern, IndexType::SPO, 100)
711 .with_explanation("Using SPO index for subject-bound query".to_string())
712 .with_confidence(0.85);
713
714 let ascii_tree = plan.to_ascii_tree();
715
716 assert!(ascii_tree.contains("Query Plan"));
718 assert!(ascii_tree.contains("Pattern"));
719 assert!(ascii_tree.contains("Subject:"));
720 assert!(ascii_tree.contains("Predicate:"));
721 assert!(ascii_tree.contains("Object:"));
722 assert!(ascii_tree.contains("Index: SPO"));
723 assert!(ascii_tree.contains("I/O Cost:"));
724 assert!(ascii_tree.contains("CPU Cost:"));
725 assert!(ascii_tree.contains("Total Cost:"));
726 assert!(ascii_tree.contains("Est. Results: 100"));
727 assert!(ascii_tree.contains("Confidence: 85%"));
728 assert!(ascii_tree.contains("Explanation"));
729 assert!(ascii_tree.contains("Using SPO index"));
730 }
731
732 #[test]
733 fn test_query_plan_dot_visualization() {
734 let pattern = QueryPattern::new(Some(NodeId::new(1)), Some(NodeId::new(2)), None);
735 let plan = QueryPlan::new(pattern, IndexType::SPO, 50);
736
737 let dot_output = plan.to_dot();
738
739 assert!(dot_output.starts_with("digraph QueryPlan {"));
741 assert!(dot_output.contains("rankdir=TB"));
742 assert!(dot_output.contains("query [label=\"Query Pattern\"]"));
743 assert!(dot_output.contains("pattern [label="));
744 assert!(dot_output.contains("index [label=\"Index: SPO\"]"));
745 assert!(dot_output.contains("cost [label="));
746 assert!(dot_output.contains("query -> pattern"));
747 assert!(dot_output.contains("pattern -> index"));
748 assert!(dot_output.contains("index -> cost"));
749 assert!(dot_output.ends_with("}\n"));
750 }
751
752 #[test]
753 fn test_query_plan_json_visualization() {
754 let pattern = QueryPattern::new(Some(NodeId::new(1)), None, None);
755 let plan = QueryPlan::new(pattern, IndexType::SPO, 200).with_confidence(0.9);
756
757 let json_result = plan.to_json();
758 assert!(json_result.is_ok());
759
760 let json_output = json_result.unwrap();
761
762 assert!(json_output.contains("\"index\""));
764 assert!(json_output.contains("\"estimated_results\": 200"));
765 assert!(json_output.contains("\"estimated_io_cost\""));
766 assert!(json_output.contains("\"estimated_cpu_cost\""));
767 assert!(json_output.contains("\"total_cost\""));
768 assert!(json_output.contains("\"confidence\": 0.9"));
769 }
770
771 #[test]
772 fn test_query_plan_summary() {
773 let pattern = QueryPattern::new(None, Some(NodeId::new(2)), None);
774 let plan = QueryPlan::new(pattern, IndexType::POS, 300).with_confidence(0.75);
775
776 let summary = plan.to_summary();
777
778 assert!(summary.contains("POS index"));
780 assert!(summary.contains("Cost:"));
781 assert!(summary.contains("Results: 300"));
782 assert!(summary.contains("Confidence: 75%"));
783 }
784
785 #[test]
786 fn test_visualization_with_empty_explanation() {
787 let pattern = QueryPattern::new(
788 Some(NodeId::new(1)),
789 Some(NodeId::new(2)),
790 Some(NodeId::new(3)),
791 );
792 let plan = QueryPlan::new(pattern, IndexType::SPO, 1);
793
794 let ascii_tree = plan.to_ascii_tree();
795
796 assert!(ascii_tree.contains("Query Plan"));
798 assert!(ascii_tree.contains("Pattern"));
799 assert!(ascii_tree.contains("Cost Estimates"));
800 }
801
802 #[test]
803 fn test_visualization_format_consistency() {
804 let pattern = QueryPattern::new(Some(NodeId::new(10)), None, Some(NodeId::new(20)));
805 let plan = QueryPlan::new(pattern, IndexType::OSP, 500)
806 .with_explanation("Multi-line\nexplanation\ntest".to_string())
807 .with_confidence(0.95);
808
809 let _ = plan.to_ascii_tree();
811 let _ = plan.to_dot();
812 let _ = plan.to_summary();
813 assert!(plan.to_json().is_ok());
814 }
815}