velesdb_core/velesql/explain/
formatter.rs1use std::fmt::{self, Write as _};
7
8use super::{
9 FilterPlan, FilterStrategy, FusionInfo, IndexType, MatchTraversalPlan, PlanNode, QueryPlan,
10};
11
12impl QueryPlan {
13 #[must_use]
15 pub fn to_tree(&self) -> String {
16 let mut output = String::from("Query Plan:\n");
17 Self::render_node(&self.root, &mut output, "", true);
18
19 Self::render_with_options(&self.with_options, &mut output);
20 Self::render_let_bindings(&self.let_bindings, &mut output);
21 Self::render_fusion_info(self.fusion_info.as_ref(), &mut output);
22
23 let _ = write!(
24 output,
25 "\nEstimated cost: {:.3}ms\n",
26 self.estimated_cost_ms
27 );
28
29 if let Some(ref idx) = self.index_used {
30 let _ = writeln!(output, "Index used: {}", idx.as_str());
31 }
32
33 if self.filter_strategy != FilterStrategy::None {
34 let _ = writeln!(output, "Filter strategy: {}", self.filter_strategy.as_str());
35 }
36
37 if let Some(hit) = self.cache_hit {
38 let _ = writeln!(output, "Cache hit: {hit}");
39 }
40 if let Some(count) = self.plan_reuse_count {
41 let _ = writeln!(output, "Plan reuse count: {count}");
42 }
43
44 output
45 }
46
47 fn render_with_options(options: &[(String, String)], output: &mut String) {
49 if options.is_empty() {
50 return;
51 }
52 let _ = writeln!(output, "\nWITH options:");
53 for (key, value) in options {
54 let _ = writeln!(output, " {key} = {value}");
55 }
56 }
57
58 fn render_let_bindings(bindings: &[String], output: &mut String) {
60 if bindings.is_empty() {
61 return;
62 }
63 let _ = writeln!(output, "\nLET bindings:");
64 for binding in bindings {
65 let _ = writeln!(output, " {binding}");
66 }
67 }
68
69 fn render_fusion_info(info: Option<&FusionInfo>, output: &mut String) {
71 let Some(fi) = info else { return };
72 let _ = writeln!(output, "\nFUSION:");
73 let _ = writeln!(output, " Strategy: {}", fi.strategy);
74 if let Some(k) = fi.k {
75 let _ = writeln!(output, " k: {k}");
76 }
77 if let Some(ref w) = fi.weights {
78 let _ = writeln!(output, " Weights: {w}");
79 }
80 }
81
82 pub(crate) fn render_node(node: &PlanNode, output: &mut String, prefix: &str, is_last: bool) {
83 let connector = if is_last { "└─ " } else { "├─ " };
84 let child_prefix = format!("{}{}", prefix, if is_last { " " } else { "│ " });
85
86 match node {
87 PlanNode::VectorSearch(vs) => {
88 let _ = writeln!(output, "{prefix}{connector}VectorSearch");
89 let _ = writeln!(output, "{child_prefix}├─ Collection: {}", vs.collection);
90 let _ = writeln!(output, "{child_prefix}├─ ef_search: {}", vs.ef_search);
91 let _ = writeln!(output, "{child_prefix}└─ Candidates: {}", vs.candidates);
92 }
93 PlanNode::Filter(f) => {
94 Self::render_filter_node(f, output, prefix, connector, &child_prefix);
95 }
96 PlanNode::Limit(l) => {
97 let suffix = if l.is_default { " (default)" } else { "" };
98 let _ = writeln!(output, "{prefix}{connector}Limit: {}{suffix}", l.count);
99 }
100 PlanNode::Offset(o) => {
101 let _ = writeln!(output, "{prefix}{connector}Offset: {}", o.count);
102 }
103 PlanNode::TableScan(ts) => {
104 let _ = writeln!(output, "{prefix}{connector}TableScan: {}", ts.collection);
105 }
106 PlanNode::IndexLookup(il) => {
107 let _ = writeln!(
108 output,
109 "{prefix}{connector}IndexLookup({}.{})",
110 il.label, il.property
111 );
112 let _ = writeln!(output, "{child_prefix}└─ Value: {}", il.value);
113 }
114 PlanNode::Sequence(nodes) => {
115 for (i, child) in nodes.iter().enumerate() {
116 Self::render_node(child, output, prefix, i == nodes.len() - 1);
117 }
118 }
119 PlanNode::MatchTraversal(mt) => {
120 Self::render_match_traversal_node(mt, output, prefix, connector, &child_prefix);
121 }
122 }
123 }
124
125 fn render_filter_node(
127 f: &FilterPlan,
128 output: &mut String,
129 prefix: &str,
130 connector: &str,
131 child_prefix: &str,
132 ) {
133 let _ = writeln!(output, "{prefix}{connector}Filter");
134 let _ = writeln!(output, "{child_prefix}├─ Conditions: {}", f.conditions);
135 if let Some(rows) = f.estimated_rows {
137 let _ = writeln!(output, "{child_prefix}├─ Estimated rows: {rows}");
138 }
139 if let Some(ref method) = f.estimation_method {
140 let _ = writeln!(output, "{child_prefix}├─ Estimation method: {method}");
141 }
142 let _ = writeln!(
143 output,
144 "{child_prefix}└─ Selectivity: {:.1}%",
145 f.selectivity * 100.0
146 );
147 }
148
149 fn render_match_traversal_node(
151 mt: &MatchTraversalPlan,
152 output: &mut String,
153 prefix: &str,
154 connector: &str,
155 child_prefix: &str,
156 ) {
157 let _ = writeln!(output, "{prefix}{connector}MatchTraversal");
158 let _ = writeln!(output, "{child_prefix}├─ Strategy: {}", mt.strategy);
159 if !mt.start_labels.is_empty() {
160 let _ = writeln!(
161 output,
162 "{child_prefix}├─ Start Labels: [{}]",
163 mt.start_labels.join(", ")
164 );
165 }
166 let _ = writeln!(output, "{child_prefix}├─ Max Depth: {}", mt.max_depth);
167 let _ = writeln!(
168 output,
169 "{child_prefix}├─ Relationships: {}",
170 mt.relationship_count
171 );
172 if let Some(threshold) = mt.similarity_threshold {
173 let _ = writeln!(
174 output,
175 "{child_prefix}└─ Similarity Threshold: {:.2}",
176 threshold
177 );
178 } else {
179 let _ = writeln!(
180 output,
181 "{child_prefix}└─ Similarity: {}",
182 if mt.has_similarity { "yes" } else { "no" }
183 );
184 }
185 }
186
187 pub fn to_json(&self) -> Result<String, serde_json::Error> {
193 serde_json::to_string_pretty(self)
194 }
195}
196
197impl IndexType {
198 #[must_use]
200 pub const fn as_str(&self) -> &'static str {
201 match self {
202 Self::Hnsw => "HNSW",
203 Self::Flat => "Flat",
204 Self::BinaryQuantization => "BinaryQuantization",
205 Self::Property => "PropertyIndex",
206 }
207 }
208}
209
210impl FilterStrategy {
211 #[must_use]
213 pub const fn as_str(&self) -> &'static str {
214 match self {
215 Self::None => "none",
216 Self::PreFilter => "pre-filtering (high selectivity)",
217 Self::PostFilter => "post-filtering (low selectivity)",
218 }
219 }
220}
221
222impl super::super::ast::CompareOp {
223 #[must_use]
225 pub const fn as_str(&self) -> &'static str {
226 match self {
227 Self::Eq => "=",
228 Self::NotEq => "!=",
229 Self::Gt => ">",
230 Self::Gte => ">=",
231 Self::Lt => "<",
232 Self::Lte => "<=",
233 }
234 }
235}
236
237impl fmt::Display for QueryPlan {
238 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239 write!(f, "{}", self.to_tree())
240 }
241}
242
243pub(super) fn format_with_value(v: &super::super::ast::WithValue) -> String {
245 match v {
246 super::super::ast::WithValue::String(s) | super::super::ast::WithValue::Identifier(s) => {
247 s.clone()
248 }
249 super::super::ast::WithValue::Integer(i) => i.to_string(),
250 super::super::ast::WithValue::Float(f) => f.to_string(),
251 super::super::ast::WithValue::Boolean(b) => b.to_string(),
252 }
253}