Skip to main content

oxisql_parse/
explain.rs

1//! Human-readable plan explanation for [`LogicalPlan`]s.
2
3use crate::plan::{JoinType, LogicalPlan, SetOpType};
4
5// ── Plan explanation ─────────────────────────────────────────────────────────
6
7/// Render a [`LogicalPlan`] as a human-readable indented tree string.
8///
9/// Each level is indented by two spaces per depth.  Example:
10///
11/// ```text
12/// Limit [count=10]
13///   Sort [age ASC]
14///     Filter [age > 18]
15///       Scan [users]
16/// ```
17pub fn explain(plan: &LogicalPlan) -> String {
18    let mut out = String::new();
19    explain_inner(plan, 0, &mut out);
20    // Trim trailing newline for a clean return value.
21    if out.ends_with('\n') {
22        out.pop();
23    }
24    out
25}
26
27pub(crate) fn explain_inner(plan: &LogicalPlan, depth: usize, out: &mut String) {
28    let indent = "  ".repeat(depth);
29    match plan {
30        LogicalPlan::Scan {
31            table,
32            alias,
33            limit,
34        } => {
35            let limit_str = limit.map(|l| format!(" limit={l}")).unwrap_or_default();
36            if let Some(a) = alias {
37                out.push_str(&format!("{indent}Scan [{table} AS {a}{limit_str}]\n"));
38            } else {
39                out.push_str(&format!("{indent}Scan [{table}{limit_str}]\n"));
40            }
41        }
42        LogicalPlan::Filter { input, predicate } => {
43            out.push_str(&format!("{indent}Filter [{predicate}]\n"));
44            explain_inner(input, depth + 1, out);
45        }
46        LogicalPlan::Project { input, columns } => {
47            let cols = columns.join(", ");
48            out.push_str(&format!("{indent}Project [{cols}]\n"));
49            explain_inner(input, depth + 1, out);
50        }
51        LogicalPlan::Join {
52            left,
53            right,
54            on,
55            join_type,
56            ..
57        } => {
58            let jt = match join_type {
59                JoinType::Inner => "INNER",
60                JoinType::Left => "LEFT",
61                JoinType::Right => "RIGHT",
62                JoinType::Full => "FULL",
63                JoinType::Cross => "CROSS",
64            };
65            if on.is_empty() {
66                out.push_str(&format!("{indent}Join [{jt}]\n"));
67            } else {
68                out.push_str(&format!("{indent}Join [{jt} ON {on}]\n"));
69            }
70            explain_inner(left, depth + 1, out);
71            explain_inner(right, depth + 1, out);
72        }
73        LogicalPlan::Aggregate {
74            input,
75            group_by,
76            aggregates,
77        } => {
78            let gb = group_by.join(", ");
79            let agg = aggregates.join(", ");
80            out.push_str(&format!(
81                "{indent}Aggregate [group_by=[{gb}] agg=[{agg}]]\n"
82            ));
83            explain_inner(input, depth + 1, out);
84        }
85        LogicalPlan::Sort { input, order_by } => {
86            let keys: Vec<String> = order_by
87                .iter()
88                .map(|s| {
89                    if s.ascending {
90                        format!("{} ASC", s.column)
91                    } else {
92                        format!("{} DESC", s.column)
93                    }
94                })
95                .collect();
96            out.push_str(&format!("{indent}Sort [{}]\n", keys.join(", ")));
97            explain_inner(input, depth + 1, out);
98        }
99        LogicalPlan::Limit {
100            input,
101            count,
102            offset,
103        } => {
104            let mut parts = Vec::new();
105            if let Some(c) = count {
106                parts.push(format!("count={c}"));
107            }
108            if let Some(o) = offset {
109                parts.push(format!("offset={o}"));
110            }
111            out.push_str(&format!("{indent}Limit [{}]\n", parts.join(", ")));
112            explain_inner(input, depth + 1, out);
113        }
114        LogicalPlan::Values { columns, rows } => {
115            let cols = if columns.is_empty() {
116                String::new()
117            } else {
118                format!(" cols=[{}]", columns.join(", "))
119            };
120            out.push_str(&format!("{indent}Values [rows={rows}{cols}]\n"));
121        }
122        LogicalPlan::Empty => {
123            out.push_str(&format!("{indent}Empty\n"));
124        }
125        LogicalPlan::SetOp {
126            op,
127            all,
128            left,
129            right,
130        } => {
131            let op_str = match op {
132                SetOpType::Union => "UNION",
133                SetOpType::Intersect => "INTERSECT",
134                SetOpType::Except => "EXCEPT",
135            };
136            let all_str = if *all { " ALL" } else { "" };
137            out.push_str(&format!("{indent}SetOp [{op_str}{all_str}]\n"));
138            explain_inner(left, depth + 1, out);
139            explain_inner(right, depth + 1, out);
140        }
141        LogicalPlan::Cte {
142            name,
143            query,
144            recursive,
145        } => {
146            let rec = if *recursive { " RECURSIVE" } else { "" };
147            out.push_str(&format!("{indent}Cte [{name}{rec}]\n"));
148            explain_inner(query, depth + 1, out);
149        }
150        LogicalPlan::CteRef { name } => {
151            out.push_str(&format!("{indent}CteRef [{name}]\n"));
152        }
153        LogicalPlan::Window { input, functions } => {
154            let names: Vec<&str> = functions.iter().map(|f| f.name.as_str()).collect();
155            out.push_str(&format!("{indent}Window [{}]\n", names.join(", ")));
156            explain_inner(input, depth + 1, out);
157        }
158        LogicalPlan::Subquery { query, alias } => {
159            if let Some(a) = alias {
160                out.push_str(&format!("{indent}Subquery [alias={a}]\n"));
161            } else {
162                out.push_str(&format!("{indent}Subquery\n"));
163            }
164            explain_inner(query, depth + 1, out);
165        }
166        LogicalPlan::Exists { subquery, negated } => {
167            let neg = if *negated { "NOT " } else { "" };
168            out.push_str(&format!("{indent}Exists [{neg}EXISTS]\n"));
169            explain_inner(subquery, depth + 1, out);
170        }
171        LogicalPlan::InSubquery {
172            expr,
173            subquery,
174            negated,
175        } => {
176            let neg = if *negated { "NOT " } else { "" };
177            out.push_str(&format!("{indent}InSubquery [{expr} {neg}IN]\n"));
178            explain_inner(subquery, depth + 1, out);
179        }
180    }
181}