Skip to main content

velesdb_core/velesql/explain/
formatter.rs

1//! Query plan rendering and formatting for EXPLAIN output.
2//!
3//! Extracted from `explain.rs` for maintainability (04-06 module splitting).
4//! Handles tree rendering, JSON serialization, and Display formatting.
5
6use std::fmt::{self, Write as _};
7
8use super::{FilterStrategy, IndexType, PlanNode, QueryPlan};
9
10impl QueryPlan {
11    /// Renders the plan as a tree string.
12    #[must_use]
13    pub fn to_tree(&self) -> String {
14        let mut output = String::from("Query Plan:\n");
15        Self::render_node(&self.root, &mut output, "", true);
16
17        let _ = write!(
18            output,
19            "\nEstimated cost: {:.3}ms\n",
20            self.estimated_cost_ms
21        );
22
23        if let Some(ref idx) = self.index_used {
24            let _ = writeln!(output, "Index used: {}", idx.as_str());
25        }
26
27        if self.filter_strategy != FilterStrategy::None {
28            let _ = writeln!(output, "Filter strategy: {}", self.filter_strategy.as_str());
29        }
30
31        if let Some(hit) = self.cache_hit {
32            let _ = writeln!(output, "Cache hit: {hit}");
33        }
34        if let Some(count) = self.plan_reuse_count {
35            let _ = writeln!(output, "Plan reuse count: {count}");
36        }
37
38        output
39    }
40
41    pub(crate) fn render_node(node: &PlanNode, output: &mut String, prefix: &str, is_last: bool) {
42        let connector = if is_last { "└─ " } else { "├─ " };
43        let child_prefix = format!("{}{}", prefix, if is_last { "   " } else { "│  " });
44
45        match node {
46            PlanNode::VectorSearch(vs) => {
47                let _ = writeln!(output, "{prefix}{connector}VectorSearch");
48                let _ = writeln!(output, "{child_prefix}├─ Collection: {}", vs.collection);
49                let _ = writeln!(output, "{child_prefix}├─ ef_search: {}", vs.ef_search);
50                let _ = writeln!(output, "{child_prefix}└─ Candidates: {}", vs.candidates);
51            }
52            PlanNode::Filter(f) => {
53                let _ = writeln!(output, "{prefix}{connector}Filter");
54                let _ = writeln!(output, "{child_prefix}├─ Conditions: {}", f.conditions);
55                let _ = writeln!(
56                    output,
57                    "{child_prefix}└─ Selectivity: {:.1}%",
58                    f.selectivity * 100.0
59                );
60            }
61            PlanNode::Limit(l) => {
62                let _ = writeln!(output, "{prefix}{connector}Limit: {}", l.count);
63            }
64            PlanNode::Offset(o) => {
65                let _ = writeln!(output, "{prefix}{connector}Offset: {}", o.count);
66            }
67            PlanNode::TableScan(ts) => {
68                let _ = writeln!(output, "{prefix}{connector}TableScan: {}", ts.collection);
69            }
70            PlanNode::IndexLookup(il) => {
71                let _ = writeln!(
72                    output,
73                    "{prefix}{connector}IndexLookup({}.{})",
74                    il.label, il.property
75                );
76                let _ = writeln!(output, "{child_prefix}└─ Value: {}", il.value);
77            }
78            PlanNode::Sequence(nodes) => {
79                for (i, child) in nodes.iter().enumerate() {
80                    Self::render_node(child, output, prefix, i == nodes.len() - 1);
81                }
82            }
83            PlanNode::MatchTraversal(mt) => {
84                let _ = writeln!(output, "{prefix}{connector}MatchTraversal");
85                let _ = writeln!(output, "{child_prefix}├─ Strategy: {}", mt.strategy);
86                if !mt.start_labels.is_empty() {
87                    let _ = writeln!(
88                        output,
89                        "{child_prefix}├─ Start Labels: [{}]",
90                        mt.start_labels.join(", ")
91                    );
92                }
93                let _ = writeln!(output, "{child_prefix}├─ Max Depth: {}", mt.max_depth);
94                let _ = writeln!(
95                    output,
96                    "{child_prefix}├─ Relationships: {}",
97                    mt.relationship_count
98                );
99                if let Some(threshold) = mt.similarity_threshold {
100                    let _ = writeln!(
101                        output,
102                        "{child_prefix}└─ Similarity Threshold: {:.2}",
103                        threshold
104                    );
105                } else {
106                    let _ = writeln!(
107                        output,
108                        "{child_prefix}└─ Similarity: {}",
109                        if mt.has_similarity { "yes" } else { "no" }
110                    );
111                }
112            }
113        }
114    }
115
116    /// Renders the plan as JSON.
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if serialization fails.
121    pub fn to_json(&self) -> Result<String, serde_json::Error> {
122        serde_json::to_string_pretty(self)
123    }
124}
125
126impl IndexType {
127    /// Returns the index type as a string.
128    #[must_use]
129    pub const fn as_str(&self) -> &'static str {
130        match self {
131            Self::Hnsw => "HNSW",
132            Self::Flat => "Flat",
133            Self::BinaryQuantization => "BinaryQuantization",
134            Self::Property => "PropertyIndex",
135        }
136    }
137}
138
139impl FilterStrategy {
140    /// Returns the filter strategy as a string.
141    #[must_use]
142    pub const fn as_str(&self) -> &'static str {
143        match self {
144            Self::None => "none",
145            Self::PreFilter => "pre-filtering (high selectivity)",
146            Self::PostFilter => "post-filtering (low selectivity)",
147        }
148    }
149}
150
151impl super::super::ast::CompareOp {
152    /// Returns the operator as a string.
153    #[must_use]
154    pub const fn as_str(&self) -> &'static str {
155        match self {
156            Self::Eq => "=",
157            Self::NotEq => "!=",
158            Self::Gt => ">",
159            Self::Gte => ">=",
160            Self::Lt => "<",
161            Self::Lte => "<=",
162        }
163    }
164}
165
166impl fmt::Display for QueryPlan {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        write!(f, "{}", self.to_tree())
169    }
170}