1use std::collections::BTreeMap;
13use std::fmt::Write as _;
14
15use lora_analyzer::ResolvedExpr;
16use lora_ast::Direction;
17
18use crate::physical::{PhysicalNodeId, PhysicalOp, PhysicalPlan};
19use crate::{CompiledQuery, CompiledUnionBranch};
20
21#[derive(Debug, Clone)]
23pub struct PlanTreeNode {
24 pub id: usize,
28 pub operator: String,
30 pub details: BTreeMap<String, String>,
33 pub estimated_rows: Option<u64>,
35 pub children: Vec<PlanTreeNode>,
37}
38
39#[derive(Debug, Clone)]
41pub struct PlanTree {
42 pub root: PlanTreeNode,
43}
44
45const SYNTHETIC_ID: usize = usize::MAX;
46
47pub fn plan_tree_from_compiled(compiled: &CompiledQuery) -> PlanTree {
49 let head = build_node(&compiled.physical, compiled.physical.root);
50 if compiled.unions.is_empty() {
51 return PlanTree { root: head };
52 }
53
54 let mut children = Vec::with_capacity(compiled.unions.len() + 1);
55 children.push(head);
56 for branch in &compiled.unions {
57 children.push(build_union_branch(branch));
58 }
59 let mut details = BTreeMap::new();
60 let all = compiled.unions.iter().all(|b| b.all);
61 let any_distinct = compiled.unions.iter().any(|b| !b.all);
62 let kind = if all && !any_distinct {
63 "ALL"
64 } else if any_distinct && compiled.unions.iter().all(|b| !b.all) {
65 "DISTINCT"
66 } else {
67 "MIXED"
68 };
69 details.insert("kind".to_string(), kind.to_string());
70 PlanTree {
71 root: PlanTreeNode {
72 id: SYNTHETIC_ID,
73 operator: "Union".to_string(),
74 details,
75 estimated_rows: None,
76 children,
77 },
78 }
79}
80
81fn build_union_branch(branch: &CompiledUnionBranch) -> PlanTreeNode {
82 let mut details = BTreeMap::new();
83 details.insert(
84 "kind".to_string(),
85 if branch.all { "ALL" } else { "DISTINCT" }.to_string(),
86 );
87 PlanTreeNode {
88 id: SYNTHETIC_ID,
89 operator: "UnionBranch".to_string(),
90 details,
91 estimated_rows: None,
92 children: vec![build_node(&branch.physical, branch.physical.root)],
93 }
94}
95
96fn build_node(plan: &PhysicalPlan, id: PhysicalNodeId) -> PlanTreeNode {
97 let op = &plan.nodes[id];
98 let (operator, details, child_ids) = describe(op);
99 let children = child_ids
100 .into_iter()
101 .map(|cid| build_node(plan, cid))
102 .collect();
103 PlanTreeNode {
104 id,
105 operator,
106 details,
107 estimated_rows: None,
108 children,
109 }
110}
111
112fn describe(op: &PhysicalOp) -> (String, BTreeMap<String, String>, Vec<PhysicalNodeId>) {
113 let mut d = BTreeMap::new();
114 match op {
115 PhysicalOp::Argument(_) => ("Argument".to_string(), d, Vec::new()),
116 PhysicalOp::NodeScan(n) => {
117 d.insert("var".to_string(), var_str(n.var));
118 ("NodeScan".to_string(), d, opt_input(n.input))
119 }
120 PhysicalOp::NodeByLabelScan(n) => {
121 d.insert("var".to_string(), var_str(n.var));
122 d.insert("labels".to_string(), label_groups_str(&n.labels));
123 ("NodeByLabelScan".to_string(), d, opt_input(n.input))
124 }
125 PhysicalOp::NodeByPropertyScan(n) => {
126 d.insert("var".to_string(), var_str(n.var));
127 if !n.labels.is_empty() {
128 d.insert("labels".to_string(), label_groups_str(&n.labels));
129 }
130 d.insert("key".to_string(), n.key.clone());
131 d.insert("value".to_string(), expr_str(&n.value));
132 ("NodeByPropertyScan".to_string(), d, opt_input(n.input))
133 }
134 PhysicalOp::Expand(n) => {
135 d.insert("src".to_string(), var_str(n.src));
136 d.insert("dst".to_string(), var_str(n.dst));
137 if let Some(rel) = n.rel {
138 d.insert("rel".to_string(), var_str(rel));
139 }
140 if !n.types.is_empty() {
141 d.insert("types".to_string(), n.types.join("|"));
142 }
143 d.insert(
144 "direction".to_string(),
145 direction_str(n.direction).to_string(),
146 );
147 if let Some(props) = &n.rel_properties {
148 d.insert("rel_properties".to_string(), expr_str(props));
149 }
150 if let Some(range) = &n.range {
151 d.insert("range".to_string(), format!("{:?}", range));
152 }
153 ("Expand".to_string(), d, vec![n.input])
154 }
155 PhysicalOp::Filter(n) => {
156 d.insert("predicate".to_string(), expr_str(&n.predicate));
157 ("Filter".to_string(), d, vec![n.input])
158 }
159 PhysicalOp::Projection(n) => {
160 d.insert("distinct".to_string(), n.distinct.to_string());
161 d.insert(
162 "include_existing".to_string(),
163 n.include_existing.to_string(),
164 );
165 d.insert(
166 "items".to_string(),
167 n.items
168 .iter()
169 .map(|p| p.name.clone())
170 .collect::<Vec<_>>()
171 .join(", "),
172 );
173 ("Projection".to_string(), d, vec![n.input])
174 }
175 PhysicalOp::Unwind(n) => {
176 d.insert("alias".to_string(), var_str(n.alias));
177 d.insert("expr".to_string(), expr_str(&n.expr));
178 ("Unwind".to_string(), d, vec![n.input])
179 }
180 PhysicalOp::HashAggregation(n) => {
181 d.insert(
182 "group_by".to_string(),
183 n.group_by
184 .iter()
185 .map(|p| p.name.clone())
186 .collect::<Vec<_>>()
187 .join(", "),
188 );
189 d.insert(
190 "aggregates".to_string(),
191 n.aggregates
192 .iter()
193 .map(|p| p.name.clone())
194 .collect::<Vec<_>>()
195 .join(", "),
196 );
197 ("HashAggregation".to_string(), d, vec![n.input])
198 }
199 PhysicalOp::Sort(n) => {
200 d.insert(
201 "items".to_string(),
202 format!("{} sort key(s)", n.items.len()),
203 );
204 ("Sort".to_string(), d, vec![n.input])
205 }
206 PhysicalOp::Limit(n) => {
207 if let Some(skip) = &n.skip {
208 d.insert("skip".to_string(), expr_str(skip));
209 }
210 if let Some(limit) = &n.limit {
211 d.insert("limit".to_string(), expr_str(limit));
212 }
213 ("Limit".to_string(), d, vec![n.input])
214 }
215 PhysicalOp::Create(n) => {
216 d.insert(
217 "elements".to_string(),
218 pattern_summary(n.pattern.parts.len()),
219 );
220 ("Create".to_string(), d, vec![n.input])
221 }
222 PhysicalOp::Merge(n) => {
223 d.insert(
224 "actions".to_string(),
225 if n.actions.is_empty() {
226 "0".to_string()
227 } else {
228 n.actions.len().to_string()
229 },
230 );
231 let _ = &n.pattern_part;
232 ("Merge".to_string(), d, vec![n.input])
233 }
234 PhysicalOp::Delete(n) => {
235 d.insert("detach".to_string(), n.detach.to_string());
236 d.insert("targets".to_string(), n.expressions.len().to_string());
237 ("Delete".to_string(), d, vec![n.input])
238 }
239 PhysicalOp::Set(n) => {
240 d.insert("items".to_string(), n.items.len().to_string());
241 ("Set".to_string(), d, vec![n.input])
242 }
243 PhysicalOp::Remove(n) => {
244 d.insert("items".to_string(), n.items.len().to_string());
245 ("Remove".to_string(), d, vec![n.input])
246 }
247 PhysicalOp::OptionalMatch(n) => {
248 d.insert(
249 "new_vars".to_string(),
250 n.new_vars
251 .iter()
252 .copied()
253 .map(var_str)
254 .collect::<Vec<_>>()
255 .join(", "),
256 );
257 ("OptionalMatch".to_string(), d, vec![n.input, n.inner])
258 }
259 PhysicalOp::PathBuild(n) => {
260 d.insert("output".to_string(), var_str(n.output));
261 d.insert("nodes".to_string(), n.node_vars.len().to_string());
262 d.insert("rels".to_string(), n.rel_vars.len().to_string());
263 if let Some(all) = n.shortest_path_all {
264 d.insert("shortest_path_all".to_string(), all.to_string());
265 }
266 ("PathBuild".to_string(), d, vec![n.input])
267 }
268 }
269}
270
271fn opt_input(input: Option<PhysicalNodeId>) -> Vec<PhysicalNodeId> {
272 input.map(|i| vec![i]).unwrap_or_default()
273}
274
275fn var_str(v: lora_analyzer::symbols::VarId) -> String {
276 format!("v{}", v.0)
277}
278
279fn label_groups_str(groups: &[Vec<String>]) -> String {
280 groups
281 .iter()
282 .map(|or_group| or_group.join("|"))
283 .collect::<Vec<_>>()
284 .join("&")
285}
286
287fn direction_str(d: Direction) -> &'static str {
288 match d {
289 Direction::Right => "->",
290 Direction::Left => "<-",
291 Direction::Undirected => "-",
292 }
293}
294
295fn expr_str(e: &ResolvedExpr) -> String {
296 let mut out = String::new();
297 let _ = write!(&mut out, "{:?}", e);
298 out
299}
300
301fn pattern_summary(part_count: usize) -> String {
302 format!("{} pattern part(s)", part_count)
303}