Skip to main content

datafusion_physical_plan/
explain.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Defines the EXPLAIN operator
19
20use std::sync::Arc;
21
22use super::{DisplayAs, PlanProperties, SendableRecordBatchStream};
23use crate::execution_plan::{Boundedness, EmissionType};
24use crate::stream::RecordBatchStreamAdapter;
25use crate::{
26    ChildrenPropertiesMode, DisplayFormatType, ExecutionPlan, Partitioning,
27    ReplaceChildrenOptions,
28};
29
30use arrow::{array::StringBuilder, datatypes::SchemaRef, record_batch::RecordBatch};
31use datafusion_common::display::StringifiedPlan;
32use datafusion_common::tree_node::TreeNodeRecursion;
33use datafusion_common::{Result, assert_eq_or_internal_err};
34use datafusion_execution::TaskContext;
35use datafusion_physical_expr::{EquivalenceProperties, PhysicalExpr};
36
37use log::trace;
38
39/// Explain execution plan operator. This operator contains the string
40/// values of the various plans it has when it is created, and passes
41/// them to its output.
42#[derive(Debug, Clone)]
43pub struct ExplainExec {
44    /// The schema that this exec plan node outputs
45    schema: SchemaRef,
46    /// The strings to be printed
47    stringified_plans: Vec<StringifiedPlan>,
48    /// control which plans to print
49    verbose: bool,
50    cache: Arc<PlanProperties>,
51}
52
53impl ExplainExec {
54    /// Create a new ExplainExec
55    pub fn new(
56        schema: SchemaRef,
57        stringified_plans: Vec<StringifiedPlan>,
58        verbose: bool,
59    ) -> Self {
60        let cache = Self::compute_properties(Arc::clone(&schema));
61        ExplainExec {
62            schema,
63            stringified_plans,
64            verbose,
65            cache: Arc::new(cache),
66        }
67    }
68
69    /// The strings to be printed
70    pub fn stringified_plans(&self) -> &[StringifiedPlan] {
71        &self.stringified_plans
72    }
73
74    /// Access to verbose
75    pub fn verbose(&self) -> bool {
76        self.verbose
77    }
78
79    /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
80    fn compute_properties(schema: SchemaRef) -> PlanProperties {
81        PlanProperties::new(
82            EquivalenceProperties::new(schema),
83            Partitioning::UnknownPartitioning(1),
84            EmissionType::Final,
85            Boundedness::Bounded,
86        )
87    }
88}
89
90impl DisplayAs for ExplainExec {
91    fn fmt_as(
92        &self,
93        t: DisplayFormatType,
94        f: &mut std::fmt::Formatter,
95    ) -> std::fmt::Result {
96        match t {
97            DisplayFormatType::Default | DisplayFormatType::Verbose => {
98                write!(f, "ExplainExec")
99            }
100            DisplayFormatType::TreeRender => {
101                // TODO: collect info
102                write!(f, "")
103            }
104        }
105    }
106}
107
108impl ExecutionPlan for ExplainExec {
109    fn name(&self) -> &'static str {
110        "ExplainExec"
111    }
112
113    /// Return a reference to Any that can be used for downcasting
114    fn properties(&self) -> &Arc<PlanProperties> {
115        &self.cache
116    }
117
118    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
119        // This is a leaf node and has no children
120        vec![]
121    }
122
123    fn apply_expressions(
124        &self,
125        _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
126    ) -> Result<TreeNodeRecursion> {
127        Ok(TreeNodeRecursion::Continue)
128    }
129
130    fn replace_children(
131        self: Arc<Self>,
132        _: Vec<Arc<dyn ExecutionPlan>>,
133        _: ReplaceChildrenOptions,
134    ) -> Result<Arc<dyn ExecutionPlan>> {
135        Ok(self)
136    }
137
138    fn with_new_children(
139        self: Arc<Self>,
140        children: Vec<Arc<dyn ExecutionPlan>>,
141    ) -> Result<Arc<dyn ExecutionPlan>> {
142        self.replace_children(
143            children,
144            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
145        )
146    }
147
148    fn execute(
149        &self,
150        partition: usize,
151        context: Arc<TaskContext>,
152    ) -> Result<SendableRecordBatchStream> {
153        trace!(
154            "Start ExplainExec::execute for partition {} of context session_id {} and task_id {:?}",
155            partition,
156            context.session_id(),
157            context.task_id()
158        );
159        assert_eq_or_internal_err!(
160            partition,
161            0,
162            "ExplainExec invalid partition {partition}"
163        );
164        let mut type_builder =
165            StringBuilder::with_capacity(self.stringified_plans.len(), 1024);
166        let mut plan_builder =
167            StringBuilder::with_capacity(self.stringified_plans.len(), 1024);
168
169        let plans_to_print = self
170            .stringified_plans
171            .iter()
172            .filter(|s| s.should_display(self.verbose));
173
174        // Identify plans that are not changed
175        let mut prev: Option<&StringifiedPlan> = None;
176
177        for p in plans_to_print {
178            type_builder.append_value(p.plan_type.to_string());
179            match prev {
180                Some(prev) if !should_show(prev, p) => {
181                    plan_builder.append_value("SAME TEXT AS ABOVE");
182                }
183                Some(_) | None => {
184                    plan_builder.append_value(&*p.plan);
185                }
186            }
187            prev = Some(p);
188        }
189
190        let record_batch = RecordBatch::try_new(
191            Arc::clone(&self.schema),
192            vec![
193                Arc::new(type_builder.finish()),
194                Arc::new(plan_builder.finish()),
195            ],
196        )?;
197
198        trace!(
199            "Before returning RecordBatchStream in ExplainExec::execute for partition {} of context session_id {} and task_id {:?}",
200            partition,
201            context.session_id(),
202            context.task_id()
203        );
204
205        Ok(Box::pin(RecordBatchStreamAdapter::new(
206            Arc::clone(&self.schema),
207            futures::stream::iter(vec![Ok(record_batch)]),
208        )))
209    }
210
211    #[cfg(feature = "proto")]
212    fn try_to_proto(
213        &self,
214        _ctx: &crate::proto::ExecutionPlanEncodeCtx<'_>,
215    ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
216        use datafusion_proto_models::protobuf;
217
218        Ok(Some(protobuf::PhysicalPlanNode {
219            physical_plan_type: Some(
220                protobuf::physical_plan_node::PhysicalPlanType::Explain(
221                    protobuf::ExplainExecNode {
222                        schema: Some(self.schema().as_ref().try_into()?),
223                        stringified_plans: self
224                            .stringified_plans()
225                            .iter()
226                            .map(stringified_plan_to_proto)
227                            .collect(),
228                        verbose: self.verbose(),
229                    },
230                ),
231            ),
232        }))
233    }
234}
235
236#[cfg(feature = "proto")]
237impl ExplainExec {
238    /// Reconstruct an [`ExplainExec`] from its protobuf representation.
239    pub fn try_from_proto(
240        node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
241        _ctx: &crate::proto::ExecutionPlanDecodeCtx<'_>,
242    ) -> Result<Arc<dyn ExecutionPlan>> {
243        use datafusion_proto_models::protobuf;
244
245        let explain = crate::expect_plan_variant!(
246            node,
247            protobuf::physical_plan_node::PhysicalPlanType::Explain,
248            "ExplainExec",
249        );
250        let schema = explain.schema.as_ref().ok_or_else(|| {
251            datafusion_common::internal_datafusion_err!(
252                "ExplainExec is missing required field 'schema'"
253            )
254        })?;
255        Ok(Arc::new(ExplainExec::new(
256            Arc::new(arrow::datatypes::Schema::try_from(schema)?),
257            explain
258                .stringified_plans
259                .iter()
260                .map(stringified_plan_from_proto)
261                .collect(),
262            explain.verbose,
263        )))
264    }
265}
266
267#[cfg(feature = "proto")]
268fn stringified_plan_to_proto(
269    stringified_plan: &StringifiedPlan,
270) -> datafusion_proto_models::protobuf::StringifiedPlan {
271    use datafusion_common::display::PlanType;
272    use datafusion_proto_models::datafusion_common::EmptyMessage;
273    use datafusion_proto_models::protobuf;
274    use protobuf::plan_type::PlanTypeEnum::{
275        AnalyzedLogicalPlan, FinalAnalyzedLogicalPlan, FinalLogicalPlan,
276        FinalPhysicalPlan, FinalPhysicalPlanWithSchema, FinalPhysicalPlanWithStats,
277        InitialLogicalPlan, InitialPhysicalPlan, InitialPhysicalPlanWithSchema,
278        InitialPhysicalPlanWithStats, OptimizedLogicalPlan, OptimizedPhysicalPlan,
279        PhysicalPlanError,
280    };
281
282    protobuf::StringifiedPlan {
283        plan_type: match stringified_plan.clone().plan_type {
284            PlanType::InitialLogicalPlan => Some(protobuf::PlanType {
285                plan_type_enum: Some(InitialLogicalPlan(EmptyMessage {})),
286            }),
287            PlanType::AnalyzedLogicalPlan { analyzer_name } => Some(protobuf::PlanType {
288                plan_type_enum: Some(AnalyzedLogicalPlan(
289                    protobuf::AnalyzedLogicalPlanType { analyzer_name },
290                )),
291            }),
292            PlanType::FinalAnalyzedLogicalPlan => Some(protobuf::PlanType {
293                plan_type_enum: Some(FinalAnalyzedLogicalPlan(EmptyMessage {})),
294            }),
295            PlanType::OptimizedLogicalPlan { optimizer_name } => {
296                Some(protobuf::PlanType {
297                    plan_type_enum: Some(OptimizedLogicalPlan(
298                        protobuf::OptimizedLogicalPlanType { optimizer_name },
299                    )),
300                })
301            }
302            PlanType::FinalLogicalPlan => Some(protobuf::PlanType {
303                plan_type_enum: Some(FinalLogicalPlan(EmptyMessage {})),
304            }),
305            PlanType::InitialPhysicalPlan => Some(protobuf::PlanType {
306                plan_type_enum: Some(InitialPhysicalPlan(EmptyMessage {})),
307            }),
308            PlanType::OptimizedPhysicalPlan { optimizer_name } => {
309                Some(protobuf::PlanType {
310                    plan_type_enum: Some(OptimizedPhysicalPlan(
311                        protobuf::OptimizedPhysicalPlanType { optimizer_name },
312                    )),
313                })
314            }
315            PlanType::FinalPhysicalPlan => Some(protobuf::PlanType {
316                plan_type_enum: Some(FinalPhysicalPlan(EmptyMessage {})),
317            }),
318            PlanType::InitialPhysicalPlanWithStats => Some(protobuf::PlanType {
319                plan_type_enum: Some(InitialPhysicalPlanWithStats(EmptyMessage {})),
320            }),
321            PlanType::InitialPhysicalPlanWithSchema => Some(protobuf::PlanType {
322                plan_type_enum: Some(InitialPhysicalPlanWithSchema(EmptyMessage {})),
323            }),
324            PlanType::FinalPhysicalPlanWithStats => Some(protobuf::PlanType {
325                plan_type_enum: Some(FinalPhysicalPlanWithStats(EmptyMessage {})),
326            }),
327            PlanType::FinalPhysicalPlanWithSchema => Some(protobuf::PlanType {
328                plan_type_enum: Some(FinalPhysicalPlanWithSchema(EmptyMessage {})),
329            }),
330            PlanType::PhysicalPlanError => Some(protobuf::PlanType {
331                plan_type_enum: Some(PhysicalPlanError(EmptyMessage {})),
332            }),
333        },
334        plan: stringified_plan.plan.to_string(),
335    }
336}
337
338#[cfg(feature = "proto")]
339fn stringified_plan_from_proto(
340    stringified_plan: &datafusion_proto_models::protobuf::StringifiedPlan,
341) -> StringifiedPlan {
342    use datafusion_common::display::PlanType;
343    use datafusion_proto_models::protobuf::plan_type::PlanTypeEnum::{
344        AnalyzedLogicalPlan, FinalAnalyzedLogicalPlan, FinalLogicalPlan,
345        FinalPhysicalPlan, FinalPhysicalPlanWithSchema, FinalPhysicalPlanWithStats,
346        InitialLogicalPlan, InitialPhysicalPlan, InitialPhysicalPlanWithSchema,
347        InitialPhysicalPlanWithStats, OptimizedLogicalPlan, OptimizedPhysicalPlan,
348        PhysicalPlanError,
349    };
350    use datafusion_proto_models::protobuf::{
351        AnalyzedLogicalPlanType, OptimizedLogicalPlanType, OptimizedPhysicalPlanType,
352    };
353
354    StringifiedPlan {
355        plan_type: match stringified_plan
356            .plan_type
357            .as_ref()
358            .and_then(|plan_type| plan_type.plan_type_enum.as_ref())
359            .unwrap_or_else(|| {
360                panic!(
361                    "Cannot create protobuf::StringifiedPlan from {stringified_plan:?}"
362                )
363            }) {
364            InitialLogicalPlan(_) => PlanType::InitialLogicalPlan,
365            AnalyzedLogicalPlan(AnalyzedLogicalPlanType { analyzer_name }) => {
366                PlanType::AnalyzedLogicalPlan {
367                    analyzer_name: analyzer_name.clone(),
368                }
369            }
370            FinalAnalyzedLogicalPlan(_) => PlanType::FinalAnalyzedLogicalPlan,
371            OptimizedLogicalPlan(OptimizedLogicalPlanType { optimizer_name }) => {
372                PlanType::OptimizedLogicalPlan {
373                    optimizer_name: optimizer_name.clone(),
374                }
375            }
376            FinalLogicalPlan(_) => PlanType::FinalLogicalPlan,
377            InitialPhysicalPlan(_) => PlanType::InitialPhysicalPlan,
378            InitialPhysicalPlanWithStats(_) => PlanType::InitialPhysicalPlanWithStats,
379            InitialPhysicalPlanWithSchema(_) => PlanType::InitialPhysicalPlanWithSchema,
380            OptimizedPhysicalPlan(OptimizedPhysicalPlanType { optimizer_name }) => {
381                PlanType::OptimizedPhysicalPlan {
382                    optimizer_name: optimizer_name.clone(),
383                }
384            }
385            FinalPhysicalPlan(_) => PlanType::FinalPhysicalPlan,
386            FinalPhysicalPlanWithStats(_) => PlanType::FinalPhysicalPlanWithStats,
387            FinalPhysicalPlanWithSchema(_) => PlanType::FinalPhysicalPlanWithSchema,
388            PhysicalPlanError(_) => PlanType::PhysicalPlanError,
389        },
390        plan: Arc::new(stringified_plan.plan.clone()),
391    }
392}
393
394/// If this plan should be shown, given the previous plan that was
395/// displayed.
396///
397/// This is meant to avoid repeating the same plan over and over again
398/// in explain plans to make clear what is changing
399fn should_show(previous_plan: &StringifiedPlan, this_plan: &StringifiedPlan) -> bool {
400    // if the plans are different, or if they would have been
401    // displayed in the normal explain (aka non verbose) plan
402    (previous_plan.plan != this_plan.plan) || this_plan.should_display(false)
403}