1use crate::cost::{CostModel, NodeCost};
4use crate::plan::{JoinType, LogicalPlan, SetOpType};
5
6pub fn explain(plan: &LogicalPlan) -> String {
19 let mut out = String::new();
20 explain_inner(plan, 0, &mut out);
21 if out.ends_with('\n') {
23 out.pop();
24 }
25 out
26}
27
28pub fn explain_verbose(plan: &LogicalPlan, model: &CostModel) -> String {
34 let costs = model.explain_costs(plan);
35 let mut out = String::new();
36 explain_verbose_inner(plan, &costs, 0, &mut out);
37 if out.ends_with('\n') {
38 out.pop();
39 }
40 out
41}
42
43fn explain_verbose_inner(plan: &LogicalPlan, cost: &NodeCost, depth: usize, out: &mut String) {
44 let mut line = String::new();
47 explain_inner(plan, depth, &mut line);
48 let line = line.trim_end_matches('\n');
50
51 let rows = cost.estimate.rows as u64;
52 let cost_val = cost.estimate.total_cost;
53 out.push_str(&format!("{line} (rows={rows}, cost={cost_val:.1})\n"));
54
55 let plan_children = plan_children_ref(plan);
57 for (child_plan, child_cost) in plan_children.iter().zip(cost.children.iter()) {
58 explain_verbose_inner(child_plan, child_cost, depth + 1, out);
59 }
60}
61
62fn plan_children_ref(plan: &LogicalPlan) -> Vec<&LogicalPlan> {
65 match plan {
66 LogicalPlan::Filter { input, .. }
67 | LogicalPlan::Project { input, .. }
68 | LogicalPlan::Aggregate { input, .. }
69 | LogicalPlan::Sort { input, .. }
70 | LogicalPlan::Limit { input, .. }
71 | LogicalPlan::Window { input, .. }
72 | LogicalPlan::Cte { query: input, .. }
73 | LogicalPlan::Subquery { query: input, .. }
74 | LogicalPlan::Exists {
75 subquery: input, ..
76 }
77 | LogicalPlan::Compute { input, .. } => vec![input.as_ref()],
78
79 LogicalPlan::InSubquery { subquery, .. } => vec![subquery.as_ref()],
80
81 LogicalPlan::Join { left, right, .. } | LogicalPlan::SetOp { left, right, .. } => {
82 vec![left.as_ref(), right.as_ref()]
83 }
84
85 LogicalPlan::Scan { .. }
86 | LogicalPlan::Values { .. }
87 | LogicalPlan::Empty
88 | LogicalPlan::CteRef { .. } => vec![],
89 }
90}
91
92pub fn explain_json(plan: &LogicalPlan, model: Option<&CostModel>) -> String {
102 let costs = model.map(|m| m.explain_costs(plan));
103 let mut out = String::new();
104 emit_json(plan, costs.as_ref(), &mut out);
105 out
106}
107
108fn emit_json(plan: &LogicalPlan, cost: Option<&NodeCost>, out: &mut String) {
109 out.push('{');
110
111 let op_name = node_op_name(plan);
113 out.push_str("\"op\":");
114 json_string(op_name, out);
115
116 emit_json_fields(plan, out);
118
119 if let Some(c) = cost {
121 let rows = c.estimate.rows as u64;
122 let cost_val = c.estimate.total_cost;
123 out.push_str(&format!(",\"rows\":{rows},\"cost\":{cost_val:.1}"));
124 }
125
126 let plan_children = plan_children_ref(plan);
128 let cost_children: Vec<Option<&NodeCost>> = if let Some(c) = cost {
129 c.children.iter().map(Some).collect()
130 } else {
131 plan_children.iter().map(|_| None).collect()
132 };
133
134 out.push_str(",\"children\":[");
135 for (i, (child_plan, child_cost)) in plan_children.iter().zip(cost_children.iter()).enumerate()
136 {
137 if i > 0 {
138 out.push(',');
139 }
140 emit_json(child_plan, *child_cost, out);
141 }
142 out.push_str("]}");
143}
144
145fn node_op_name(plan: &LogicalPlan) -> &'static str {
147 match plan {
148 LogicalPlan::Scan { .. } => "Scan",
149 LogicalPlan::Filter { .. } => "Filter",
150 LogicalPlan::Project { .. } => "Project",
151 LogicalPlan::Join { .. } => "Join",
152 LogicalPlan::Aggregate { .. } => "Aggregate",
153 LogicalPlan::Sort { .. } => "Sort",
154 LogicalPlan::Limit { .. } => "Limit",
155 LogicalPlan::Values { .. } => "Values",
156 LogicalPlan::Empty => "Empty",
157 LogicalPlan::SetOp { .. } => "SetOp",
158 LogicalPlan::Cte { .. } => "Cte",
159 LogicalPlan::CteRef { .. } => "CteRef",
160 LogicalPlan::Window { .. } => "Window",
161 LogicalPlan::Subquery { .. } => "Subquery",
162 LogicalPlan::Exists { .. } => "Exists",
163 LogicalPlan::InSubquery { .. } => "InSubquery",
164 LogicalPlan::Compute { .. } => "Compute",
165 }
166}
167
168fn emit_json_fields(plan: &LogicalPlan, out: &mut String) {
170 match plan {
171 LogicalPlan::Scan {
172 table,
173 alias,
174 limit,
175 } => {
176 out.push_str(",\"table\":");
177 json_string(table, out);
178 if let Some(a) = alias {
179 out.push_str(",\"alias\":");
180 json_string(a, out);
181 }
182 if let Some(l) = limit {
183 out.push_str(&format!(",\"limit\":{l}"));
184 }
185 }
186 LogicalPlan::Filter { predicate, .. } => {
187 out.push_str(",\"predicate\":");
188 json_string(predicate, out);
189 }
190 LogicalPlan::Project { columns, .. } => {
191 out.push_str(",\"columns\":[");
192 for (i, c) in columns.iter().enumerate() {
193 if i > 0 {
194 out.push(',');
195 }
196 json_string(c, out);
197 }
198 out.push(']');
199 }
200 LogicalPlan::Join { on, join_type, .. } => {
201 let jt = match join_type {
202 JoinType::Inner => "INNER",
203 JoinType::Left => "LEFT",
204 JoinType::Right => "RIGHT",
205 JoinType::Full => "FULL",
206 JoinType::Cross => "CROSS",
207 JoinType::LeftSemi => "SEMI",
208 JoinType::LeftAnti => "ANTI",
209 };
210 out.push_str(",\"join_type\":");
211 json_string(jt, out);
212 if !on.is_empty() {
213 out.push_str(",\"on\":");
214 json_string(on, out);
215 }
216 }
217 LogicalPlan::Aggregate {
218 group_by,
219 aggregates,
220 ..
221 } => {
222 out.push_str(",\"group_by\":[");
223 for (i, g) in group_by.iter().enumerate() {
224 if i > 0 {
225 out.push(',');
226 }
227 json_string(g, out);
228 }
229 out.push_str("],\"aggregates\":[");
230 for (i, a) in aggregates.iter().enumerate() {
231 if i > 0 {
232 out.push(',');
233 }
234 json_string(a, out);
235 }
236 out.push(']');
237 }
238 LogicalPlan::Sort { order_by, .. } => {
239 out.push_str(",\"order_by\":[");
240 for (i, s) in order_by.iter().enumerate() {
241 if i > 0 {
242 out.push(',');
243 }
244 let dir = if s.ascending { "ASC" } else { "DESC" };
245 out.push('{');
246 out.push_str("\"column\":");
247 json_string(&s.column, out);
248 out.push_str(",\"dir\":");
249 json_string(dir, out);
250 out.push('}');
251 }
252 out.push(']');
253 }
254 LogicalPlan::Limit { count, offset, .. } => {
255 if let Some(c) = count {
256 out.push_str(&format!(",\"count\":{c}"));
257 }
258 if let Some(o) = offset {
259 out.push_str(&format!(",\"offset\":{o}"));
260 }
261 }
262 LogicalPlan::Values { columns, rows } => {
263 out.push_str(&format!(",\"row_count\":{rows}"));
264 out.push_str(",\"columns\":[");
265 for (i, c) in columns.iter().enumerate() {
266 if i > 0 {
267 out.push(',');
268 }
269 json_string(c, out);
270 }
271 out.push(']');
272 }
273 LogicalPlan::SetOp { op, all, .. } => {
274 let op_str = match op {
275 SetOpType::Union => "UNION",
276 SetOpType::Intersect => "INTERSECT",
277 SetOpType::Except => "EXCEPT",
278 };
279 out.push_str(",\"set_op\":");
280 json_string(op_str, out);
281 out.push_str(&format!(",\"all\":{all}"));
282 }
283 LogicalPlan::Cte {
284 name, recursive, ..
285 } => {
286 out.push_str(",\"name\":");
287 json_string(name, out);
288 out.push_str(&format!(",\"recursive\":{recursive}"));
289 }
290 LogicalPlan::CteRef { name } => {
291 out.push_str(",\"name\":");
292 json_string(name, out);
293 }
294 LogicalPlan::Window { functions, .. } => {
295 out.push_str(",\"functions\":[");
296 for (i, f) in functions.iter().enumerate() {
297 if i > 0 {
298 out.push(',');
299 }
300 out.push('{');
301 out.push_str("\"name\":");
302 json_string(&f.name, out);
303 out.push_str(",\"alias\":");
304 json_string(&f.alias, out);
305 out.push('}');
306 }
307 out.push(']');
308 }
309 LogicalPlan::Subquery { alias, .. } => {
310 if let Some(a) = alias {
311 out.push_str(",\"alias\":");
312 json_string(a, out);
313 }
314 }
315 LogicalPlan::Exists { negated, .. } => {
316 out.push_str(&format!(",\"negated\":{negated}"));
317 }
318 LogicalPlan::InSubquery { expr, negated, .. } => {
319 out.push_str(",\"expr\":");
320 json_string(expr, out);
321 out.push_str(&format!(",\"negated\":{negated}"));
322 }
323 LogicalPlan::Compute { bindings, .. } => {
324 out.push_str(",\"bindings\":[");
325 for (i, (alias, expr)) in bindings.iter().enumerate() {
326 if i > 0 {
327 out.push(',');
328 }
329 out.push('{');
330 out.push_str("\"alias\":");
331 json_string(alias, out);
332 out.push_str(",\"expr\":");
333 json_string(expr, out);
334 out.push('}');
335 }
336 out.push(']');
337 }
338 LogicalPlan::Empty => {}
339 }
340}
341
342fn json_string(s: &str, out: &mut String) {
344 out.push('"');
345 for ch in s.chars() {
346 match ch {
347 '"' => out.push_str("\\\""),
348 '\\' => out.push_str("\\\\"),
349 '\n' => out.push_str("\\n"),
350 '\r' => out.push_str("\\r"),
351 '\t' => out.push_str("\\t"),
352 c => out.push(c),
353 }
354 }
355 out.push('"');
356}
357
358pub(crate) fn explain_inner(plan: &LogicalPlan, depth: usize, out: &mut String) {
361 let indent = " ".repeat(depth);
362 match plan {
363 LogicalPlan::Scan {
364 table,
365 alias,
366 limit,
367 } => {
368 let limit_str = limit.map(|l| format!(" limit={l}")).unwrap_or_default();
369 if let Some(a) = alias {
370 out.push_str(&format!("{indent}Scan [{table} AS {a}{limit_str}]\n"));
371 } else {
372 out.push_str(&format!("{indent}Scan [{table}{limit_str}]\n"));
373 }
374 }
375 LogicalPlan::Filter { input, predicate } => {
376 out.push_str(&format!("{indent}Filter [{predicate}]\n"));
377 explain_inner(input, depth + 1, out);
378 }
379 LogicalPlan::Project { input, columns } => {
380 let cols = columns.join(", ");
381 out.push_str(&format!("{indent}Project [{cols}]\n"));
382 explain_inner(input, depth + 1, out);
383 }
384 LogicalPlan::Join {
385 left,
386 right,
387 on,
388 join_type,
389 ..
390 } => {
391 let jt = match join_type {
392 JoinType::Inner => "INNER",
393 JoinType::Left => "LEFT",
394 JoinType::Right => "RIGHT",
395 JoinType::Full => "FULL",
396 JoinType::Cross => "CROSS",
397 JoinType::LeftSemi => "SEMI",
398 JoinType::LeftAnti => "ANTI",
399 };
400 if on.is_empty() {
401 out.push_str(&format!("{indent}Join [{jt}]\n"));
402 } else {
403 out.push_str(&format!("{indent}Join [{jt} ON {on}]\n"));
404 }
405 explain_inner(left, depth + 1, out);
406 explain_inner(right, depth + 1, out);
407 }
408 LogicalPlan::Aggregate {
409 input,
410 group_by,
411 aggregates,
412 } => {
413 let gb = group_by.join(", ");
414 let agg = aggregates.join(", ");
415 out.push_str(&format!(
416 "{indent}Aggregate [group_by=[{gb}] agg=[{agg}]]\n"
417 ));
418 explain_inner(input, depth + 1, out);
419 }
420 LogicalPlan::Sort { input, order_by } => {
421 let keys: Vec<String> = order_by
422 .iter()
423 .map(|s| {
424 if s.ascending {
425 format!("{} ASC", s.column)
426 } else {
427 format!("{} DESC", s.column)
428 }
429 })
430 .collect();
431 out.push_str(&format!("{indent}Sort [{}]\n", keys.join(", ")));
432 explain_inner(input, depth + 1, out);
433 }
434 LogicalPlan::Limit {
435 input,
436 count,
437 offset,
438 } => {
439 let mut parts = Vec::new();
440 if let Some(c) = count {
441 parts.push(format!("count={c}"));
442 }
443 if let Some(o) = offset {
444 parts.push(format!("offset={o}"));
445 }
446 out.push_str(&format!("{indent}Limit [{}]\n", parts.join(", ")));
447 explain_inner(input, depth + 1, out);
448 }
449 LogicalPlan::Values { columns, rows } => {
450 let cols = if columns.is_empty() {
451 String::new()
452 } else {
453 format!(" cols=[{}]", columns.join(", "))
454 };
455 out.push_str(&format!("{indent}Values [rows={rows}{cols}]\n"));
456 }
457 LogicalPlan::Empty => {
458 out.push_str(&format!("{indent}Empty\n"));
459 }
460 LogicalPlan::SetOp {
461 op,
462 all,
463 left,
464 right,
465 } => {
466 let op_str = match op {
467 SetOpType::Union => "UNION",
468 SetOpType::Intersect => "INTERSECT",
469 SetOpType::Except => "EXCEPT",
470 };
471 let all_str = if *all { " ALL" } else { "" };
472 out.push_str(&format!("{indent}SetOp [{op_str}{all_str}]\n"));
473 explain_inner(left, depth + 1, out);
474 explain_inner(right, depth + 1, out);
475 }
476 LogicalPlan::Cte {
477 name,
478 query,
479 recursive,
480 } => {
481 let rec = if *recursive { " RECURSIVE" } else { "" };
482 out.push_str(&format!("{indent}Cte [{name}{rec}]\n"));
483 explain_inner(query, depth + 1, out);
484 }
485 LogicalPlan::CteRef { name } => {
486 out.push_str(&format!("{indent}CteRef [{name}]\n"));
487 }
488 LogicalPlan::Window { input, functions } => {
489 let names: Vec<&str> = functions.iter().map(|f| f.name.as_str()).collect();
490 out.push_str(&format!("{indent}Window [{}]\n", names.join(", ")));
491 explain_inner(input, depth + 1, out);
492 }
493 LogicalPlan::Subquery { query, alias } => {
494 if let Some(a) = alias {
495 out.push_str(&format!("{indent}Subquery [alias={a}]\n"));
496 } else {
497 out.push_str(&format!("{indent}Subquery\n"));
498 }
499 explain_inner(query, depth + 1, out);
500 }
501 LogicalPlan::Exists { subquery, negated } => {
502 let neg = if *negated { "NOT " } else { "" };
503 out.push_str(&format!("{indent}Exists [{neg}EXISTS]\n"));
504 explain_inner(subquery, depth + 1, out);
505 }
506 LogicalPlan::InSubquery {
507 expr,
508 subquery,
509 negated,
510 } => {
511 let neg = if *negated { "NOT " } else { "" };
512 out.push_str(&format!("{indent}InSubquery [{expr} {neg}IN]\n"));
513 explain_inner(subquery, depth + 1, out);
514 }
515 LogicalPlan::Compute { input, bindings } => {
516 let bind_str: Vec<String> = bindings
517 .iter()
518 .map(|(alias, expr)| format!("{alias} = {expr}"))
519 .collect();
520 out.push_str(&format!("{indent}Compute [{}]\n", bind_str.join(", ")));
521 explain_inner(input, depth + 1, out);
522 }
523 }
524}