linkrs_query/executor/explain/
physical_plan_explain.rs1use std::collections::{HashMap, HashSet};
9
10use crate::executor::streaming::plan::types::{
11 FragmentGraph, FragmentId, PhysicalOperatorId, PhysicalPlan,
12};
13use crate::planning::plan::explain::{Pair, PlanDescription, PlanNodeDescription};
14
15fn expand_bypass_display(
18 col_names: &[String],
19 edge_props: &Option<Vec<String>>,
20 dst_props: &Option<Vec<String>>,
21) -> Vec<String> {
22 let Some(edge_var) = col_names.get(1) else {
23 return Vec::new();
24 };
25 let Some(dst_var) = col_names.get(2) else {
26 return Vec::new();
27 };
28 let mut out = Vec::new();
29 if let Some(props) = edge_props {
30 let mut sorted = props.clone();
31 sorted.sort();
32 sorted.dedup();
33 out.extend(sorted.iter().map(|prop| format!("{edge_var}.{prop}")));
34 }
35 if let Some(props) = dst_props {
36 let mut sorted = props.clone();
37 sorted.sort();
38 sorted.dedup();
39 out.extend(sorted.iter().map(|prop| format!("{dst_var}.{prop}")));
40 }
41 out
42}
43
44pub fn physical_plan_to_plan_description(plan: &PhysicalPlan) -> PlanDescription {
49 let mut desc = PlanDescription::new();
50 desc.requested_workers = 1;
51 desc.parallel_fallback_reason = plan.parallel_fallback_reason.clone();
52 desc.cbo_notes = plan.cbo_notes.clone();
53 desc.statistics_summary = plan.statistics_summary.clone();
54 if let Some(spec) = plan.partition_spec() {
55 let version = spec
56 .layout_version()
57 .map(|v| format!(", layout_version={v}"))
58 .unwrap_or_default();
59 desc.partition_spec_description = Some(match spec.strategy() {
60 crate::planning::plan::PartitionStrategy::Range => {
61 let ranges = spec
62 .ranges()
63 .iter()
64 .map(|r| format!("[{}..{})", r.start, r.end))
65 .collect::<Vec<_>>()
66 .join(", ");
67 format!(
68 "{} partitioned into {} ranges [{}]{}",
69 spec.source(),
70 spec.partition_count(),
71 ranges,
72 version
73 )
74 }
75 crate::planning::plan::PartitionStrategy::Hash { key } => {
76 let key_desc = if key.is_empty() {
77 String::new()
78 } else {
79 format!(" by key '{key}'")
80 };
81 format!(
82 "{} hash-partitioned into {} buckets{}{}",
83 spec.source(),
84 spec.partition_count(),
85 key_desc,
86 version
87 )
88 }
89 crate::planning::plan::PartitionStrategy::RoundRobin => {
90 format!(
91 "{} round-robin partitioned into {} buckets{}",
92 spec.source(),
93 spec.partition_count(),
94 version
95 )
96 }
97 });
98 }
99
100 let topo = topological_operator_order(&plan.fragments, plan);
103
104 let mut operator_to_fragment: HashMap<PhysicalOperatorId, FragmentId> = HashMap::new();
105 for frag in plan.fragments.fragments() {
106 for op_id in &frag.operators {
107 operator_to_fragment.insert(*op_id, frag.id);
108 }
109 }
110
111 let mut parent_of: HashMap<PhysicalOperatorId, Vec<PhysicalOperatorId>> = HashMap::new();
114
115 for frag in plan.fragments.fragments() {
117 for (i, op_id) in frag.operators.iter().enumerate() {
118 if i > 0 {
119 parent_of
121 .entry(frag.operators[i - 1])
122 .or_default()
123 .push(*op_id);
124 }
125 }
126 if let Some(first_op) = frag.operators.first() {
128 for input_fid in &frag.inputs {
129 if let Some(input_frag) = plan.fragments.get(*input_fid) {
130 parent_of
131 .entry(input_frag.root_operator)
132 .or_default()
133 .push(*first_op);
134 }
135 }
136 }
137 }
138
139 let mut desc_ids: HashSet<PhysicalOperatorId> = HashSet::new();
141 let mut ordered_ops: Vec<PhysicalOperatorId> = Vec::new();
142 for &fid in &topo {
143 if let Some(frag) = plan.fragments.get(fid) {
144 for op_id in &frag.operators {
145 if desc_ids.insert(*op_id) {
146 ordered_ops.push(*op_id);
147 }
148 }
149 }
150 }
151
152 for &op_id in &ordered_ops {
153 let op_spec = match plan.operator(op_id) {
154 Some(s) => s,
155 None => continue,
156 };
157
158 let mut pnd = PlanNodeDescription::new(op_spec.explain_name, op_id.0 as i64);
159
160 let mut pairs = Vec::new();
162 if let Some(est) = op_spec.estimated_cardinality {
163 pairs.push(Pair::new("est_rows", est.to_string()));
164 }
165 if let Some(reason) = &op_spec.choice_reason {
166 pairs.push(Pair::new("reason", reason.clone()));
167 }
168 if op_spec.has_folded_expressions {
170 pairs.push(Pair::new("folded", "true"));
171 }
172
173 let props = &op_spec.properties;
174 if let crate::executor::streaming::plan::properties::Ordering::Sorted(orders) =
175 &props.ordering
176 {
177 let order_str = orders
178 .iter()
179 .map(|o| {
180 format!(
181 "slot#{} {}",
182 o.slot,
183 if o.ascending { "ASC" } else { "DESC" }
184 )
185 })
186 .collect::<Vec<_>>()
187 .join(", ");
188 if !order_str.is_empty() {
189 pairs.push(Pair::new("ordering", order_str));
190 }
191 }
192
193 if let crate::executor::streaming::plan::properties::Distribution::HashPartitioned(keys) =
194 &props.distribution
195 {
196 if !keys.is_empty() {
197 pairs.push(Pair::new(
198 "hash_slots",
199 keys.iter()
200 .map(|s| format!("slot#{}", s))
201 .collect::<Vec<_>>()
202 .join(", "),
203 ));
204 }
205 }
206
207 if props.pipeline_kind
208 == crate::executor::streaming::plan::properties::PipelineKind::Blocking
209 {
210 pairs.push(Pair::new("blocking", "true"));
211 }
212
213 if let crate::executor::streaming::plan::types::OperatorKindSpec::Apply(
216 crate::executor::streaming::operators::spec::ApplySpec::PatternApply {
217 hash_keys,
218 probe_keys,
219 anti,
220 },
221 ) = &op_spec.spec
222 {
223 if !hash_keys.is_empty() {
224 pairs.push(Pair::new(
225 "hash_keys",
226 hash_keys
227 .iter()
228 .map(linkrs_core::types::expr::Expression::to_expression_string)
229 .collect::<Vec<_>>()
230 .join(", "),
231 ));
232 }
233 if !probe_keys.is_empty() {
234 pairs.push(Pair::new(
235 "probe_keys",
236 probe_keys
237 .iter()
238 .map(linkrs_core::types::expr::Expression::to_expression_string)
239 .collect::<Vec<_>>()
240 .join(", "),
241 ));
242 }
243 if *anti {
244 pairs.push(Pair::new("anti", "true"));
245 }
246 }
247
248 if let crate::executor::streaming::plan::types::OperatorKindSpec::Apply(
250 crate::executor::streaming::operators::spec::ApplySpec::CorrelatedApply {
251 anti, ..
252 },
253 ) = &op_spec.spec
254 {
255 if *anti {
256 pairs.push(Pair::new("anti", "true"));
257 }
258 }
259
260 if let crate::executor::streaming::plan::types::OperatorKindSpec::Join(
263 crate::executor::streaming::operators::spec::JoinSpec::SemiJoin {
264 join_condition,
265 anti,
266 },
267 ) = &op_spec.spec
268 {
269 if let Some(condition) = join_condition {
270 pairs.push(Pair::new(
271 "join_condition",
272 condition.to_expression_string(),
273 ));
274 }
275 if *anti {
276 pairs.push(Pair::new("anti", "true"));
277 }
278 }
279
280 if let crate::executor::streaming::plan::types::OperatorKindSpec::Unary(unary_spec) =
283 &op_spec.spec
284 {
285 let count = unary_spec.subquery_runners().len();
286 if count > 0 {
287 pairs.push(Pair::new("subquery", count.to_string()));
288 }
289 }
290
291 if let crate::executor::streaming::plan::properties::MemoryPolicy::Spillable { threshold } =
292 &props.memory_policy
293 {
294 pairs.push(Pair::new("spill_threshold", format!("{}", threshold)));
295 }
296
297 if let crate::executor::streaming::plan::types::OperatorKindSpec::Graph(
302 crate::executor::streaming::operators::spec::GraphSpec::ExpandAll {
303 count_only,
304 emit_raw_ids,
305 closed_loop,
306 skip_rows,
307 col_names,
308 edge_required_props,
309 dst_required_props,
310 ..
311 },
312 ) = &op_spec.spec
313 {
314 if *count_only {
315 pairs.push(crate::planning::plan::explain::Pair::new(
316 "mode",
317 "count_only",
318 ));
319 pairs.push(crate::planning::plan::explain::Pair::new("unit", "chunks"));
322 } else if *emit_raw_ids {
323 pairs.push(crate::planning::plan::explain::Pair::new("mode", "id_only"));
324 } else if *closed_loop {
325 pairs.push(crate::planning::plan::explain::Pair::new(
326 "mode", "columnar",
327 ));
328 if *skip_rows {
329 pairs.push(crate::planning::plan::explain::Pair::new("rows", "skipped"));
330 let bypass =
331 expand_bypass_display(col_names, edge_required_props, dst_required_props);
332 if !bypass.is_empty() {
333 pairs.push(crate::planning::plan::explain::Pair::new(
334 "bypass",
335 bypass.join(","),
336 ));
337 }
338 }
339 }
340 }
341
342 if let crate::executor::streaming::plan::types::OperatorKindSpec::Source(src_spec) =
344 &op_spec.spec
345 {
346 let col_names: Vec<String> = match src_spec {
347 crate::executor::streaming::operators::spec::SourceSpec::ScanVertices {
348 col_names,
349 ..
350 }
351 | crate::executor::streaming::operators::spec::SourceSpec::StandaloneValues {
352 col_names,
353 ..
354 } => col_names.clone(),
355 crate::executor::streaming::operators::spec::SourceSpec::StorageScanVertices {
356 col_names,
357 identity_only,
358 skip_rows,
359 ..
360 } => {
361 if *identity_only {
362 pairs.push(crate::planning::plan::explain::Pair::new(
363 "mode", "identity",
364 ));
365 }
366 if *skip_rows {
367 pairs.push(crate::planning::plan::explain::Pair::new("rows", "skipped"));
368 }
369 col_names.clone()
370 }
371 crate::executor::streaming::operators::spec::SourceSpec::ScanEdges {
372 col_names,
373 ..
374 } => col_names.clone(),
375 crate::executor::streaming::operators::spec::SourceSpec::StorageScanEdges {
376 col_names,
377 identity_only,
378 ..
379 } => {
380 if *identity_only {
381 pairs.push(crate::planning::plan::explain::Pair::new(
382 "mode", "identity",
383 ));
384 }
385 col_names.clone()
386 }
387 _ => vec![],
388 };
389 if !col_names.is_empty() {
390 pnd.output_var = col_names.join(", ");
391 }
392 let projected: Vec<String> = match src_spec {
397 crate::executor::streaming::operators::spec::SourceSpec::StorageScanVertices {
398 projected_properties,
399 ..
400 }
401 | crate::executor::streaming::operators::spec::SourceSpec::StorageScanEdges {
402 projected_properties,
403 ..
404 }
405 | crate::executor::streaming::operators::spec::SourceSpec::GetVertices {
406 projected_properties,
407 ..
408 }
409 | crate::executor::streaming::operators::spec::SourceSpec::GetEdges {
410 projected_properties,
411 ..
412 }
413 | crate::executor::streaming::operators::spec::SourceSpec::GetNeighbors {
414 projected_properties,
415 ..
416 } => projected_properties.clone(),
417 _ => vec![],
418 };
419 if !projected.is_empty() {
420 pairs.push(crate::planning::plan::explain::Pair::new(
421 "projected",
422 projected.join(","),
423 ));
424 }
425 }
426
427 if !pairs.is_empty() {
428 pnd.description = Some(pairs);
429 }
430
431 if let Some(deps) = parent_of.get(&op_id) {
433 let dep_ids: Vec<i64> = deps.iter().map(|d| d.0 as i64).collect();
434 pnd.set_dependencies(dep_ids);
435 }
436
437 desc.add_node_desc(pnd);
438 }
439
440 desc
441}
442
443fn topological_operator_order(fragments: &FragmentGraph, _plan: &PhysicalPlan) -> Vec<FragmentId> {
445 let root = fragments.root();
446 let mut order = Vec::new();
447 let mut visited = HashSet::new();
448 let mut stack = vec![root];
449
450 while let Some(fid) = stack.pop() {
451 if !visited.insert(fid) {
452 continue;
453 }
454 if let Some(frag) = fragments.get(fid) {
455 for &input in &frag.inputs {
456 stack.push(input);
457 }
458 order.push(fid);
459 }
460 }
461
462 order.reverse();
463 order
464}