1use serde::{Deserialize, Serialize};
31use std::fmt;
32
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42pub struct QueryPlan {
43 pub root: PlanNode,
45 pub estimated_cost: f64,
47 pub estimated_cardinality: u64,
49}
50
51#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53#[serde(tag = "type", rename_all = "snake_case")]
54pub enum PlanNode {
55 TripleScan {
57 pattern: String,
59 index_used: IndexType,
61 estimated_rows: u64,
63 },
64 HashJoin {
67 left: Box<PlanNode>,
68 right: Box<PlanNode>,
69 join_vars: Vec<String>,
71 },
72 NestedLoopJoin {
75 outer: Box<PlanNode>,
76 inner: Box<PlanNode>,
77 },
78 Filter {
80 expr: String,
82 child: Box<PlanNode>,
83 },
84 Sort {
86 vars: Vec<String>,
88 child: Box<PlanNode>,
89 },
90 Limit {
92 limit: usize,
93 offset: usize,
94 child: Box<PlanNode>,
95 },
96 Distinct { child: Box<PlanNode> },
98 Union {
100 left: Box<PlanNode>,
101 right: Box<PlanNode>,
102 },
103 Optional {
105 left: Box<PlanNode>,
106 right: Box<PlanNode>,
107 },
108 Aggregate {
110 group_by: Vec<String>,
112 aggs: Vec<String>,
114 child: Box<PlanNode>,
115 },
116 Subquery { plan: Box<QueryPlan> },
118 PropertyPath {
120 subject: String,
121 path: String,
122 object: String,
123 },
124 Service {
126 endpoint: String,
127 subplan: Box<QueryPlan>,
128 },
129 MergeJoin {
131 left: Box<PlanNode>,
132 right: Box<PlanNode>,
133 join_vars: Vec<String>,
134 },
135 ValuesScan { vars: Vec<String>, row_count: usize },
137 NamedGraph { graph: String, child: Box<PlanNode> },
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
143#[serde(rename_all = "UPPERCASE")]
144pub enum IndexType {
145 Spo,
147 Pos,
149 Osp,
151 FullScan,
153}
154
155impl fmt::Display for IndexType {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 match self {
158 Self::Spo => write!(f, "SPO"),
159 Self::Pos => write!(f, "POS"),
160 Self::Osp => write!(f, "OSP"),
161 Self::FullScan => write!(f, "FULL_SCAN"),
162 }
163 }
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
168pub enum ExplainFormat {
169 Text,
171 Json,
173 Dot,
175}
176
177#[derive(Debug, Clone)]
196pub struct QueryExplainer {
197 show_estimates: bool,
198 show_costs: bool,
199 format: ExplainFormat,
200}
201
202impl Default for QueryExplainer {
203 fn default() -> Self {
204 Self {
205 show_estimates: true,
206 show_costs: true,
207 format: ExplainFormat::Text,
208 }
209 }
210}
211
212impl QueryExplainer {
213 pub fn new() -> Self {
215 Self::default()
216 }
217
218 pub fn builder() -> QueryExplainerBuilder {
220 QueryExplainerBuilder::default()
221 }
222
223 pub fn explain(&self, plan: &QueryPlan) -> String {
225 match self.format {
226 ExplainFormat::Text => self.explain_text(plan),
227 ExplainFormat::Json => self.explain_json(plan),
228 ExplainFormat::Dot => self.explain_dot(plan),
229 }
230 }
231
232 pub fn explain_with_format(&self, plan: &QueryPlan, format: ExplainFormat) -> String {
234 match format {
235 ExplainFormat::Text => self.explain_text(plan),
236 ExplainFormat::Json => self.explain_json(plan),
237 ExplainFormat::Dot => self.explain_dot(plan),
238 }
239 }
240
241 pub fn explain_text(&self, plan: &QueryPlan) -> String {
245 let mut out = String::new();
246 out.push_str("Query Plan\n");
247 out.push_str("==========\n");
248 if self.show_costs {
249 out.push_str(&format!(
250 "Estimated cost : {:.4}\n",
251 plan.estimated_cost
252 ));
253 }
254 if self.show_estimates {
255 out.push_str(&format!(
256 "Estimated cardinality : {}\n",
257 plan.estimated_cardinality
258 ));
259 }
260 out.push('\n');
261 self.format_node_text(&plan.root, &mut out, 0);
262 out
263 }
264
265 fn format_node_text(&self, node: &PlanNode, out: &mut String, depth: usize) {
266 let indent = " ".repeat(depth);
267 let prefix = if depth == 0 {
268 String::new()
269 } else {
270 format!("{indent}└─ ")
271 };
272
273 match node {
274 PlanNode::TripleScan {
275 pattern,
276 index_used,
277 estimated_rows,
278 } => {
279 out.push_str(&format!("{prefix}TripleScan\n"));
280 let child_indent = " ".repeat(depth + 1);
281 out.push_str(&format!("{child_indent} pattern : {pattern}\n"));
282 out.push_str(&format!("{child_indent} index : {index_used}\n"));
283 if self.show_estimates {
284 out.push_str(&format!("{child_indent} est. rows : {estimated_rows}\n"));
285 }
286 }
287 PlanNode::HashJoin {
288 left,
289 right,
290 join_vars,
291 } => {
292 out.push_str(&format!(
293 "{prefix}HashJoin [key: {}]\n",
294 join_vars.join(", ")
295 ));
296 self.format_node_text(left, out, depth + 1);
297 self.format_node_text(right, out, depth + 1);
298 }
299 PlanNode::NestedLoopJoin { outer, inner } => {
300 out.push_str(&format!("{prefix}NestedLoopJoin\n"));
301 self.format_node_text(outer, out, depth + 1);
302 self.format_node_text(inner, out, depth + 1);
303 }
304 PlanNode::Filter { expr, child } => {
305 out.push_str(&format!("{prefix}Filter [{expr}]\n"));
306 self.format_node_text(child, out, depth + 1);
307 }
308 PlanNode::Sort { vars, child } => {
309 out.push_str(&format!("{prefix}Sort [{}]\n", vars.join(", ")));
310 self.format_node_text(child, out, depth + 1);
311 }
312 PlanNode::Limit {
313 limit,
314 offset,
315 child,
316 } => {
317 out.push_str(&format!("{prefix}Limit [{limit} offset {offset}]\n"));
318 self.format_node_text(child, out, depth + 1);
319 }
320 PlanNode::Distinct { child } => {
321 out.push_str(&format!("{prefix}Distinct\n"));
322 self.format_node_text(child, out, depth + 1);
323 }
324 PlanNode::Union { left, right } => {
325 out.push_str(&format!("{prefix}Union\n"));
326 self.format_node_text(left, out, depth + 1);
327 self.format_node_text(right, out, depth + 1);
328 }
329 PlanNode::Optional { left, right } => {
330 out.push_str(&format!("{prefix}Optional\n"));
331 self.format_node_text(left, out, depth + 1);
332 self.format_node_text(right, out, depth + 1);
333 }
334 PlanNode::Aggregate {
335 group_by,
336 aggs,
337 child,
338 } => {
339 out.push_str(&format!(
340 "{prefix}Aggregate [group: {}] [aggs: {}]\n",
341 group_by.join(", "),
342 aggs.join(", ")
343 ));
344 self.format_node_text(child, out, depth + 1);
345 }
346 PlanNode::Subquery { plan } => {
347 out.push_str(&format!("{prefix}Subquery\n"));
348 let child_indent = " ".repeat(depth + 1);
349 if self.show_costs {
350 out.push_str(&format!(
351 "{child_indent} est. cost : {:.4}\n",
352 plan.estimated_cost
353 ));
354 }
355 if self.show_estimates {
356 out.push_str(&format!(
357 "{child_indent} est. card : {}\n",
358 plan.estimated_cardinality
359 ));
360 }
361 self.format_node_text(&plan.root, out, depth + 1);
362 }
363 PlanNode::PropertyPath {
364 subject,
365 path,
366 object,
367 } => {
368 out.push_str(&format!(
369 "{prefix}PropertyPath [{subject} {path} {object}]\n"
370 ));
371 }
372 PlanNode::Service { endpoint, subplan } => {
373 out.push_str(&format!("{prefix}Service [{endpoint}]\n"));
374 let child_indent = " ".repeat(depth + 1);
375 if self.show_costs {
376 out.push_str(&format!(
377 "{child_indent} est. cost : {:.4}\n",
378 subplan.estimated_cost
379 ));
380 }
381 self.format_node_text(&subplan.root, out, depth + 1);
382 }
383 PlanNode::MergeJoin {
384 left,
385 right,
386 join_vars,
387 } => {
388 out.push_str(&format!(
389 "{prefix}MergeJoin [key: {}]\n",
390 join_vars.join(", ")
391 ));
392 self.format_node_text(left, out, depth + 1);
393 self.format_node_text(right, out, depth + 1);
394 }
395 PlanNode::ValuesScan { vars, row_count } => {
396 out.push_str(&format!(
397 "{prefix}ValuesScan [vars: {}] [{row_count} rows]\n",
398 vars.join(", ")
399 ));
400 }
401 PlanNode::NamedGraph { graph, child } => {
402 out.push_str(&format!("{prefix}NamedGraph [{graph}]\n"));
403 self.format_node_text(child, out, depth + 1);
404 }
405 }
406 }
407
408 pub fn explain_json(&self, plan: &QueryPlan) -> String {
412 match serde_json::to_string_pretty(plan) {
413 Ok(s) => s,
414 Err(e) => format!("{{\"error\": \"{e}\"}}"),
415 }
416 }
417
418 pub fn explain_dot(&self, plan: &QueryPlan) -> String {
422 let mut state = DotState::default();
423 let mut out = String::new();
424 out.push_str("digraph QueryPlan {\n");
425 out.push_str(" node [shape=box fontname=\"Helvetica\" fontsize=10];\n");
426 out.push_str(" rankdir=TB;\n");
427
428 let root_id = state.next_id();
430 if self.show_costs {
431 out.push_str(&format!(
432 " {root_id} [label=\"QueryPlan\\ncost={:.4}\\ncard={}\"];\n",
433 plan.estimated_cost, plan.estimated_cardinality
434 ));
435 } else {
436 out.push_str(&format!(" {root_id} [label=\"QueryPlan\"];\n"));
437 }
438
439 let child_id = self.emit_dot_node(&plan.root, &mut state, &mut out);
440 out.push_str(&format!(" {root_id} -> {child_id};\n"));
441
442 out.push_str("}\n");
443 out
444 }
445
446 fn emit_dot_node(&self, node: &PlanNode, state: &mut DotState, out: &mut String) -> usize {
447 let id = state.next_id();
448 match node {
449 PlanNode::TripleScan {
450 pattern,
451 index_used,
452 estimated_rows,
453 } => {
454 let label = if self.show_estimates {
455 format!("TripleScan\\n{pattern}\\nidx={index_used}\\nrows={estimated_rows}")
456 } else {
457 format!("TripleScan\\n{pattern}\\nidx={index_used}")
458 };
459 out.push_str(&format!(" {id} [label=\"{label}\"];\n"));
460 }
461 PlanNode::HashJoin {
462 left,
463 right,
464 join_vars,
465 } => {
466 let vars = join_vars.join(",");
467 out.push_str(&format!(" {id} [label=\"HashJoin\\nkey={vars}\"];\n"));
468 let l = self.emit_dot_node(left, state, out);
469 let r = self.emit_dot_node(right, state, out);
470 out.push_str(&format!(" {id} -> {l} [label=\"left\"];\n"));
471 out.push_str(&format!(" {id} -> {r} [label=\"right\"];\n"));
472 }
473 PlanNode::NestedLoopJoin { outer, inner } => {
474 out.push_str(&format!(" {id} [label=\"NestedLoopJoin\"];\n"));
475 let o = self.emit_dot_node(outer, state, out);
476 let i = self.emit_dot_node(inner, state, out);
477 out.push_str(&format!(" {id} -> {o} [label=\"outer\"];\n"));
478 out.push_str(&format!(" {id} -> {i} [label=\"inner\"];\n"));
479 }
480 PlanNode::Filter { expr, child } => {
481 out.push_str(&format!(" {id} [label=\"Filter\\n{expr}\"];\n"));
482 let c = self.emit_dot_node(child, state, out);
483 out.push_str(&format!(" {id} -> {c};\n"));
484 }
485 PlanNode::Sort { vars, child } => {
486 let keys = vars.join(",");
487 out.push_str(&format!(" {id} [label=\"Sort\\n{keys}\"];\n"));
488 let c = self.emit_dot_node(child, state, out);
489 out.push_str(&format!(" {id} -> {c};\n"));
490 }
491 PlanNode::Limit {
492 limit,
493 offset,
494 child,
495 } => {
496 out.push_str(&format!(
497 " {id} [label=\"Limit {limit}\\noffset {offset}\"];\n"
498 ));
499 let c = self.emit_dot_node(child, state, out);
500 out.push_str(&format!(" {id} -> {c};\n"));
501 }
502 PlanNode::Distinct { child } => {
503 out.push_str(&format!(" {id} [label=\"Distinct\"];\n"));
504 let c = self.emit_dot_node(child, state, out);
505 out.push_str(&format!(" {id} -> {c};\n"));
506 }
507 PlanNode::Union { left, right } => {
508 out.push_str(&format!(" {id} [label=\"Union\"];\n"));
509 let l = self.emit_dot_node(left, state, out);
510 let r = self.emit_dot_node(right, state, out);
511 out.push_str(&format!(" {id} -> {l} [label=\"left\"];\n"));
512 out.push_str(&format!(" {id} -> {r} [label=\"right\"];\n"));
513 }
514 PlanNode::Optional { left, right } => {
515 out.push_str(&format!(" {id} [label=\"Optional\"];\n"));
516 let l = self.emit_dot_node(left, state, out);
517 let r = self.emit_dot_node(right, state, out);
518 out.push_str(&format!(" {id} -> {l} [label=\"left\"];\n"));
519 out.push_str(&format!(" {id} -> {r} [label=\"right\"];\n"));
520 }
521 PlanNode::Aggregate {
522 group_by,
523 aggs,
524 child,
525 } => {
526 let gb = group_by.join(",");
527 let ag = aggs.join(",");
528 out.push_str(&format!(
529 " {id} [label=\"Aggregate\\ngroup={gb}\\naggs={ag}\"];\n"
530 ));
531 let c = self.emit_dot_node(child, state, out);
532 out.push_str(&format!(" {id} -> {c};\n"));
533 }
534 PlanNode::Subquery { plan } => {
535 out.push_str(&format!(" {id} [label=\"Subquery\"];\n"));
536 let c = self.emit_dot_node(&plan.root, state, out);
537 out.push_str(&format!(" {id} -> {c};\n"));
538 }
539 PlanNode::PropertyPath {
540 subject,
541 path,
542 object,
543 } => {
544 out.push_str(&format!(
545 " {id} [label=\"PropertyPath\\n{subject} {path} {object}\"];\n"
546 ));
547 }
548 PlanNode::Service { endpoint, subplan } => {
549 out.push_str(&format!(" {id} [label=\"Service\\n{endpoint}\"];\n"));
550 let c = self.emit_dot_node(&subplan.root, state, out);
551 out.push_str(&format!(" {id} -> {c};\n"));
552 }
553 PlanNode::MergeJoin {
554 left,
555 right,
556 join_vars,
557 } => {
558 let vars = join_vars.join(",");
559 out.push_str(&format!(" {id} [label=\"MergeJoin\\nkey={vars}\"];\n"));
560 let l = self.emit_dot_node(left, state, out);
561 let r = self.emit_dot_node(right, state, out);
562 out.push_str(&format!(" {id} -> {l} [label=\"left\"];\n"));
563 out.push_str(&format!(" {id} -> {r} [label=\"right\"];\n"));
564 }
565 PlanNode::ValuesScan { vars, row_count } => {
566 let v = vars.join(",");
567 out.push_str(&format!(
568 " {id} [label=\"ValuesScan\\nvars={v}\\nrows={row_count}\"];\n"
569 ));
570 }
571 PlanNode::NamedGraph { graph, child } => {
572 out.push_str(&format!(" {id} [label=\"NamedGraph\\n{graph}\"];\n"));
573 let c = self.emit_dot_node(child, state, out);
574 out.push_str(&format!(" {id} -> {c};\n"));
575 }
576 }
577 id
578 }
579}
580
581#[derive(Debug, Clone, Default)]
585pub struct QueryExplainerBuilder {
586 show_estimates: Option<bool>,
587 show_costs: Option<bool>,
588 format: Option<ExplainFormat>,
589}
590
591impl QueryExplainerBuilder {
592 pub fn show_estimates(mut self, val: bool) -> Self {
594 self.show_estimates = Some(val);
595 self
596 }
597
598 pub fn show_costs(mut self, val: bool) -> Self {
600 self.show_costs = Some(val);
601 self
602 }
603
604 pub fn format(mut self, fmt: ExplainFormat) -> Self {
606 self.format = Some(fmt);
607 self
608 }
609
610 pub fn build(self) -> QueryExplainer {
612 QueryExplainer {
613 show_estimates: self.show_estimates.unwrap_or(true),
614 show_costs: self.show_costs.unwrap_or(true),
615 format: self.format.unwrap_or(ExplainFormat::Text),
616 }
617 }
618}
619
620#[derive(Debug, Default)]
623struct DotState {
624 counter: usize,
625}
626
627impl DotState {
628 fn next_id(&mut self) -> usize {
629 self.counter += 1;
630 self.counter
631 }
632}
633
634impl PlanNode {
637 pub fn triple_scan(pattern: impl Into<String>, index: IndexType, rows: u64) -> Self {
639 Self::TripleScan {
640 pattern: pattern.into(),
641 index_used: index,
642 estimated_rows: rows,
643 }
644 }
645
646 pub fn hash_join(left: PlanNode, right: PlanNode, vars: Vec<String>) -> Self {
648 Self::HashJoin {
649 left: Box::new(left),
650 right: Box::new(right),
651 join_vars: vars,
652 }
653 }
654
655 pub fn filter(expr: impl Into<String>, child: PlanNode) -> Self {
657 Self::Filter {
658 expr: expr.into(),
659 child: Box::new(child),
660 }
661 }
662
663 pub fn sort(vars: Vec<String>, child: PlanNode) -> Self {
665 Self::Sort {
666 vars,
667 child: Box::new(child),
668 }
669 }
670
671 pub fn limit(limit: usize, offset: usize, child: PlanNode) -> Self {
673 Self::Limit {
674 limit,
675 offset,
676 child: Box::new(child),
677 }
678 }
679
680 pub fn distinct(child: PlanNode) -> Self {
682 Self::Distinct {
683 child: Box::new(child),
684 }
685 }
686
687 pub fn union(left: PlanNode, right: PlanNode) -> Self {
689 Self::Union {
690 left: Box::new(left),
691 right: Box::new(right),
692 }
693 }
694
695 pub fn optional(left: PlanNode, right: PlanNode) -> Self {
697 Self::Optional {
698 left: Box::new(left),
699 right: Box::new(right),
700 }
701 }
702
703 pub fn node_count(&self) -> usize {
705 match self {
706 Self::TripleScan { .. } | Self::PropertyPath { .. } | Self::ValuesScan { .. } => 1,
707 Self::Distinct { child }
708 | Self::Filter { child, .. }
709 | Self::Sort { child, .. }
710 | Self::Limit { child, .. }
711 | Self::NamedGraph { child, .. } => 1 + child.node_count(),
712 Self::HashJoin { left, right, .. }
713 | Self::NestedLoopJoin {
714 outer: left,
715 inner: right,
716 }
717 | Self::Union { left, right }
718 | Self::Optional { left, right }
719 | Self::MergeJoin { left, right, .. } => 1 + left.node_count() + right.node_count(),
720 Self::Aggregate { child, .. } => 1 + child.node_count(),
721 Self::Subquery { plan } => 1 + plan.root.node_count(),
722 Self::Service { subplan, .. } => 1 + subplan.root.node_count(),
723 }
724 }
725
726 pub fn depth(&self) -> usize {
728 match self {
729 Self::TripleScan { .. } | Self::PropertyPath { .. } | Self::ValuesScan { .. } => 0,
730 Self::Distinct { child }
731 | Self::Filter { child, .. }
732 | Self::Sort { child, .. }
733 | Self::Limit { child, .. }
734 | Self::NamedGraph { child, .. }
735 | Self::Aggregate { child, .. } => 1 + child.depth(),
736 Self::HashJoin { left, right, .. }
737 | Self::NestedLoopJoin {
738 outer: left,
739 inner: right,
740 }
741 | Self::Union { left, right }
742 | Self::Optional { left, right }
743 | Self::MergeJoin { left, right, .. } => 1 + left.depth().max(right.depth()),
744 Self::Subquery { plan } => 1 + plan.root.depth(),
745 Self::Service { subplan, .. } => 1 + subplan.root.depth(),
746 }
747 }
748}
749
750#[cfg(test)]
753mod tests {
754 use super::*;
755
756 fn make_scan(pattern: &str, index: IndexType, rows: u64) -> PlanNode {
757 PlanNode::triple_scan(pattern, index, rows)
758 }
759
760 fn make_plan(root: PlanNode) -> QueryPlan {
761 let cardinality = 100;
762 QueryPlan {
763 estimated_cost: 5.0,
764 estimated_cardinality: cardinality,
765 root,
766 }
767 }
768
769 #[test]
772 fn test_triple_scan_construction() {
773 let node = make_scan("?s rdf:type ?t", IndexType::Spo, 500);
774 if let PlanNode::TripleScan {
775 pattern,
776 index_used,
777 estimated_rows,
778 } = &node
779 {
780 assert_eq!(pattern, "?s rdf:type ?t");
781 assert_eq!(*index_used, IndexType::Spo);
782 assert_eq!(*estimated_rows, 500);
783 } else {
784 panic!("expected TripleScan");
785 }
786 }
787
788 #[test]
789 fn test_hash_join_construction() {
790 let left = make_scan("?s ?p ?o", IndexType::FullScan, 1000);
791 let right = make_scan("?s foaf:name ?n", IndexType::Spo, 50);
792 let join = PlanNode::hash_join(left, right, vec!["?s".to_string()]);
793 assert!(matches!(join, PlanNode::HashJoin { .. }));
794 }
795
796 #[test]
797 fn test_nested_loop_join_construction() {
798 let outer = make_scan("?s a ?t", IndexType::Pos, 10);
799 let inner = make_scan("?s ?p ?o", IndexType::Spo, 5);
800 let node = PlanNode::NestedLoopJoin {
801 outer: Box::new(outer),
802 inner: Box::new(inner),
803 };
804 assert!(matches!(node, PlanNode::NestedLoopJoin { .. }));
805 }
806
807 #[test]
808 fn test_filter_construction() {
809 let scan = make_scan("?s ?p ?o", IndexType::FullScan, 100);
810 let node = PlanNode::filter("?o > 5", scan);
811 if let PlanNode::Filter { expr, .. } = &node {
812 assert_eq!(expr, "?o > 5");
813 } else {
814 panic!("expected Filter");
815 }
816 }
817
818 #[test]
819 fn test_sort_construction() {
820 let scan = make_scan("?s ?p ?o", IndexType::FullScan, 100);
821 let node = PlanNode::sort(vec!["+?s".into(), "-?o".into()], scan);
822 if let PlanNode::Sort { vars, .. } = &node {
823 assert_eq!(vars, &["+?s", "-?o"]);
824 } else {
825 panic!("expected Sort");
826 }
827 }
828
829 #[test]
830 fn test_limit_construction() {
831 let scan = make_scan("?s ?p ?o", IndexType::FullScan, 100);
832 let node = PlanNode::limit(10, 20, scan);
833 if let PlanNode::Limit { limit, offset, .. } = &node {
834 assert_eq!(*limit, 10);
835 assert_eq!(*offset, 20);
836 } else {
837 panic!("expected Limit");
838 }
839 }
840
841 #[test]
842 fn test_distinct_construction() {
843 let scan = make_scan("?s ?p ?o", IndexType::FullScan, 100);
844 let node = PlanNode::distinct(scan);
845 assert!(matches!(node, PlanNode::Distinct { .. }));
846 }
847
848 #[test]
849 fn test_union_construction() {
850 let left = make_scan("?s a owl:Class", IndexType::Pos, 30);
851 let right = make_scan("?s a rdfs:Class", IndexType::Pos, 10);
852 let node = PlanNode::union(left, right);
853 assert!(matches!(node, PlanNode::Union { .. }));
854 }
855
856 #[test]
857 fn test_optional_construction() {
858 let main = make_scan("?s foaf:name ?n", IndexType::Spo, 200);
859 let opt = make_scan("?s foaf:mbox ?m", IndexType::Spo, 80);
860 let node = PlanNode::optional(main, opt);
861 assert!(matches!(node, PlanNode::Optional { .. }));
862 }
863
864 #[test]
865 fn test_aggregate_construction() {
866 let scan = make_scan("?s ?p ?o", IndexType::FullScan, 1000);
867 let node = PlanNode::Aggregate {
868 group_by: vec!["?s".into()],
869 aggs: vec!["COUNT(?o) AS ?cnt".into()],
870 child: Box::new(scan),
871 };
872 if let PlanNode::Aggregate { aggs, group_by, .. } = &node {
873 assert_eq!(group_by[0], "?s");
874 assert!(aggs[0].contains("COUNT"));
875 } else {
876 panic!("expected Aggregate");
877 }
878 }
879
880 #[test]
881 fn test_subquery_construction() {
882 let inner_plan = make_plan(make_scan("?x ?y ?z", IndexType::FullScan, 5));
883 let node = PlanNode::Subquery {
884 plan: Box::new(inner_plan),
885 };
886 assert!(matches!(node, PlanNode::Subquery { .. }));
887 }
888
889 #[test]
890 fn test_property_path_construction() {
891 let node = PlanNode::PropertyPath {
892 subject: "?s".into(),
893 path: "foaf:knows+".into(),
894 object: "?o".into(),
895 };
896 if let PlanNode::PropertyPath { path, .. } = &node {
897 assert!(path.contains("foaf"));
898 } else {
899 panic!("expected PropertyPath");
900 }
901 }
902
903 #[test]
904 fn test_service_construction() {
905 let sub = make_plan(make_scan("?s ?p ?o", IndexType::FullScan, 50));
906 let node = PlanNode::Service {
907 endpoint: "http://remote.example.org/sparql".into(),
908 subplan: Box::new(sub),
909 };
910 if let PlanNode::Service { endpoint, .. } = &node {
911 assert!(endpoint.contains("remote"));
912 } else {
913 panic!("expected Service");
914 }
915 }
916
917 #[test]
918 fn test_merge_join_construction() {
919 let left = make_scan("?s ?p ?o", IndexType::Spo, 500);
920 let right = make_scan("?s a ?t", IndexType::Pos, 100);
921 let node = PlanNode::MergeJoin {
922 left: Box::new(left),
923 right: Box::new(right),
924 join_vars: vec!["?s".into()],
925 };
926 assert!(matches!(node, PlanNode::MergeJoin { .. }));
927 }
928
929 #[test]
930 fn test_values_scan_construction() {
931 let node = PlanNode::ValuesScan {
932 vars: vec!["?s".into(), "?p".into()],
933 row_count: 3,
934 };
935 if let PlanNode::ValuesScan { row_count, .. } = &node {
936 assert_eq!(*row_count, 3);
937 } else {
938 panic!("expected ValuesScan");
939 }
940 }
941
942 #[test]
943 fn test_named_graph_construction() {
944 let scan = make_scan("?s ?p ?o", IndexType::FullScan, 100);
945 let node = PlanNode::NamedGraph {
946 graph: "http://example.org/g1".into(),
947 child: Box::new(scan),
948 };
949 if let PlanNode::NamedGraph { graph, .. } = &node {
950 assert!(graph.contains("g1"));
951 } else {
952 panic!("expected NamedGraph");
953 }
954 }
955
956 #[test]
959 fn test_node_count_leaf() {
960 let node = make_scan("?s ?p ?o", IndexType::Spo, 0);
961 assert_eq!(node.node_count(), 1);
962 }
963
964 #[test]
965 fn test_node_count_nested() {
966 let left = make_scan("?s a ?t", IndexType::Pos, 10);
967 let right = make_scan("?s ?p ?o", IndexType::Spo, 5);
968 let join = PlanNode::hash_join(left, right, vec![]);
969 assert_eq!(join.node_count(), 3);
970 }
971
972 #[test]
973 fn test_node_count_deep() {
974 let scan = make_scan("?s ?p ?o", IndexType::FullScan, 100);
975 let filter = PlanNode::filter("?o > 0", scan);
976 let sort = PlanNode::sort(vec!["?s".into()], filter);
977 let limit = PlanNode::limit(10, 0, sort);
978 assert_eq!(limit.node_count(), 4);
979 }
980
981 #[test]
982 fn test_depth_leaf() {
983 let node = make_scan("?s ?p ?o", IndexType::Spo, 0);
984 assert_eq!(node.depth(), 0);
985 }
986
987 #[test]
988 fn test_depth_chain() {
989 let scan = make_scan("?s ?p ?o", IndexType::FullScan, 10);
990 let filter = PlanNode::filter("true", scan);
991 let sort = PlanNode::sort(vec![], filter);
992 assert_eq!(sort.depth(), 2);
993 }
994
995 #[test]
996 fn test_depth_join() {
997 let left = make_scan("?s a ?t", IndexType::Pos, 10);
998 let right = make_scan("?s ?p ?o", IndexType::Spo, 5);
999 let join = PlanNode::hash_join(left, right, vec![]);
1000 assert_eq!(join.depth(), 1);
1001 }
1002
1003 #[test]
1006 fn test_index_type_display_spo() {
1007 assert_eq!(IndexType::Spo.to_string(), "SPO");
1008 }
1009
1010 #[test]
1011 fn test_index_type_display_pos() {
1012 assert_eq!(IndexType::Pos.to_string(), "POS");
1013 }
1014
1015 #[test]
1016 fn test_index_type_display_osp() {
1017 assert_eq!(IndexType::Osp.to_string(), "OSP");
1018 }
1019
1020 #[test]
1021 fn test_index_type_display_fullscan() {
1022 assert_eq!(IndexType::FullScan.to_string(), "FULL_SCAN");
1023 }
1024
1025 #[test]
1028 fn test_explain_text_contains_header() {
1029 let plan = make_plan(make_scan("?s ?p ?o", IndexType::Spo, 10));
1030 let out = QueryExplainer::new().explain_text(&plan);
1031 assert!(out.contains("Query Plan"));
1032 assert!(out.contains("=========="));
1033 }
1034
1035 #[test]
1036 fn test_explain_text_contains_cost() {
1037 let plan = make_plan(make_scan("?s ?p ?o", IndexType::Spo, 10));
1038 let out = QueryExplainer::new().explain_text(&plan);
1039 assert!(out.contains("5.0000"));
1040 }
1041
1042 #[test]
1043 fn test_explain_text_contains_cardinality() {
1044 let plan = make_plan(make_scan("?s ?p ?o", IndexType::Spo, 10));
1045 let out = QueryExplainer::new().explain_text(&plan);
1046 assert!(out.contains("100"));
1047 }
1048
1049 #[test]
1050 fn test_explain_text_triple_scan() {
1051 let plan = make_plan(make_scan("?s rdf:type owl:Class", IndexType::Pos, 42));
1052 let out = QueryExplainer::new().explain_text(&plan);
1053 assert!(out.contains("TripleScan"));
1054 assert!(out.contains("owl:Class"));
1055 assert!(out.contains("POS"));
1056 assert!(out.contains("42"));
1057 }
1058
1059 #[test]
1060 fn test_explain_text_hash_join() {
1061 let left = make_scan("?s a ?t", IndexType::Pos, 10);
1062 let right = make_scan("?s ?p ?o", IndexType::Spo, 5);
1063 let join = PlanNode::hash_join(left, right, vec!["?s".into()]);
1064 let plan = make_plan(join);
1065 let out = QueryExplainer::new().explain_text(&plan);
1066 assert!(out.contains("HashJoin"));
1067 assert!(out.contains("?s"));
1068 }
1069
1070 #[test]
1071 fn test_explain_text_filter() {
1072 let scan = make_scan("?s ?p ?o", IndexType::FullScan, 100);
1073 let filtered = PlanNode::filter("?o > 10", scan);
1074 let plan = make_plan(filtered);
1075 let out = QueryExplainer::new().explain_text(&plan);
1076 assert!(out.contains("Filter"));
1077 assert!(out.contains("?o > 10"));
1078 }
1079
1080 #[test]
1081 fn test_explain_text_sort() {
1082 let scan = make_scan("?s ?p ?o", IndexType::FullScan, 100);
1083 let sorted = PlanNode::sort(vec!["+?s".into()], scan);
1084 let plan = make_plan(sorted);
1085 let out = QueryExplainer::new().explain_text(&plan);
1086 assert!(out.contains("Sort"));
1087 assert!(out.contains("+?s"));
1088 }
1089
1090 #[test]
1091 fn test_explain_text_limit() {
1092 let scan = make_scan("?s ?p ?o", IndexType::FullScan, 100);
1093 let limited = PlanNode::limit(25, 0, scan);
1094 let plan = make_plan(limited);
1095 let out = QueryExplainer::new().explain_text(&plan);
1096 assert!(out.contains("Limit"));
1097 assert!(out.contains("25"));
1098 }
1099
1100 #[test]
1101 fn test_explain_text_distinct() {
1102 let scan = make_scan("?s ?p ?o", IndexType::FullScan, 100);
1103 let node = PlanNode::distinct(scan);
1104 let plan = make_plan(node);
1105 let out = QueryExplainer::new().explain_text(&plan);
1106 assert!(out.contains("Distinct"));
1107 }
1108
1109 #[test]
1110 fn test_explain_text_union() {
1111 let left = make_scan("?s a owl:Class", IndexType::Pos, 30);
1112 let right = make_scan("?s a rdfs:Class", IndexType::Pos, 10);
1113 let node = PlanNode::union(left, right);
1114 let plan = make_plan(node);
1115 let out = QueryExplainer::new().explain_text(&plan);
1116 assert!(out.contains("Union"));
1117 }
1118
1119 #[test]
1120 fn test_explain_text_optional() {
1121 let main = make_scan("?s foaf:name ?n", IndexType::Spo, 200);
1122 let opt = make_scan("?s foaf:mbox ?m", IndexType::Spo, 80);
1123 let node = PlanNode::optional(main, opt);
1124 let plan = make_plan(node);
1125 let out = QueryExplainer::new().explain_text(&plan);
1126 assert!(out.contains("Optional"));
1127 }
1128
1129 #[test]
1130 fn test_explain_text_aggregate() {
1131 let scan = make_scan("?s ?p ?o", IndexType::FullScan, 1000);
1132 let node = PlanNode::Aggregate {
1133 group_by: vec!["?s".into()],
1134 aggs: vec!["COUNT(?o) AS ?cnt".into()],
1135 child: Box::new(scan),
1136 };
1137 let plan = make_plan(node);
1138 let out = QueryExplainer::new().explain_text(&plan);
1139 assert!(out.contains("Aggregate"));
1140 assert!(out.contains("COUNT"));
1141 }
1142
1143 #[test]
1144 fn test_explain_text_subquery() {
1145 let inner = make_plan(make_scan("?x ?y ?z", IndexType::FullScan, 5));
1146 let node = PlanNode::Subquery {
1147 plan: Box::new(inner),
1148 };
1149 let plan = make_plan(node);
1150 let out = QueryExplainer::new().explain_text(&plan);
1151 assert!(out.contains("Subquery"));
1152 }
1153
1154 #[test]
1155 fn test_explain_text_property_path() {
1156 let node = PlanNode::PropertyPath {
1157 subject: "?s".into(),
1158 path: "foaf:knows+".into(),
1159 object: "?o".into(),
1160 };
1161 let plan = make_plan(node);
1162 let out = QueryExplainer::new().explain_text(&plan);
1163 assert!(out.contains("PropertyPath"));
1164 assert!(out.contains("foaf:knows+"));
1165 }
1166
1167 #[test]
1168 fn test_explain_text_service() {
1169 let sub = make_plan(make_scan("?s ?p ?o", IndexType::FullScan, 50));
1170 let node = PlanNode::Service {
1171 endpoint: "http://remote.example.org/sparql".into(),
1172 subplan: Box::new(sub),
1173 };
1174 let plan = make_plan(node);
1175 let out = QueryExplainer::new().explain_text(&plan);
1176 assert!(out.contains("Service"));
1177 assert!(out.contains("remote.example.org"));
1178 }
1179
1180 #[test]
1181 fn test_explain_text_merge_join() {
1182 let left = make_scan("?s ?p ?o", IndexType::Spo, 500);
1183 let right = make_scan("?s a ?t", IndexType::Pos, 100);
1184 let node = PlanNode::MergeJoin {
1185 left: Box::new(left),
1186 right: Box::new(right),
1187 join_vars: vec!["?s".into()],
1188 };
1189 let plan = make_plan(node);
1190 let out = QueryExplainer::new().explain_text(&plan);
1191 assert!(out.contains("MergeJoin"));
1192 }
1193
1194 #[test]
1195 fn test_explain_text_values_scan() {
1196 let node = PlanNode::ValuesScan {
1197 vars: vec!["?s".into()],
1198 row_count: 7,
1199 };
1200 let plan = make_plan(node);
1201 let out = QueryExplainer::new().explain_text(&plan);
1202 assert!(out.contains("ValuesScan"));
1203 assert!(out.contains("7"));
1204 }
1205
1206 #[test]
1207 fn test_explain_text_named_graph() {
1208 let scan = make_scan("?s ?p ?o", IndexType::FullScan, 50);
1209 let node = PlanNode::NamedGraph {
1210 graph: "http://example.org/g1".into(),
1211 child: Box::new(scan),
1212 };
1213 let plan = make_plan(node);
1214 let out = QueryExplainer::new().explain_text(&plan);
1215 assert!(out.contains("NamedGraph"));
1216 assert!(out.contains("g1"));
1217 }
1218
1219 #[test]
1222 fn test_explain_json_is_valid() {
1223 let plan = make_plan(make_scan("?s ?p ?o", IndexType::Spo, 10));
1224 let out = QueryExplainer::new().explain_json(&plan);
1225 let parsed: serde_json::Value = serde_json::from_str(&out).expect("invalid JSON");
1226 assert!(parsed.is_object());
1227 }
1228
1229 #[test]
1230 fn test_explain_json_contains_type_field() {
1231 let plan = make_plan(make_scan("?s ?p ?o", IndexType::Spo, 10));
1232 let out = QueryExplainer::new().explain_json(&plan);
1233 assert!(out.contains("\"type\""));
1234 }
1235
1236 #[test]
1237 fn test_explain_json_triple_scan_fields() {
1238 let plan = make_plan(make_scan("?s rdf:type owl:Class", IndexType::Pos, 42));
1239 let out = QueryExplainer::new().explain_json(&plan);
1240 assert!(out.contains("triple_scan"));
1241 assert!(out.contains("owl:Class"));
1242 assert!(out.contains("POS"));
1243 assert!(out.contains("42"));
1244 }
1245
1246 #[test]
1247 fn test_explain_json_hash_join() {
1248 let left = make_scan("?s a ?t", IndexType::Pos, 10);
1249 let right = make_scan("?s ?p ?o", IndexType::Spo, 5);
1250 let join = PlanNode::hash_join(left, right, vec!["?s".into()]);
1251 let plan = make_plan(join);
1252 let out = QueryExplainer::new().explain_json(&plan);
1253 assert!(out.contains("hash_join"));
1254 assert!(out.contains("join_vars"));
1255 }
1256
1257 #[test]
1258 fn test_explain_json_filter() {
1259 let scan = make_scan("?s ?p ?o", IndexType::FullScan, 100);
1260 let node = PlanNode::filter("?o > 10", scan);
1261 let plan = make_plan(node);
1262 let out = QueryExplainer::new().explain_json(&plan);
1263 assert!(out.contains("filter"));
1264 assert!(out.contains("expr"));
1265 }
1266
1267 #[test]
1268 fn test_explain_json_estimated_cost() {
1269 let plan = make_plan(make_scan("?s ?p ?o", IndexType::Spo, 10));
1270 let out = QueryExplainer::new().explain_json(&plan);
1271 assert!(out.contains("estimated_cost"));
1272 assert!(out.contains("5.0"));
1273 }
1274
1275 #[test]
1276 fn test_explain_json_aggregate() {
1277 let scan = make_scan("?s ?p ?o", IndexType::FullScan, 1000);
1278 let node = PlanNode::Aggregate {
1279 group_by: vec!["?s".into()],
1280 aggs: vec!["SUM(?v) AS ?total".into()],
1281 child: Box::new(scan),
1282 };
1283 let plan = make_plan(node);
1284 let out = QueryExplainer::new().explain_json(&plan);
1285 assert!(out.contains("aggregate"));
1286 assert!(out.contains("group_by"));
1287 assert!(out.contains("aggs"));
1288 }
1289
1290 #[test]
1291 fn test_explain_json_union() {
1292 let left = make_scan("?s a owl:Class", IndexType::Pos, 30);
1293 let right = make_scan("?s a rdfs:Class", IndexType::Pos, 10);
1294 let node = PlanNode::union(left, right);
1295 let plan = make_plan(node);
1296 let out = QueryExplainer::new().explain_json(&plan);
1297 assert!(out.contains("union"));
1298 }
1299
1300 #[test]
1303 fn test_explain_dot_starts_with_digraph() {
1304 let plan = make_plan(make_scan("?s ?p ?o", IndexType::Spo, 10));
1305 let out = QueryExplainer::new().explain_dot(&plan);
1306 assert!(out.starts_with("digraph QueryPlan {"));
1307 }
1308
1309 #[test]
1310 fn test_explain_dot_ends_with_brace() {
1311 let plan = make_plan(make_scan("?s ?p ?o", IndexType::Spo, 10));
1312 let out = QueryExplainer::new().explain_dot(&plan);
1313 assert!(out.trim().ends_with('}'));
1314 }
1315
1316 #[test]
1317 fn test_explain_dot_contains_triple_scan() {
1318 let plan = make_plan(make_scan("?s a owl:Class", IndexType::Pos, 42));
1319 let out = QueryExplainer::new().explain_dot(&plan);
1320 assert!(out.contains("TripleScan"));
1321 }
1322
1323 #[test]
1324 fn test_explain_dot_contains_edge_arrows() {
1325 let left = make_scan("?s a ?t", IndexType::Pos, 10);
1326 let right = make_scan("?s ?p ?o", IndexType::Spo, 5);
1327 let join = PlanNode::hash_join(left, right, vec!["?s".into()]);
1328 let plan = make_plan(join);
1329 let out = QueryExplainer::new().explain_dot(&plan);
1330 assert!(out.contains("->"));
1331 }
1332
1333 #[test]
1334 fn test_explain_dot_hash_join_labels() {
1335 let left = make_scan("?s a ?t", IndexType::Pos, 10);
1336 let right = make_scan("?s ?p ?o", IndexType::Spo, 5);
1337 let join = PlanNode::hash_join(left, right, vec!["?s".into()]);
1338 let plan = make_plan(join);
1339 let out = QueryExplainer::new().explain_dot(&plan);
1340 assert!(out.contains("HashJoin"));
1341 assert!(out.contains("left"));
1342 assert!(out.contains("right"));
1343 }
1344
1345 #[test]
1346 fn test_explain_dot_filter() {
1347 let scan = make_scan("?s ?p ?o", IndexType::FullScan, 100);
1348 let node = PlanNode::filter("?o > 10", scan);
1349 let plan = make_plan(node);
1350 let out = QueryExplainer::new().explain_dot(&plan);
1351 assert!(out.contains("Filter"));
1352 }
1353
1354 #[test]
1355 fn test_explain_dot_union() {
1356 let left = make_scan("?s a owl:Class", IndexType::Pos, 30);
1357 let right = make_scan("?s a rdfs:Class", IndexType::Pos, 10);
1358 let node = PlanNode::union(left, right);
1359 let plan = make_plan(node);
1360 let out = QueryExplainer::new().explain_dot(&plan);
1361 assert!(out.contains("Union"));
1362 }
1363
1364 #[test]
1365 fn test_explain_dot_service() {
1366 let sub = make_plan(make_scan("?s ?p ?o", IndexType::FullScan, 50));
1367 let node = PlanNode::Service {
1368 endpoint: "http://remote.example.org/sparql".into(),
1369 subplan: Box::new(sub),
1370 };
1371 let plan = make_plan(node);
1372 let out = QueryExplainer::new().explain_dot(&plan);
1373 assert!(out.contains("Service"));
1374 }
1375
1376 #[test]
1379 fn test_explain_with_format_text() {
1380 let plan = make_plan(make_scan("?s ?p ?o", IndexType::Spo, 10));
1381 let exp = QueryExplainer::new();
1382 let out = exp.explain_with_format(&plan, ExplainFormat::Text);
1383 assert!(out.contains("Query Plan"));
1384 }
1385
1386 #[test]
1387 fn test_explain_with_format_json() {
1388 let plan = make_plan(make_scan("?s ?p ?o", IndexType::Spo, 10));
1389 let exp = QueryExplainer::new();
1390 let out = exp.explain_with_format(&plan, ExplainFormat::Json);
1391 let v: serde_json::Value = serde_json::from_str(&out).expect("valid JSON");
1392 assert!(v.is_object());
1393 }
1394
1395 #[test]
1396 fn test_explain_with_format_dot() {
1397 let plan = make_plan(make_scan("?s ?p ?o", IndexType::Spo, 10));
1398 let exp = QueryExplainer::new();
1399 let out = exp.explain_with_format(&plan, ExplainFormat::Dot);
1400 assert!(out.contains("digraph"));
1401 }
1402
1403 #[test]
1406 fn test_builder_default() {
1407 let exp = QueryExplainer::builder().build();
1408 assert!(exp.show_estimates);
1409 assert!(exp.show_costs);
1410 assert_eq!(exp.format, ExplainFormat::Text);
1411 }
1412
1413 #[test]
1414 fn test_builder_no_estimates() {
1415 let exp = QueryExplainer::builder().show_estimates(false).build();
1416 let plan = make_plan(make_scan("?s ?p ?o", IndexType::Spo, 999));
1417 let out = exp.explain_text(&plan);
1418 assert!(!out.contains("999"));
1420 }
1421
1422 #[test]
1423 fn test_builder_no_costs() {
1424 let exp = QueryExplainer::builder().show_costs(false).build();
1425 let plan = make_plan(make_scan("?s ?p ?o", IndexType::Spo, 10));
1426 let out = exp.explain_text(&plan);
1427 assert!(!out.contains("5.0000"));
1428 }
1429
1430 #[test]
1431 fn test_builder_json_format() {
1432 let exp = QueryExplainer::builder()
1433 .format(ExplainFormat::Json)
1434 .build();
1435 let plan = make_plan(make_scan("?s ?p ?o", IndexType::Spo, 10));
1436 let out = exp.explain(&plan); let v: serde_json::Value = serde_json::from_str(&out).expect("valid JSON");
1438 assert!(v.is_object());
1439 }
1440
1441 #[test]
1442 fn test_builder_dot_format() {
1443 let exp = QueryExplainer::builder().format(ExplainFormat::Dot).build();
1444 let plan = make_plan(make_scan("?s ?p ?o", IndexType::Spo, 10));
1445 let out = exp.explain(&plan);
1446 assert!(out.contains("digraph"));
1447 }
1448
1449 #[test]
1452 fn test_nested_plan_text() {
1453 let s1 = make_scan("?s a ?t", IndexType::Pos, 500);
1455 let s2 = make_scan("?s foaf:name ?n", IndexType::Spo, 200);
1456 let join = PlanNode::hash_join(s1, s2, vec!["?s".into()]);
1457 let filter = PlanNode::filter("LANG(?n) = 'en'", join);
1458 let sort = PlanNode::sort(vec!["+?n".into()], filter);
1459 let distinct = PlanNode::distinct(sort);
1460 let plan = QueryPlan {
1461 root: distinct,
1462 estimated_cost: 42.7,
1463 estimated_cardinality: 150,
1464 };
1465 let out = QueryExplainer::new().explain_text(&plan);
1466 assert!(out.contains("Distinct"));
1467 assert!(out.contains("Sort"));
1468 assert!(out.contains("Filter"));
1469 assert!(out.contains("HashJoin"));
1470 assert!(out.contains("TripleScan"));
1471 }
1472
1473 #[test]
1474 fn test_nested_plan_json_roundtrip() {
1475 let scan = make_scan("?s ?p ?o", IndexType::Spo, 100);
1476 let filter = PlanNode::filter("?o > 0", scan);
1477 let plan = QueryPlan {
1478 root: filter,
1479 estimated_cost: std::f64::consts::PI,
1480 estimated_cardinality: 80,
1481 };
1482 let exp = QueryExplainer::new();
1483 let json = exp.explain_json(&plan);
1484 let decoded: QueryPlan = serde_json::from_str(&json).expect("roundtrip failed");
1485 assert_eq!(decoded.estimated_cardinality, 80);
1486 assert!((decoded.estimated_cost - std::f64::consts::PI).abs() < 1e-9);
1487 }
1488
1489 #[test]
1490 fn test_deeply_nested_node_count() {
1491 let s = make_scan("?s ?p ?o", IndexType::FullScan, 10);
1493 let f = PlanNode::filter("true", s);
1494 let so = PlanNode::sort(vec![], f);
1495 let li = PlanNode::limit(5, 0, so);
1496 let di = PlanNode::distinct(li);
1497 assert_eq!(di.node_count(), 5);
1498 assert_eq!(di.depth(), 4);
1499 }
1500
1501 #[test]
1502 fn test_subquery_in_text() {
1503 let inner_scan = make_scan("?x ?y ?z", IndexType::FullScan, 5);
1504 let inner_plan = QueryPlan {
1505 root: inner_scan,
1506 estimated_cost: 1.0,
1507 estimated_cardinality: 5,
1508 };
1509 let outer_scan = make_scan("?a ?b ?c", IndexType::Spo, 100);
1510 let sub = PlanNode::Subquery {
1511 plan: Box::new(inner_plan),
1512 };
1513 let join = PlanNode::hash_join(outer_scan, sub, vec!["?x".into()]);
1514 let plan = make_plan(join);
1515 let out = QueryExplainer::new().explain_text(&plan);
1516 assert!(out.contains("Subquery"));
1517 assert!(out.contains("HashJoin"));
1518 }
1519}