1use 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 PlanNode::Join(_)
123 | PlanNode::GroupBy(_)
124 | PlanNode::Aggregate(_)
125 | PlanNode::Sort(_) => {
126 Self::render_post_filter_node(node, output, (prefix, &child_prefix, connector));
127 }
128 }
129 }
130
131 fn render_post_filter_node(node: &PlanNode, output: &mut String, frame: (&str, &str, &str)) {
134 match node {
135 PlanNode::Join(j) => {
136 Self::render_leaf_with_child(
137 output,
138 frame,
139 &format!("{}Join", j.join_type),
140 ("Table", &j.table),
141 );
142 }
143 PlanNode::GroupBy(g) => Self::render_leaf_with_child(
144 output,
145 frame,
146 "GroupBy",
147 ("Columns", &format!("[{}]", g.columns.join(", "))),
148 ),
149 PlanNode::Aggregate(a) => Self::render_leaf_with_child(
150 output,
151 frame,
152 "Aggregate",
153 ("Functions", &format!("[{}]", a.functions.join(", "))),
154 ),
155 PlanNode::Sort(s) => {
156 Self::render_leaf_with_child(output, frame, "Sort", ("Keys", &s.keys.join(", ")));
157 }
158 _ => {}
159 }
160 }
161
162 fn render_leaf_with_child(
168 output: &mut String,
169 frame: (&str, &str, &str),
170 label: &str,
171 child: (&str, &str),
172 ) {
173 let (prefix, child_prefix, connector) = frame;
174 let (key, value) = child;
175 let _ = writeln!(output, "{prefix}{connector}{label}");
176 let _ = writeln!(output, "{child_prefix}└─ {key}: {value}");
177 }
178
179 fn render_filter_node(
181 f: &FilterPlan,
182 output: &mut String,
183 prefix: &str,
184 connector: &str,
185 child_prefix: &str,
186 ) {
187 let _ = writeln!(output, "{prefix}{connector}Filter");
188 let _ = writeln!(output, "{child_prefix}├─ Conditions: {}", f.conditions);
189 if let Some(rows) = f.estimated_rows {
191 let _ = writeln!(output, "{child_prefix}├─ Estimated rows: {rows}");
192 }
193 if let Some(ref method) = f.estimation_method {
194 let _ = writeln!(output, "{child_prefix}├─ Estimation method: {method}");
195 }
196 let _ = writeln!(
197 output,
198 "{child_prefix}└─ Selectivity: {:.1}%",
199 f.selectivity * 100.0
200 );
201 }
202
203 fn render_match_traversal_node(
205 mt: &MatchTraversalPlan,
206 output: &mut String,
207 prefix: &str,
208 connector: &str,
209 child_prefix: &str,
210 ) {
211 let _ = writeln!(output, "{prefix}{connector}MatchTraversal");
212 let _ = writeln!(output, "{child_prefix}├─ Strategy: {}", mt.strategy);
213 if !mt.start_labels.is_empty() {
214 let _ = writeln!(
215 output,
216 "{child_prefix}├─ Start Labels: [{}]",
217 mt.start_labels.join(", ")
218 );
219 }
220 let _ = writeln!(output, "{child_prefix}├─ Max Depth: {}", mt.max_depth);
221 let _ = writeln!(
222 output,
223 "{child_prefix}├─ Relationships: {}",
224 mt.relationship_count
225 );
226 if let Some(threshold) = mt.similarity_threshold {
227 let _ = writeln!(
228 output,
229 "{child_prefix}└─ Similarity Threshold: {:.2}",
230 threshold
231 );
232 } else {
233 let _ = writeln!(
234 output,
235 "{child_prefix}└─ Similarity: {}",
236 if mt.has_similarity { "yes" } else { "no" }
237 );
238 }
239 }
240
241 pub fn to_json(&self) -> Result<String, serde_json::Error> {
247 serde_json::to_string_pretty(self)
248 }
249}
250
251impl IndexType {
252 #[must_use]
254 pub const fn as_str(&self) -> &'static str {
255 match self {
256 Self::Hnsw => "HNSW",
257 Self::Flat => "Flat",
258 Self::BinaryQuantization => "BinaryQuantization",
259 Self::Property => "PropertyIndex",
260 }
261 }
262}
263
264impl FilterStrategy {
265 #[must_use]
267 pub const fn as_str(&self) -> &'static str {
268 match self {
269 Self::None => "none",
270 Self::PreFilter => "pre-filtering (high selectivity)",
271 Self::PostFilter => "post-filtering (low selectivity)",
272 }
273 }
274}
275
276impl super::super::ast::CompareOp {
277 #[must_use]
279 pub const fn as_str(&self) -> &'static str {
280 match self {
281 Self::Eq => "=",
282 Self::NotEq => "!=",
283 Self::Gt => ">",
284 Self::Gte => ">=",
285 Self::Lt => "<",
286 Self::Lte => "<=",
287 }
288 }
289}
290
291impl fmt::Display for QueryPlan {
292 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 write!(f, "{}", self.to_tree())
294 }
295}
296
297pub(super) fn format_with_value(v: &super::super::ast::WithValue) -> String {
299 match v {
300 super::super::ast::WithValue::String(s) | super::super::ast::WithValue::Identifier(s) => {
301 s.clone()
302 }
303 super::super::ast::WithValue::Integer(i) => i.to_string(),
304 super::super::ast::WithValue::Float(f) => f.to_string(),
305 super::super::ast::WithValue::Boolean(b) => b.to_string(),
306 }
307}