diff --git a/datafusion-examples/examples/planner_api.rs b/datafusion-examples/examples/planner_api.rs index e52f0d78682f..41110a3e0a9c 100644 --- a/datafusion-examples/examples/planner_api.rs +++ b/datafusion-examples/examples/planner_api.rs @@ -17,7 +17,7 @@ use datafusion::error::Result; use datafusion::logical_expr::{LogicalPlan, PlanType}; -use datafusion::physical_plan::displayable; +use datafusion::physical_plan::{displayable, DisplayFormatType}; use datafusion::physical_planner::DefaultPhysicalPlanner; use datafusion::prelude::*; @@ -78,7 +78,11 @@ async fn to_physical_plan_in_one_api_demo( println!( "Physical plan direct from logical plan:\n\n{}\n\n", displayable(physical_plan.as_ref()) - .to_stringified(false, PlanType::InitialPhysicalPlan) + .to_stringified( + false, + PlanType::InitialPhysicalPlan, + DisplayFormatType::Default + ) .plan ); @@ -120,7 +124,11 @@ async fn to_physical_plan_step_by_step_demo( println!( "Final physical plan:\n\n{}\n\n", displayable(physical_plan.as_ref()) - .to_stringified(false, PlanType::InitialPhysicalPlan) + .to_stringified( + false, + PlanType::InitialPhysicalPlan, + DisplayFormatType::Default + ) .plan ); @@ -135,7 +143,11 @@ async fn to_physical_plan_step_by_step_demo( println!( "Optimized physical plan:\n\n{}\n\n", displayable(physical_plan.as_ref()) - .to_stringified(false, PlanType::InitialPhysicalPlan) + .to_stringified( + false, + PlanType::InitialPhysicalPlan, + DisplayFormatType::Default + ) .plan ); diff --git a/datafusion/common/src/config.rs b/datafusion/common/src/config.rs index e2f61cfc0866..8c093a9db899 100644 --- a/datafusion/common/src/config.rs +++ b/datafusion/common/src/config.rs @@ -711,6 +711,10 @@ config_namespace! { /// When set to true, the explain statement will print schema information pub show_schema: bool, default = false + + /// Display format of explain. Default is "indent". + /// When set to "tree", it will print the plan in a tree-rendered format. + pub format: String, default = "indent".to_string() } } diff --git a/datafusion/core/src/datasource/file_format/arrow.rs b/datafusion/core/src/datasource/file_format/arrow.rs index 3614b788af90..6835d9a6da2a 100644 --- a/datafusion/core/src/datasource/file_format/arrow.rs +++ b/datafusion/core/src/datasource/file_format/arrow.rs @@ -301,6 +301,10 @@ impl DisplayAs for ArrowFileSink { FileGroupDisplay(&self.config.file_groups).fmt_as(t, f)?; write!(f, ")") } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/core/src/datasource/file_format/csv.rs b/datafusion/core/src/datasource/file_format/csv.rs index 9c6d42007d12..ca22a85d35c1 100644 --- a/datafusion/core/src/datasource/file_format/csv.rs +++ b/datafusion/core/src/datasource/file_format/csv.rs @@ -16,7 +16,6 @@ // under the License. //! Re-exports the [`datafusion_datasource_csv::file_format`] module, and contains tests for it. - pub use datafusion_datasource_csv::file_format::*; #[cfg(test)] diff --git a/datafusion/core/src/datasource/file_format/json.rs b/datafusion/core/src/datasource/file_format/json.rs index 49b3378e1e68..ef9dd08c1c26 100644 --- a/datafusion/core/src/datasource/file_format/json.rs +++ b/datafusion/core/src/datasource/file_format/json.rs @@ -16,7 +16,6 @@ // under the License. //! Re-exports the [`datafusion_datasource_json::file_format`] module, and contains tests for it. - pub use datafusion_datasource_json::file_format::*; #[cfg(test)] diff --git a/datafusion/core/src/datasource/memory.rs b/datafusion/core/src/datasource/memory.rs index b8bec410070c..d96944fa7a69 100644 --- a/datafusion/core/src/datasource/memory.rs +++ b/datafusion/core/src/datasource/memory.rs @@ -315,6 +315,10 @@ impl DisplayAs for MemSink { let partition_count = self.batches.len(); write!(f, "MemoryTable (partitions={partition_count})") } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/core/src/physical_planner.rs b/datafusion/core/src/physical_planner.rs index a74cdcc5920b..eef3d1b79694 100644 --- a/datafusion/core/src/physical_planner.rs +++ b/datafusion/core/src/physical_planner.rs @@ -19,6 +19,7 @@ use std::borrow::Cow; use std::collections::HashMap; +use std::str::FromStr; use std::sync::Arc; use crate::datasource::file_format::file_type_to_format; @@ -87,6 +88,7 @@ use datafusion_physical_optimizer::PhysicalOptimizerRule; use datafusion_physical_plan::execution_plan::InvariantLevel; use datafusion_physical_plan::placeholder_row::PlaceholderRowExec; use datafusion_physical_plan::unnest::ListUnnest; +use datafusion_physical_plan::DisplayFormatType; use crate::schema_equivalence::schema_satisfied_by; use async_trait::async_trait; @@ -1723,6 +1725,7 @@ impl DefaultPhysicalPlanner { let mut stringified_plans = vec![]; let config = &session_state.config_options().explain; + let explain_format = DisplayFormatType::from_str(&config.format)?; if !config.physical_plan_only { stringified_plans.clone_from(&e.stringified_plans); @@ -1742,7 +1745,11 @@ impl DefaultPhysicalPlanner { displayable(input.as_ref()) .set_show_statistics(config.show_statistics) .set_show_schema(config.show_schema) - .to_stringified(e.verbose, InitialPhysicalPlan), + .to_stringified( + e.verbose, + InitialPhysicalPlan, + explain_format, + ), ); // Show statistics + schema in verbose output even if not @@ -1755,6 +1762,7 @@ impl DefaultPhysicalPlanner { .to_stringified( e.verbose, InitialPhysicalPlanWithStats, + explain_format, ), ); } @@ -1765,6 +1773,7 @@ impl DefaultPhysicalPlanner { .to_stringified( e.verbose, InitialPhysicalPlanWithSchema, + explain_format, ), ); } @@ -1780,7 +1789,11 @@ impl DefaultPhysicalPlanner { displayable(plan) .set_show_statistics(config.show_statistics) .set_show_schema(config.show_schema) - .to_stringified(e.verbose, plan_type), + .to_stringified( + e.verbose, + plan_type, + explain_format, + ), ); }, ); @@ -1791,7 +1804,11 @@ impl DefaultPhysicalPlanner { displayable(input.as_ref()) .set_show_statistics(config.show_statistics) .set_show_schema(config.show_schema) - .to_stringified(e.verbose, FinalPhysicalPlan), + .to_stringified( + e.verbose, + FinalPhysicalPlan, + explain_format, + ), ); // Show statistics + schema in verbose output even if not @@ -1804,6 +1821,7 @@ impl DefaultPhysicalPlanner { .to_stringified( e.verbose, FinalPhysicalPlanWithStats, + explain_format, ), ); } @@ -1814,6 +1832,7 @@ impl DefaultPhysicalPlanner { .to_stringified( e.verbose, FinalPhysicalPlanWithSchema, + explain_format, ), ); } @@ -2720,6 +2739,10 @@ mod tests { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "NoOpExecutionPlan") } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/core/tests/custom_sources_cases/mod.rs b/datafusion/core/tests/custom_sources_cases/mod.rs index aafefac04e32..eb930b9a60bc 100644 --- a/datafusion/core/tests/custom_sources_cases/mod.rs +++ b/datafusion/core/tests/custom_sources_cases/mod.rs @@ -138,6 +138,11 @@ impl DisplayAs for CustomExecutionPlan { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "CustomExecutionPlan: projection={:#?}", self.projection) } + + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/core/tests/custom_sources_cases/provider_filter_pushdown.rs b/datafusion/core/tests/custom_sources_cases/provider_filter_pushdown.rs index af0506a50558..f68bcfaf1550 100644 --- a/datafusion/core/tests/custom_sources_cases/provider_filter_pushdown.rs +++ b/datafusion/core/tests/custom_sources_cases/provider_filter_pushdown.rs @@ -92,6 +92,10 @@ impl DisplayAs for CustomPlan { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "CustomPlan: batch_size={}", self.batches.len(),) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/core/tests/custom_sources_cases/statistics.rs b/datafusion/core/tests/custom_sources_cases/statistics.rs index 1fd6dfec79fb..66c886510e96 100644 --- a/datafusion/core/tests/custom_sources_cases/statistics.rs +++ b/datafusion/core/tests/custom_sources_cases/statistics.rs @@ -141,6 +141,10 @@ impl DisplayAs for StatisticsValidation { self.stats.num_rows, ) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/core/tests/physical_optimizer/enforce_distribution.rs b/datafusion/core/tests/physical_optimizer/enforce_distribution.rs index 66d1380e09c3..fc2394d889d1 100644 --- a/datafusion/core/tests/physical_optimizer/enforce_distribution.rs +++ b/datafusion/core/tests/physical_optimizer/enforce_distribution.rs @@ -99,10 +99,18 @@ impl SortRequiredExec { impl DisplayAs for SortRequiredExec { fn fmt_as( &self, - _t: DisplayFormatType, + t: DisplayFormatType, f: &mut std::fmt::Formatter, ) -> std::fmt::Result { - write!(f, "SortRequiredExec: [{}]", self.expr) + match t { + DisplayFormatType::Default | DisplayFormatType::Verbose => { + write!(f, "SortRequiredExec: [{}]", self.expr) + } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } + } } } diff --git a/datafusion/core/tests/physical_optimizer/join_selection.rs b/datafusion/core/tests/physical_optimizer/join_selection.rs index 375af94acaf4..d3b6ec700bee 100644 --- a/datafusion/core/tests/physical_optimizer/join_selection.rs +++ b/datafusion/core/tests/physical_optimizer/join_selection.rs @@ -924,6 +924,10 @@ impl DisplayAs for UnboundedExec { self.batch_produce.is_none(), ) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } @@ -1019,6 +1023,11 @@ impl DisplayAs for StatisticsExec { self.stats.num_rows, ) } + + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/core/tests/physical_optimizer/test_utils.rs b/datafusion/core/tests/physical_optimizer/test_utils.rs index c7572eb08900..1b8e754ee357 100644 --- a/datafusion/core/tests/physical_optimizer/test_utils.rs +++ b/datafusion/core/tests/physical_optimizer/test_utils.rs @@ -339,8 +339,16 @@ impl RequirementsTestExec { } impl DisplayAs for RequirementsTestExec { - fn fmt_as(&self, _t: DisplayFormatType, f: &mut Formatter) -> std::fmt::Result { - write!(f, "RequiredInputOrderingExec") + fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> std::fmt::Result { + match t { + DisplayFormatType::Default | DisplayFormatType::Verbose => { + write!(f, "RequiredInputOrderingExec") + } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } + } } } diff --git a/datafusion/core/tests/user_defined/user_defined_plan.rs b/datafusion/core/tests/user_defined/user_defined_plan.rs index fae4b2cd82ab..915d61712074 100644 --- a/datafusion/core/tests/user_defined/user_defined_plan.rs +++ b/datafusion/core/tests/user_defined/user_defined_plan.rs @@ -700,6 +700,10 @@ impl DisplayAs for TopKExec { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "TopKExec: k={}", self.k) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/datasource-csv/src/file_format.rs b/datafusion/datasource-csv/src/file_format.rs index 2e083f9ef22f..cab561d163b3 100644 --- a/datafusion/datasource-csv/src/file_format.rs +++ b/datafusion/datasource-csv/src/file_format.rs @@ -665,6 +665,10 @@ impl DisplayAs for CsvSink { FileGroupDisplay(&self.config.file_groups).fmt_as(t, f)?; write!(f, ")") } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/datasource-json/src/file_format.rs b/datafusion/datasource-json/src/file_format.rs index 71106c316b25..2df49b535128 100644 --- a/datafusion/datasource-json/src/file_format.rs +++ b/datafusion/datasource-json/src/file_format.rs @@ -324,6 +324,10 @@ impl DisplayAs for JsonSink { FileGroupDisplay(&self.config.file_groups).fmt_as(t, f)?; write!(f, ")") } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/datasource-parquet/src/file_format.rs b/datafusion/datasource-parquet/src/file_format.rs index 78f78aad4c15..268bbf453505 100644 --- a/datafusion/datasource-parquet/src/file_format.rs +++ b/datafusion/datasource-parquet/src/file_format.rs @@ -871,6 +871,10 @@ impl DisplayAs for ParquetSink { FileGroupDisplay(&self.config.file_groups).fmt_as(t, f)?; write!(f, ")") } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/datasource-parquet/src/source.rs b/datafusion/datasource-parquet/src/source.rs index b9a2f3e999d0..51b38f04fa95 100644 --- a/datafusion/datasource-parquet/src/source.rs +++ b/datafusion/datasource-parquet/src/source.rs @@ -554,7 +554,9 @@ impl FileSource for ParquetSource { fn fmt_extra(&self, t: DisplayFormatType, f: &mut Formatter) -> std::fmt::Result { match t { - DisplayFormatType::Default | DisplayFormatType::Verbose => { + DisplayFormatType::Default + | DisplayFormatType::Verbose + | DisplayFormatType::TreeRender => { let predicate_string = self .predicate() .map(|p| format!(", predicate={p}")) diff --git a/datafusion/datasource/src/display.rs b/datafusion/datasource/src/display.rs index 58fc27bb8010..7ab8d407be52 100644 --- a/datafusion/datasource/src/display.rs +++ b/datafusion/datasource/src/display.rs @@ -36,7 +36,7 @@ impl DisplayAs for FileGroupsDisplay<'_> { let groups = if n_groups == 1 { "group" } else { "groups" }; write!(f, "{{{n_groups} {groups}: [")?; match t { - DisplayFormatType::Default => { + DisplayFormatType::Default | DisplayFormatType::TreeRender => { // To avoid showing too many partitions let max_groups = 5; fmt_up_to_n_elements(self.0, max_groups, f, |group, f| { @@ -66,7 +66,7 @@ impl DisplayAs for FileGroupDisplay<'_> { fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> FmtResult { write!(f, "[")?; match t { - DisplayFormatType::Default => { + DisplayFormatType::Default | DisplayFormatType::TreeRender => { // To avoid showing too many files let max_files = 5; fmt_up_to_n_elements(self.0, max_files, f, |pf, f| { diff --git a/datafusion/datasource/src/file_scan_config.rs b/datafusion/datasource/src/file_scan_config.rs index 933c8f87e9ed..66ef6262688e 100644 --- a/datafusion/datasource/src/file_scan_config.rs +++ b/datafusion/datasource/src/file_scan_config.rs @@ -194,26 +194,34 @@ impl DataSource for FileScanConfig { } fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> FmtResult { - let (schema, _, _, orderings) = self.project(); + match t { + DisplayFormatType::Default | DisplayFormatType::Verbose => { + let (schema, _, _, orderings) = self.project(); - write!(f, "file_groups=")?; - FileGroupsDisplay(&self.file_groups).fmt_as(t, f)?; + write!(f, "file_groups=")?; + FileGroupsDisplay(&self.file_groups).fmt_as(t, f)?; - if !schema.fields().is_empty() { - write!(f, ", projection={}", ProjectSchemaDisplay(&schema))?; - } + if !schema.fields().is_empty() { + write!(f, ", projection={}", ProjectSchemaDisplay(&schema))?; + } - if let Some(limit) = self.limit { - write!(f, ", limit={limit}")?; - } + if let Some(limit) = self.limit { + write!(f, ", limit={limit}")?; + } - display_orderings(f, &orderings)?; + display_orderings(f, &orderings)?; - if !self.constraints.is_empty() { - write!(f, ", {}", self.constraints)?; - } + if !self.constraints.is_empty() { + write!(f, ", {}", self.constraints)?; + } - self.fmt_file_source(t, f) + self.fmt_file_source(t, f) + } + DisplayFormatType::TreeRender => { + // TODO: collect info + Ok(()) + } + } } /// If supported by the underlying [`FileSource`], redistribute files across partitions according to their size. diff --git a/datafusion/datasource/src/memory.rs b/datafusion/datasource/src/memory.rs index 363ef4348e91..1ea0f2ea2e8b 100644 --- a/datafusion/datasource/src/memory.rs +++ b/datafusion/datasource/src/memory.rs @@ -412,10 +412,10 @@ impl DataSource for MemorySourceConfig { .map_or(String::new(), |limit| format!(", fetch={}", limit)); if self.show_sizes { write!( - f, - "partitions={}, partition_sizes={partition_sizes:?}{limit}{output_ordering}{constraints}", - partition_sizes.len(), - ) + f, + "partitions={}, partition_sizes={partition_sizes:?}{limit}{output_ordering}{constraints}", + partition_sizes.len(), + ) } else { write!( f, @@ -424,6 +424,27 @@ impl DataSource for MemorySourceConfig { ) } } + DisplayFormatType::TreeRender => { + let partition_sizes: Vec<_> = + self.partitions.iter().map(|b| b.len()).collect(); + writeln!(f, "partition_sizes={:?}", partition_sizes)?; + + if let Some(output_ordering) = self.sort_information.first() { + writeln!(f, "output_ordering={}", output_ordering)?; + } + + let eq_properties = self.eq_properties(); + let constraints = eq_properties.constraints(); + if !constraints.is_empty() { + writeln!(f, "constraints={}", constraints)?; + } + + if let Some(limit) = self.fetch { + writeln!(f, "fetch={}", limit)?; + } + + write!(f, "partitions={}", partition_sizes.len()) + } } } diff --git a/datafusion/datasource/src/source.rs b/datafusion/datasource/src/source.rs index 07cee7fba00e..bb9790e875b9 100644 --- a/datafusion/datasource/src/source.rs +++ b/datafusion/datasource/src/source.rs @@ -99,7 +99,12 @@ pub struct DataSourceExec { impl DisplayAs for DataSourceExec { fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> fmt::Result { - write!(f, "DataSourceExec: ")?; + match t { + DisplayFormatType::Default | DisplayFormatType::Verbose => { + write!(f, "DataSourceExec: ")?; + } + DisplayFormatType::TreeRender => write!(f, "")?, + } self.data_source.fmt_as(t, f) } } diff --git a/datafusion/ffi/src/execution_plan.rs b/datafusion/ffi/src/execution_plan.rs index 00602474d621..14a0908c4795 100644 --- a/datafusion/ffi/src/execution_plan.rs +++ b/datafusion/ffi/src/execution_plan.rs @@ -21,12 +21,12 @@ use abi_stable::{ std_types::{RResult, RString, RVec}, StableAbi, }; -use datafusion::error::Result; use datafusion::{ error::DataFusionError, execution::{SendableRecordBatchStream, TaskContext}, physical_plan::{DisplayAs, ExecutionPlan, PlanProperties}, }; +use datafusion::{error::Result, physical_plan::DisplayFormatType}; use tokio::runtime::Handle; use crate::{ @@ -198,14 +198,22 @@ unsafe impl Sync for ForeignExecutionPlan {} impl DisplayAs for ForeignExecutionPlan { fn fmt_as( &self, - _t: datafusion::physical_plan::DisplayFormatType, + t: DisplayFormatType, f: &mut std::fmt::Formatter, ) -> std::fmt::Result { - write!( - f, - "FFI_ExecutionPlan(number_of_children={})", - self.children.len(), - ) + match t { + DisplayFormatType::Default | DisplayFormatType::Verbose => { + write!( + f, + "FFI_ExecutionPlan(number_of_children={})", + self.children.len(), + ) + } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } + } } } @@ -315,7 +323,7 @@ mod tests { impl DisplayAs for EmptyExec { fn fmt_as( &self, - _t: datafusion::physical_plan::DisplayFormatType, + _t: DisplayFormatType, _f: &mut std::fmt::Formatter, ) -> std::fmt::Result { unimplemented!() diff --git a/datafusion/physical-optimizer/src/output_requirements.rs b/datafusion/physical-optimizer/src/output_requirements.rs index 90a570894a44..3ca0547aa11d 100644 --- a/datafusion/physical-optimizer/src/output_requirements.rs +++ b/datafusion/physical-optimizer/src/output_requirements.rs @@ -132,10 +132,18 @@ impl OutputRequirementExec { impl DisplayAs for OutputRequirementExec { fn fmt_as( &self, - _t: DisplayFormatType, + t: DisplayFormatType, f: &mut std::fmt::Formatter, ) -> std::fmt::Result { - write!(f, "OutputRequirementExec") + match t { + DisplayFormatType::Default | DisplayFormatType::Verbose => { + write!(f, "OutputRequirementExec") + } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } + } } } diff --git a/datafusion/physical-plan/src/aggregates/mod.rs b/datafusion/physical-plan/src/aggregates/mod.rs index bac9a4287a73..7d4837d04774 100644 --- a/datafusion/physical-plan/src/aggregates/mod.rs +++ b/datafusion/physical-plan/src/aggregates/mod.rs @@ -808,6 +808,10 @@ impl DisplayAs for AggregateExec { write!(f, ", ordering_mode={:?}", self.input_order_mode)?; } } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "")?; + } } Ok(()) } @@ -1816,6 +1820,10 @@ mod tests { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "TestYieldingExec") } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/analyze.rs b/datafusion/physical-plan/src/analyze.rs index 708f006b0d39..ea14ce676c1a 100644 --- a/datafusion/physical-plan/src/analyze.rs +++ b/datafusion/physical-plan/src/analyze.rs @@ -108,6 +108,10 @@ impl DisplayAs for AnalyzeExec { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "AnalyzeExec verbose={}", self.verbose) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/coalesce_batches.rs b/datafusion/physical-plan/src/coalesce_batches.rs index fa8d125d62d1..0eb95bb66598 100644 --- a/datafusion/physical-plan/src/coalesce_batches.rs +++ b/datafusion/physical-plan/src/coalesce_batches.rs @@ -122,6 +122,10 @@ impl DisplayAs for CoalesceBatchesExec { Ok(()) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/coalesce_partitions.rs b/datafusion/physical-plan/src/coalesce_partitions.rs index 9a955155c01e..8fb40640dcc0 100644 --- a/datafusion/physical-plan/src/coalesce_partitions.rs +++ b/datafusion/physical-plan/src/coalesce_partitions.rs @@ -92,6 +92,10 @@ impl DisplayAs for CoalescePartitionsExec { } None => write!(f, "CoalescePartitionsExec"), }, + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/display.rs b/datafusion/physical-plan/src/display.rs index 0cc1cb02438a..26e0a822f588 100644 --- a/datafusion/physical-plan/src/display.rs +++ b/datafusion/physical-plan/src/display.rs @@ -18,15 +18,19 @@ //! Implementation of physical plan display. See //! [`crate::displayable`] for examples of how to format -use std::fmt; +use std::collections::{BTreeMap, HashMap}; use std::fmt::Formatter; +use std::{fmt, str::FromStr}; use arrow::datatypes::SchemaRef; use datafusion_common::display::{GraphvizBuilder, PlanType, StringifiedPlan}; +use datafusion_common::DataFusionError; use datafusion_expr::display_schema; use datafusion_physical_expr::LexOrdering; +use crate::render_tree::RenderTree; + use super::{accept, ExecutionPlan, ExecutionPlanVisitor}; /// Options for controlling how each [`ExecutionPlan`] should format itself @@ -36,6 +40,23 @@ pub enum DisplayFormatType { Default, /// Verbose, showing all available details Verbose, + /// TreeRender, display plan like a tree. + TreeRender, +} + +impl FromStr for DisplayFormatType { + type Err = DataFusionError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "indent" => Ok(Self::Default), + "tree" => Ok(Self::TreeRender), + _ => Err(DataFusionError::Configuration(format!( + "Invalid explain format: {}", + s + ))), + } + } } /// Wraps an `ExecutionPlan` with various methods for formatting @@ -224,6 +245,19 @@ impl<'a> DisplayableExecutionPlan<'a> { } } + pub fn tree_render(&self) -> impl fmt::Display + 'a { + struct Wrapper<'a> { + plan: &'a dyn ExecutionPlan, + } + impl fmt::Display for Wrapper<'_> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let mut visitor = TreeRenderVisitor { f }; + visitor.visit(self.plan) + } + } + Wrapper { plan: self.inner } + } + /// Return a single-line summary of the root of the plan /// Example: `ProjectionExec: expr=[a@0 as a]`. pub fn one_line(&self) -> impl fmt::Display + 'a { @@ -258,8 +292,18 @@ impl<'a> DisplayableExecutionPlan<'a> { } /// format as a `StringifiedPlan` - pub fn to_stringified(&self, verbose: bool, plan_type: PlanType) -> StringifiedPlan { - StringifiedPlan::new(plan_type, self.indent(verbose).to_string()) + pub fn to_stringified( + &self, + verbose: bool, + plan_type: PlanType, + explain_format: DisplayFormatType, + ) -> StringifiedPlan { + match (&explain_format, &plan_type) { + (DisplayFormatType::TreeRender, PlanType::FinalPhysicalPlan) => { + StringifiedPlan::new(plan_type, self.tree_render().to_string()) + } + _ => StringifiedPlan::new(plan_type, self.indent(verbose).to_string()), + } } } @@ -448,6 +492,430 @@ impl ExecutionPlanVisitor for GraphvizVisitor<'_, '_> { } } +/// This module implements a tree-like art renderer for execution plans, +/// based on DuckDB's implementation: +/// +/// +/// The rendered output looks like this: +/// ```text +/// ┌───────────────────────────┐ +/// │ CoalesceBatchesExec │ +/// └─────────────┬─────────────┘ +/// ┌─────────────┴─────────────┐ +/// │ HashJoinExec ├──────────────┐ +/// └─────────────┬─────────────┘ │ +/// ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +/// │ DataSourceExec ││ DataSourceExec │ +/// └───────────────────────────┘└───────────────────────────┘ +/// ``` +/// +/// The renderer uses a three-layer approach for each node: +/// 1. Top layer: renders the top borders and connections +/// 2. Content layer: renders the node content and vertical connections +/// 3. Bottom layer: renders the bottom borders and connections +/// +/// Each node is rendered in a box of fixed width (NODE_RENDER_WIDTH). +struct TreeRenderVisitor<'a, 'b> { + /// Write to this formatter + f: &'a mut Formatter<'b>, +} + +impl TreeRenderVisitor<'_, '_> { + // Unicode box-drawing characters for creating borders and connections. + const LTCORNER: &'static str = "┌"; // Left top corner + const RTCORNER: &'static str = "┐"; // Right top corner + const LDCORNER: &'static str = "└"; // Left bottom corner + const RDCORNER: &'static str = "┘"; // Right bottom corner + + const TMIDDLE: &'static str = "┬"; // Top T-junction (connects down) + const LMIDDLE: &'static str = "├"; // Left T-junction (connects right) + const DMIDDLE: &'static str = "┴"; // Bottom T-junction (connects up) + + const VERTICAL: &'static str = "│"; // Vertical line + const HORIZONTAL: &'static str = "─"; // Horizontal line + + // TODO: Make these variables configurable. + const MAXIMUM_RENDER_WIDTH: usize = 240; // Maximum total width of the rendered tree + const NODE_RENDER_WIDTH: usize = 29; // Width of each node's box + const MAX_EXTRA_LINES: usize = 30; // Maximum number of extra info lines per node + + /// Main entry point for rendering an execution plan as a tree. + /// The rendering process happens in three stages for each level of the tree: + /// 1. Render top borders and connections + /// 2. Render node content and vertical connections + /// 3. Render bottom borders and connections + pub fn visit(&mut self, plan: &dyn ExecutionPlan) -> Result<(), fmt::Error> { + let root = RenderTree::create_tree(plan); + + for y in 0..root.height { + // Start by rendering the top layer. + self.render_top_layer(&root, y)?; + // Now we render the content of the boxes + self.render_box_content(&root, y)?; + // Render the bottom layer of each of the boxes + self.render_bottom_layer(&root, y)?; + } + + Ok(()) + } + + /// Renders the top layer of boxes at the given y-level of the tree. + /// This includes: + /// - Top corners (┌─┐) for nodes + /// - Horizontal connections between nodes + /// - Vertical connections to parent nodes + fn render_top_layer( + &mut self, + root: &RenderTree, + y: usize, + ) -> Result<(), fmt::Error> { + for x in 0..root.width { + if root.has_node(x, y) { + write!(self.f, "{}", Self::LTCORNER)?; + write!( + self.f, + "{}", + Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2 - 1) + )?; + if y == 0 { + // top level node: no node above this one + write!(self.f, "{}", Self::HORIZONTAL)?; + } else { + // render connection to node above this one + write!(self.f, "{}", Self::DMIDDLE)?; + } + write!( + self.f, + "{}", + Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2 - 1) + )?; + write!(self.f, "{}", Self::RTCORNER)?; + } else { + let mut has_adjacent_nodes = false; + for i in 0..(root.width - x) { + has_adjacent_nodes = has_adjacent_nodes || root.has_node(x + i, y); + } + if !has_adjacent_nodes { + // There are no nodes to the right side of this position + // no need to fill the empty space + continue; + } + // there are nodes next to this, fill the space + write!(self.f, "{}", &" ".repeat(Self::NODE_RENDER_WIDTH))?; + } + } + writeln!(self.f)?; + + Ok(()) + } + + /// Renders the content layer of boxes at the given y-level of the tree. + /// This includes: + /// - Node names and extra information + /// - Vertical borders (│) for boxes + /// - Vertical connections between nodes + fn render_box_content( + &mut self, + root: &RenderTree, + y: usize, + ) -> Result<(), fmt::Error> { + let mut extra_info: Vec> = vec![vec![]; root.width]; + let mut extra_height = 0; + + for (x, extra_info_item) in extra_info.iter_mut().enumerate().take(root.width) { + if let Some(node) = root.get_node(x, y) { + Self::split_up_extra_info( + &node.extra_text, + extra_info_item, + Self::MAX_EXTRA_LINES, + ); + if extra_info_item.len() > extra_height { + extra_height = extra_info_item.len(); + } + } + } + + let halfway_point = (extra_height + 1) / 2; + + // Render the actual node. + for render_y in 0..=extra_height { + for (x, _) in root.nodes.iter().enumerate().take(root.width) { + if x * Self::NODE_RENDER_WIDTH >= Self::MAXIMUM_RENDER_WIDTH { + break; + } + + let mut has_adjacent_nodes = false; + for i in 0..(root.width - x) { + has_adjacent_nodes = has_adjacent_nodes || root.has_node(x + i, y); + } + + if let Some(node) = root.get_node(x, y) { + write!(self.f, "{}", Self::VERTICAL)?; + + // Rigure out what to render. + let mut render_text = String::new(); + if render_y == 0 { + render_text = node.name.clone(); + } else if render_y <= extra_info[x].len() { + render_text = extra_info[x][render_y - 1].clone(); + } + + render_text = Self::adjust_text_for_rendering( + &render_text, + Self::NODE_RENDER_WIDTH - 2, + ); + write!(self.f, "{}", render_text)?; + + if render_y == halfway_point && node.child_positions.len() > 1 { + write!(self.f, "{}", Self::LMIDDLE)?; + } else { + write!(self.f, "{}", Self::VERTICAL)?; + } + } else if render_y == halfway_point { + let has_child_to_the_right = + Self::should_render_whitespace(root, x, y); + if root.has_node(x, y + 1) { + // Node right below this one. + write!( + self.f, + "{}", + Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2) + )?; + if has_child_to_the_right { + write!(self.f, "{}", Self::TMIDDLE)?; + // Have another child to the right, Keep rendering the line. + write!( + self.f, + "{}", + Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2) + )?; + } else { + write!(self.f, "{}", Self::RTCORNER)?; + if has_adjacent_nodes { + // Only a child below this one: fill the reset with spaces. + write!( + self.f, + "{}", + " ".repeat(Self::NODE_RENDER_WIDTH / 2) + )?; + } + } + } else if has_child_to_the_right { + // Child to the right, but no child right below this one: render a full + // line. + write!( + self.f, + "{}", + Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH) + )?; + } else if has_adjacent_nodes { + // Empty spot: render spaces. + write!(self.f, "{}", " ".repeat(Self::NODE_RENDER_WIDTH))?; + } + } else if render_y >= halfway_point { + if root.has_node(x, y + 1) { + // Have a node below this empty spot: render a vertical line. + write!( + self.f, + "{}{}", + " ".repeat(Self::NODE_RENDER_WIDTH / 2), + Self::VERTICAL + )?; + if has_adjacent_nodes + || Self::should_render_whitespace(root, x, y) + { + write!( + self.f, + "{}", + " ".repeat(Self::NODE_RENDER_WIDTH / 2) + )?; + } + } else if has_adjacent_nodes + || Self::should_render_whitespace(root, x, y) + { + // Empty spot: render spaces. + write!(self.f, "{}", " ".repeat(Self::NODE_RENDER_WIDTH))?; + } + } else if has_adjacent_nodes { + // Empty spot: render spaces. + write!(self.f, "{}", " ".repeat(Self::NODE_RENDER_WIDTH))?; + } + } + writeln!(self.f)?; + } + + Ok(()) + } + + /// Renders the bottom layer of boxes at the given y-level of the tree. + /// This includes: + /// - Bottom corners (└─┘) for nodes + /// - Horizontal connections between nodes + /// - Vertical connections to child nodes + fn render_bottom_layer( + &mut self, + root: &RenderTree, + y: usize, + ) -> Result<(), fmt::Error> { + for x in 0..=root.width { + if x * Self::NODE_RENDER_WIDTH >= Self::MAXIMUM_RENDER_WIDTH { + break; + } + let mut has_adjacent_nodes = false; + for i in 0..(root.width - x) { + has_adjacent_nodes = has_adjacent_nodes || root.has_node(x + i, y); + } + if root.get_node(x, y).is_some() { + write!(self.f, "{}", Self::LDCORNER)?; + write!( + self.f, + "{}", + Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2 - 1) + )?; + if root.has_node(x, y + 1) { + // node below this one: connect to that one + write!(self.f, "{}", Self::TMIDDLE)?; + } else { + // no node below this one: end the box + write!(self.f, "{}", Self::HORIZONTAL)?; + } + write!( + self.f, + "{}", + Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2 - 1) + )?; + write!(self.f, "{}", Self::RDCORNER)?; + } else if root.has_node(x, y + 1) { + write!(self.f, "{}", &" ".repeat(Self::NODE_RENDER_WIDTH / 2))?; + write!(self.f, "{}", Self::VERTICAL)?; + if has_adjacent_nodes || Self::should_render_whitespace(root, x, y) { + write!(self.f, "{}", &" ".repeat(Self::NODE_RENDER_WIDTH / 2))?; + } + } else if has_adjacent_nodes || Self::should_render_whitespace(root, x, y) { + write!(self.f, "{}", &" ".repeat(Self::NODE_RENDER_WIDTH))?; + } + } + writeln!(self.f)?; + + Ok(()) + } + + fn extra_info_separator() -> String { + "-".repeat(Self::NODE_RENDER_WIDTH - 9) + } + + fn remove_padding(s: &str) -> String { + s.trim().to_string() + } + + pub fn split_up_extra_info( + extra_info: &HashMap, + result: &mut Vec, + max_lines: usize, + ) { + if extra_info.is_empty() { + return; + } + + result.push(Self::extra_info_separator()); + + let mut requires_padding = false; + let mut was_inlined = false; + + // use BTreeMap for repeatable key order + let sorted_extra_info: BTreeMap<_, _> = extra_info.iter().collect(); + for (key, value) in sorted_extra_info { + let mut str = Self::remove_padding(value); + if str.is_empty() { + continue; + } + let mut is_inlined = false; + let available_width = Self::NODE_RENDER_WIDTH - 7; + let total_size = key.len() + str.len() + 2; + let is_multiline = str.contains('\n'); + if !is_multiline && total_size < available_width { + str = format!("{}: {}", key, str); + is_inlined = true; + } else { + str = format!("{}:\n{}", key, str); + } + if is_inlined && was_inlined { + requires_padding = false; + } + if requires_padding { + result.push(String::new()); + } + + let mut splits: Vec = str.split('\n').map(String::from).collect(); + if splits.len() > max_lines { + let mut truncated_splits = Vec::new(); + for split in splits.iter().take(max_lines / 2) { + truncated_splits.push(split.clone()); + } + truncated_splits.push("...".to_string()); + for split in splits.iter().skip(splits.len() - max_lines / 2) { + truncated_splits.push(split.clone()); + } + splits = truncated_splits; + } + for split in splits { + // TODO: check every line is less than MAX_LINE_RENDER_SIZE. + result.push(split); + } + requires_padding = true; + was_inlined = is_inlined; + } + } + + /// Adjusts text to fit within the specified width by: + /// 1. Truncating with ellipsis if too long + /// 2. Center-aligning within the available space if shorter + fn adjust_text_for_rendering(source: &str, max_render_width: usize) -> String { + let render_width = source.chars().count(); + if render_width > max_render_width { + let truncated = &source[..max_render_width - 3]; + format!("{}...", truncated) + } else { + let total_spaces = max_render_width - render_width; + let half_spaces = total_spaces / 2; + let extra_left_space = if total_spaces % 2 == 0 { 0 } else { 1 }; + format!( + "{}{}{}", + " ".repeat(half_spaces + extra_left_space), + source, + " ".repeat(half_spaces) + ) + } + } + + /// Determines if whitespace should be rendered at a given position. + /// This is important for: + /// 1. Maintaining proper spacing between sibling nodes + /// 2. Ensuring correct alignment of connections between parents and children + /// 3. Preserving the tree structure's visual clarity + fn should_render_whitespace(root: &RenderTree, x: usize, y: usize) -> bool { + let mut found_children = 0; + + for i in (0..=x).rev() { + let node = root.get_node(i, y); + if root.has_node(i, y + 1) { + found_children += 1; + } + if let Some(node) = node { + if node.child_positions.len() > 1 + && found_children < node.child_positions.len() + { + return true; + } + + return false; + } + } + + false + } +} + /// Trait for types which could have additional details when formatted in `Verbose` mode pub trait DisplayAs { /// Format according to `DisplayFormatType`, used when verbose representation looks diff --git a/datafusion/physical-plan/src/empty.rs b/datafusion/physical-plan/src/empty.rs index c4e738cb3ad1..3fdde39df6f1 100644 --- a/datafusion/physical-plan/src/empty.rs +++ b/datafusion/physical-plan/src/empty.rs @@ -94,6 +94,10 @@ impl DisplayAs for EmptyExec { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "EmptyExec") } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/explain.rs b/datafusion/physical-plan/src/explain.rs index cb00958cec4c..bf488ccfae56 100644 --- a/datafusion/physical-plan/src/explain.rs +++ b/datafusion/physical-plan/src/explain.rs @@ -94,6 +94,10 @@ impl DisplayAs for ExplainExec { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "ExplainExec") } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/filter.rs b/datafusion/physical-plan/src/filter.rs index a66873bc6576..524da586f52e 100644 --- a/datafusion/physical-plan/src/filter.rs +++ b/datafusion/physical-plan/src/filter.rs @@ -329,6 +329,10 @@ impl DisplayAs for FilterExec { }; write!(f, "FilterExec: {}{}", self.predicate, display_projections) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/insert.rs b/datafusion/physical-plan/src/insert.rs index 63c9c9921248..ff65f6154b4a 100644 --- a/datafusion/physical-plan/src/insert.rs +++ b/datafusion/physical-plan/src/insert.rs @@ -153,6 +153,10 @@ impl DisplayAs for DataSinkExec { write!(f, "DataSinkExec: sink=")?; self.sink.fmt_as(t, f) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/joins/cross_join.rs b/datafusion/physical-plan/src/joins/cross_join.rs index ca4c26251de0..48dc7c9df302 100644 --- a/datafusion/physical-plan/src/joins/cross_join.rs +++ b/datafusion/physical-plan/src/joins/cross_join.rs @@ -240,6 +240,10 @@ impl DisplayAs for CrossJoinExec { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "CrossJoinExec") } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/joins/hash_join.rs b/datafusion/physical-plan/src/joins/hash_join.rs index b2e9b37655f1..f949c408cadc 100644 --- a/datafusion/physical-plan/src/joins/hash_join.rs +++ b/datafusion/physical-plan/src/joins/hash_join.rs @@ -668,6 +668,10 @@ impl DisplayAs for HashJoinExec { self.mode, self.join_type, on, display_filter, display_projections ) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/joins/nested_loop_join.rs b/datafusion/physical-plan/src/joins/nested_loop_join.rs index 64dfc8219b64..f680de6738e5 100644 --- a/datafusion/physical-plan/src/joins/nested_loop_join.rs +++ b/datafusion/physical-plan/src/joins/nested_loop_join.rs @@ -424,6 +424,10 @@ impl DisplayAs for NestedLoopJoinExec { self.join_type, display_filter, display_projections ) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/joins/sort_merge_join.rs b/datafusion/physical-plan/src/joins/sort_merge_join.rs index 9b008f5242c4..20c0d76a9cdb 100644 --- a/datafusion/physical-plan/src/joins/sort_merge_join.rs +++ b/datafusion/physical-plan/src/joins/sort_merge_join.rs @@ -369,6 +369,10 @@ impl DisplayAs for SortMergeJoinExec { )) ) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/joins/symmetric_hash_join.rs b/datafusion/physical-plan/src/joins/symmetric_hash_join.rs index 47af4ab9a765..03a68831438a 100644 --- a/datafusion/physical-plan/src/joins/symmetric_hash_join.rs +++ b/datafusion/physical-plan/src/joins/symmetric_hash_join.rs @@ -380,6 +380,10 @@ impl DisplayAs for SymmetricHashJoinExec { self.mode, self.join_type, on, display_filter ) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/lib.rs b/datafusion/physical-plan/src/lib.rs index 6ddaef1a2d28..2cd9e8b52ab8 100644 --- a/datafusion/physical-plan/src/lib.rs +++ b/datafusion/physical-plan/src/lib.rs @@ -51,6 +51,7 @@ pub use crate::topk::TopK; pub use crate::visitor::{accept, visit_execution_plan, ExecutionPlanVisitor}; mod ordering; +mod render_tree; mod topk; mod visitor; diff --git a/datafusion/physical-plan/src/limit.rs b/datafusion/physical-plan/src/limit.rs index f720294c7ad9..71a23d31ff66 100644 --- a/datafusion/physical-plan/src/limit.rs +++ b/datafusion/physical-plan/src/limit.rs @@ -108,6 +108,10 @@ impl DisplayAs for GlobalLimitExec { self.fetch.map_or("None".to_string(), |x| x.to_string()) ) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } @@ -261,6 +265,10 @@ impl DisplayAs for LocalLimitExec { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "LocalLimitExec: fetch={}", self.fetch) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/memory.rs b/datafusion/physical-plan/src/memory.rs index fd338cc91353..ae878fdab37b 100644 --- a/datafusion/physical-plan/src/memory.rs +++ b/datafusion/physical-plan/src/memory.rs @@ -192,6 +192,10 @@ impl DisplayAs for LazyMemoryExec { .join(", ") ) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/placeholder_row.rs b/datafusion/physical-plan/src/placeholder_row.rs index 6e31f601e152..72f2c13d2040 100644 --- a/datafusion/physical-plan/src/placeholder_row.rs +++ b/datafusion/physical-plan/src/placeholder_row.rs @@ -112,6 +112,11 @@ impl DisplayAs for PlaceholderRowExec { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "PlaceholderRowExec") } + + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/projection.rs b/datafusion/physical-plan/src/projection.rs index 08c4d24f4c7f..8ff3824eff4c 100644 --- a/datafusion/physical-plan/src/projection.rs +++ b/datafusion/physical-plan/src/projection.rs @@ -167,6 +167,10 @@ impl DisplayAs for ProjectionExec { write!(f, "ProjectionExec: expr=[{}]", expr.join(", ")) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/recursive_query.rs b/datafusion/physical-plan/src/recursive_query.rs index 05b78e4e1da4..7268735ea457 100644 --- a/datafusion/physical-plan/src/recursive_query.rs +++ b/datafusion/physical-plan/src/recursive_query.rs @@ -223,6 +223,10 @@ impl DisplayAs for RecursiveQueryExec { self.name, self.is_distinct ) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/render_tree.rs b/datafusion/physical-plan/src/render_tree.rs new file mode 100644 index 000000000000..5f8da76a89b4 --- /dev/null +++ b/datafusion/physical-plan/src/render_tree.rs @@ -0,0 +1,227 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// This code is based on the DuckDB’s implementation: +// + +//! This module provides functionality for rendering an execution plan as a tree structure. +//! It helps in visualizing how different operations in a query are connected and organized. + +use std::collections::HashMap; +use std::fmt::Formatter; +use std::sync::Arc; +use std::{cmp, fmt}; + +use crate::{DisplayFormatType, ExecutionPlan}; + +// TODO: It's never used. +/// Represents a 2D coordinate in the rendered tree. +/// Used to track positions of nodes and their connections. +#[allow(dead_code)] +pub struct Coordinate { + /// Horizontal position in the tree + pub x: usize, + /// Vertical position in the tree + pub y: usize, +} + +impl Coordinate { + pub fn new(x: usize, y: usize) -> Self { + Coordinate { x, y } + } +} + +/// Represents a node in the render tree, containing information about an execution plan operator +/// and its relationships to other operators. +pub struct RenderTreeNode { + /// The name of physical `ExecutionPlan`. + pub name: String, + /// Execution info collected from `ExecutionPlan`. + pub extra_text: HashMap, + /// Positions of child nodes in the rendered tree. + pub child_positions: Vec, +} + +impl RenderTreeNode { + pub fn new(name: String, extra_text: HashMap) -> Self { + RenderTreeNode { + name, + extra_text, + child_positions: vec![], + } + } + + fn add_child_position(&mut self, x: usize, y: usize) { + self.child_positions.push(Coordinate::new(x, y)); + } +} + +/// Main structure for rendering an execution plan as a tree. +/// Manages a 2D grid of nodes and their layout information. +pub struct RenderTree { + /// Storage for tree nodes in a flattened 2D grid + pub nodes: Vec>>, + /// Total width of the rendered tree + pub width: usize, + /// Total height of the rendered tree + pub height: usize, +} + +impl RenderTree { + /// Creates a new render tree from an execution plan. + pub fn create_tree(plan: &dyn ExecutionPlan) -> Self { + let (width, height) = get_tree_width_height(plan); + + let mut result = Self::new(width, height); + + create_tree_recursive(&mut result, plan, 0, 0); + + result + } + + fn new(width: usize, height: usize) -> Self { + RenderTree { + nodes: vec![None; (width + 1) * (height + 1)], + width, + height, + } + } + + pub fn get_node(&self, x: usize, y: usize) -> Option> { + if x >= self.width || y >= self.height { + return None; + } + + let pos = self.get_position(x, y); + self.nodes.get(pos).and_then(|node| node.clone()) + } + + pub fn set_node(&mut self, x: usize, y: usize, node: Arc) { + let pos = self.get_position(x, y); + if let Some(slot) = self.nodes.get_mut(pos) { + *slot = Some(node); + } + } + + pub fn has_node(&self, x: usize, y: usize) -> bool { + if x >= self.width || y >= self.height { + return false; + } + + let pos = self.get_position(x, y); + self.nodes.get(pos).is_some_and(|node| node.is_some()) + } + + fn get_position(&self, x: usize, y: usize) -> usize { + y * self.width + x + } +} + +/// Calculates the required dimensions of the tree. +/// This ensures we allocate enough space for the entire tree structure. +/// +/// # Arguments +/// * `plan` - The execution plan to measure +/// +/// # Returns +/// * A tuple of (width, height) representing the dimensions needed for the tree +fn get_tree_width_height(plan: &dyn ExecutionPlan) -> (usize, usize) { + let children = plan.children(); + + // Leaf nodes take up 1x1 space + if children.is_empty() { + return (1, 1); + } + + let mut width = 0; + let mut height = 0; + + for child in children { + let (child_width, child_height) = get_tree_width_height(child.as_ref()); + width += child_width; + height = cmp::max(height, child_height); + } + + height += 1; + + (width, height) +} + +fn fmt_display(plan: &dyn ExecutionPlan) -> impl fmt::Display + '_ { + struct Wrapper<'a> { + plan: &'a dyn ExecutionPlan, + } + + impl fmt::Display for Wrapper<'_> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.plan.fmt_as(DisplayFormatType::TreeRender, f)?; + Ok(()) + } + } + + Wrapper { plan } +} + +/// Recursively builds the render tree structure. +/// Traverses the execution plan and creates corresponding render nodes while +/// maintaining proper positioning and parent-child relationships. +/// +/// # Arguments +/// * `result` - The render tree being constructed +/// * `plan` - Current execution plan node being processed +/// * `x` - Horizontal position in the tree +/// * `y` - Vertical position in the tree +/// +/// # Returns +/// * The width of the subtree rooted at the current node +fn create_tree_recursive( + result: &mut RenderTree, + plan: &dyn ExecutionPlan, + x: usize, + y: usize, +) -> usize { + let display_info = fmt_display(plan).to_string(); + let mut extra_info = HashMap::new(); + + // Parse the key-value pairs from the formatted string. + for line in display_info.lines() { + if let Some((key, value)) = line.split_once('=') { + extra_info.insert(key.to_string(), value.to_string()); + } + } + + let mut node = RenderTreeNode::new(plan.name().to_string(), extra_info); + + let children = plan.children(); + + if children.is_empty() { + result.set_node(x, y, Arc::new(node)); + return 1; + } + + let mut width = 0; + for child in children { + let child_x = x + width; + let child_y = y + 1; + node.add_child_position(child_x, child_y); + width += create_tree_recursive(result, child.as_ref(), child_x, child_y); + } + + result.set_node(x, y, Arc::new(node)); + + width +} diff --git a/datafusion/physical-plan/src/repartition/mod.rs b/datafusion/physical-plan/src/repartition/mod.rs index 40e68cfcae83..e9a360c2ece3 100644 --- a/datafusion/physical-plan/src/repartition/mod.rs +++ b/datafusion/physical-plan/src/repartition/mod.rs @@ -506,6 +506,10 @@ impl DisplayAs for RepartitionExec { } Ok(()) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/sorts/partial_sort.rs b/datafusion/physical-plan/src/sorts/partial_sort.rs index dc03c012d9be..bd0e6268de52 100644 --- a/datafusion/physical-plan/src/sorts/partial_sort.rs +++ b/datafusion/physical-plan/src/sorts/partial_sort.rs @@ -226,6 +226,10 @@ impl DisplayAs for PartialSortExec { None => write!(f, "PartialSortExec: expr=[{}], common_prefix_length=[{common_prefix_length}]", self.expr), } } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/sorts/sort.rs b/datafusion/physical-plan/src/sorts/sort.rs index 751496c70808..a6c5dbec74dc 100644 --- a/datafusion/physical-plan/src/sorts/sort.rs +++ b/datafusion/physical-plan/src/sorts/sort.rs @@ -997,6 +997,10 @@ impl DisplayAs for SortExec { None => write!(f, "SortExec: expr=[{}], preserve_partitioning=[{preserve_partitioning}]", self.expr), } } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } @@ -1216,6 +1220,10 @@ mod tests { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "UnboundableExec",).unwrap() } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "").unwrap() + } } Ok(()) } diff --git a/datafusion/physical-plan/src/sorts/sort_preserving_merge.rs b/datafusion/physical-plan/src/sorts/sort_preserving_merge.rs index 454a06855175..00fa78ce5229 100644 --- a/datafusion/physical-plan/src/sorts/sort_preserving_merge.rs +++ b/datafusion/physical-plan/src/sorts/sort_preserving_merge.rs @@ -185,6 +185,10 @@ impl DisplayAs for SortPreservingMergeExec { Ok(()) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } @@ -1383,6 +1387,10 @@ mod tests { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "CongestedExec",).unwrap() } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "").unwrap() + } } Ok(()) } diff --git a/datafusion/physical-plan/src/streaming.rs b/datafusion/physical-plan/src/streaming.rs index 8bdfca2a8907..8104e8acf1f6 100644 --- a/datafusion/physical-plan/src/streaming.rs +++ b/datafusion/physical-plan/src/streaming.rs @@ -208,6 +208,10 @@ impl DisplayAs for StreamingTableExec { Ok(()) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/test.rs b/datafusion/physical-plan/src/test.rs index 7d0e3778452f..a2dc1d778436 100644 --- a/datafusion/physical-plan/src/test.rs +++ b/datafusion/physical-plan/src/test.rs @@ -105,10 +105,10 @@ impl DisplayAs for TestMemoryExec { .map_or(String::new(), |limit| format!(", fetch={}", limit)); if self.show_sizes { write!( - f, - "partitions={}, partition_sizes={partition_sizes:?}{limit}{output_ordering}{constraints}", - partition_sizes.len(), - ) + f, + "partitions={}, partition_sizes={partition_sizes:?}{limit}{output_ordering}{constraints}", + partition_sizes.len(), + ) } else { write!( f, @@ -117,6 +117,10 @@ impl DisplayAs for TestMemoryExec { ) } } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/test/exec.rs b/datafusion/physical-plan/src/test/exec.rs index f0149faa8433..d0a0d25779cc 100644 --- a/datafusion/physical-plan/src/test/exec.rs +++ b/datafusion/physical-plan/src/test/exec.rs @@ -175,6 +175,10 @@ impl DisplayAs for MockExec { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "MockExec") } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } @@ -337,6 +341,10 @@ impl DisplayAs for BarrierExec { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "BarrierExec") } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } @@ -449,6 +457,10 @@ impl DisplayAs for ErrorExec { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "ErrorExec") } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } @@ -535,6 +547,10 @@ impl DisplayAs for StatisticsExec { self.stats.num_rows, ) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } @@ -630,6 +646,10 @@ impl DisplayAs for BlockingExec { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "BlockingExec",) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } @@ -772,6 +792,10 @@ impl DisplayAs for PanicExec { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "PanicExec",) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/union.rs b/datafusion/physical-plan/src/union.rs index 68d1803b7133..791370917523 100644 --- a/datafusion/physical-plan/src/union.rs +++ b/datafusion/physical-plan/src/union.rs @@ -157,6 +157,10 @@ impl DisplayAs for UnionExec { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "UnionExec") } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } @@ -387,6 +391,10 @@ impl DisplayAs for InterleaveExec { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "InterleaveExec") } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/unnest.rs b/datafusion/physical-plan/src/unnest.rs index 430391de5922..4e70d2dc4ee5 100644 --- a/datafusion/physical-plan/src/unnest.rs +++ b/datafusion/physical-plan/src/unnest.rs @@ -137,6 +137,9 @@ impl DisplayAs for UnnestExec { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "UnnestExec") } + DisplayFormatType::TreeRender => { + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/values.rs b/datafusion/physical-plan/src/values.rs index b90c50510cb0..6cb64bcb5d86 100644 --- a/datafusion/physical-plan/src/values.rs +++ b/datafusion/physical-plan/src/values.rs @@ -162,6 +162,10 @@ impl DisplayAs for ValuesExec { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "ValuesExec") } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/physical-plan/src/windows/bounded_window_agg_exec.rs b/datafusion/physical-plan/src/windows/bounded_window_agg_exec.rs index 0d9c58b3bf49..7e37156a133e 100644 --- a/datafusion/physical-plan/src/windows/bounded_window_agg_exec.rs +++ b/datafusion/physical-plan/src/windows/bounded_window_agg_exec.rs @@ -252,6 +252,10 @@ impl DisplayAs for BoundedWindowAggExec { let mode = &self.input_order_mode; write!(f, "wdw=[{}], mode=[{:?}]", g.join(", "), mode)?; } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "")?; + } } Ok(()) } diff --git a/datafusion/physical-plan/src/windows/window_agg_exec.rs b/datafusion/physical-plan/src/windows/window_agg_exec.rs index d31fd66ca1f1..cd171115b610 100644 --- a/datafusion/physical-plan/src/windows/window_agg_exec.rs +++ b/datafusion/physical-plan/src/windows/window_agg_exec.rs @@ -181,6 +181,10 @@ impl DisplayAs for WindowAggExec { .collect(); write!(f, "wdw=[{}]", g.join(", "))?; } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "")?; + } } Ok(()) } diff --git a/datafusion/physical-plan/src/work_table.rs b/datafusion/physical-plan/src/work_table.rs index d3d29bfad7ce..f082b05410dd 100644 --- a/datafusion/physical-plan/src/work_table.rs +++ b/datafusion/physical-plan/src/work_table.rs @@ -162,6 +162,10 @@ impl DisplayAs for WorkTableExec { DisplayFormatType::Default | DisplayFormatType::Verbose => { write!(f, "WorkTableExec: name={}", self.name) } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } } } } diff --git a/datafusion/sqllogictest/test_files/explain_tree.slt b/datafusion/sqllogictest/test_files/explain_tree.slt new file mode 100644 index 000000000000..5f0785782008 --- /dev/null +++ b/datafusion/sqllogictest/test_files/explain_tree.slt @@ -0,0 +1,198 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# Tests for tree explain + + + +statement ok +set datafusion.explain.format = "tree"; + +######## Setup Data Files ####### + +# table1: CSV +query I +COPY (VALUES (1, 'foo', 1, '2023-01-01'), (2, 'bar', 2, '2023-01-02'), (3, 'baz', 3, '2023-01-03')) +TO 'test_files/scratch/explain_tree/table1.csv'; +---- +3 + +statement ok +CREATE EXTERNAL TABLE table1 ( + int_col INT, + string_col TEXT, + bigint_col BIGINT, + date_col DATE +) +STORED AS CSV +LOCATION 'test_files/scratch/explain_tree/table1.csv'; + +# table2: Parquet +query I +COPY (SELECT * from table1) +TO 'test_files/scratch/explain_tree/table2.parquet' +---- +3 + +statement ok +CREATE EXTERNAL TABLE table2 +STORED AS PARQUET +LOCATION 'test_files/scratch/explain_tree/table2.parquet'; + + +# table3: Memoru +statement ok +CREATE TABLE table3 as select * from table1; + +######## Begin Queries ######## + +# Filter +query TT +explain SELECT int_col FROM table1 WHERE string_col != 'foo'; +---- +logical_plan +01)Projection: table1.int_col +02)--Filter: table1.string_col != Utf8("foo") +03)----TableScan: table1 projection=[int_col, string_col], partial_filters=[table1.string_col != Utf8("foo")] +physical_plan +01)┌───────────────────────────┐ +02)│ CoalesceBatchesExec │ +03)└─────────────┬─────────────┘ +04)┌─────────────┴─────────────┐ +05)│ FilterExec │ +06)└─────────────┬─────────────┘ +07)┌─────────────┴─────────────┐ +08)│ RepartitionExec │ +09)└─────────────┬─────────────┘ +10)┌─────────────┴─────────────┐ +11)│ DataSourceExec │ +12)└───────────────────────────┘ + +# Aggregate +query TT +explain SELECT string_col, SUM(bigint_col) FROM table1 GROUP BY string_col; +---- +logical_plan +01)Aggregate: groupBy=[[table1.string_col]], aggr=[[sum(table1.bigint_col)]] +02)--TableScan: table1 projection=[string_col, bigint_col] +physical_plan +01)┌───────────────────────────┐ +02)│ AggregateExec │ +03)└─────────────┬─────────────┘ +04)┌─────────────┴─────────────┐ +05)│ CoalesceBatchesExec │ +06)└─────────────┬─────────────┘ +07)┌─────────────┴─────────────┐ +08)│ RepartitionExec │ +09)└─────────────┬─────────────┘ +10)┌─────────────┴─────────────┐ +11)│ AggregateExec │ +12)└─────────────┬─────────────┘ +13)┌─────────────┴─────────────┐ +14)│ RepartitionExec │ +15)└─────────────┬─────────────┘ +16)┌─────────────┴─────────────┐ +17)│ DataSourceExec │ +18)└───────────────────────────┘ + +# 2 Joins +query TT +explain SELECT table1.string_col, table2.date_col FROM table1 JOIN table2 ON table1.int_col = table2.int_col; +---- +logical_plan +01)Projection: table1.string_col, table2.date_col +02)--Inner Join: table1.int_col = table2.int_col +03)----TableScan: table1 projection=[int_col, string_col] +04)----TableScan: table2 projection=[int_col, date_col] +physical_plan +01)┌───────────────────────────┐ +02)│ CoalesceBatchesExec │ +03)└─────────────┬─────────────┘ +04)┌─────────────┴─────────────┐ +05)│ HashJoinExec ├──────────────┐ +06)└─────────────┬─────────────┘ │ +07)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +08)│ CoalesceBatchesExec ││ CoalesceBatchesExec │ +09)└─────────────┬─────────────┘└─────────────┬─────────────┘ +10)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +11)│ RepartitionExec ││ RepartitionExec │ +12)└─────────────┬─────────────┘└─────────────┬─────────────┘ +13)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +14)│ RepartitionExec ││ RepartitionExec │ +15)└─────────────┬─────────────┘└─────────────┬─────────────┘ +16)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +17)│ DataSourceExec ││ DataSourceExec │ +18)└───────────────────────────┘└───────────────────────────┘ + +# 3 Joins +query TT +explain SELECT + table1.string_col, + table2.date_col, + table3.date_col +FROM + table1 JOIN table2 ON table1.int_col = table2.int_col + JOIN table3 ON table2.int_col = table3.int_col; +---- +logical_plan +01)Projection: table1.string_col, table2.date_col, table3.date_col +02)--Inner Join: table2.int_col = table3.int_col +03)----Projection: table1.string_col, table2.int_col, table2.date_col +04)------Inner Join: table1.int_col = table2.int_col +05)--------TableScan: table1 projection=[int_col, string_col] +06)--------TableScan: table2 projection=[int_col, date_col] +07)----TableScan: table3 projection=[int_col, date_col] +physical_plan +01)┌───────────────────────────┐ +02)│ CoalesceBatchesExec │ +03)└─────────────┬─────────────┘ +04)┌─────────────┴─────────────┐ +05)│ HashJoinExec ├───────────────────────────────────────────┐ +06)└─────────────┬─────────────┘ │ +07)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +08)│ CoalesceBatchesExec │ │ CoalesceBatchesExec │ +09)└─────────────┬─────────────┘ └─────────────┬─────────────┘ +10)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +11)│ HashJoinExec ├──────────────┐ │ RepartitionExec │ +12)└─────────────┬─────────────┘ │ └─────────────┬─────────────┘ +13)┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +14)│ CoalesceBatchesExec ││ CoalesceBatchesExec ││ DataSourceExec │ +15)│ ││ ││ -------------------- │ +16)│ ││ ││ partition_sizes: [1] │ +17)│ ││ ││ partitions: 1 │ +18)└─────────────┬─────────────┘└─────────────┬─────────────┘└───────────────────────────┘ +19)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +20)│ RepartitionExec ││ RepartitionExec │ +21)└─────────────┬─────────────┘└─────────────┬─────────────┘ +22)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +23)│ RepartitionExec ││ RepartitionExec │ +24)└─────────────┬─────────────┘└─────────────┬─────────────┘ +25)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +26)│ DataSourceExec ││ DataSourceExec │ +27)└───────────────────────────┘└───────────────────────────┘ + + + +# cleanup +statement ok +drop table table1; + +statement ok +drop table table2; + +statement ok +drop table table3; diff --git a/datafusion/sqllogictest/test_files/information_schema.slt b/datafusion/sqllogictest/test_files/information_schema.slt index 5c2f730a3586..454055b53930 100644 --- a/datafusion/sqllogictest/test_files/information_schema.slt +++ b/datafusion/sqllogictest/test_files/information_schema.slt @@ -232,6 +232,7 @@ datafusion.execution.split_file_groups_by_statistics false datafusion.execution.target_partitions 7 datafusion.execution.time_zone +00:00 datafusion.execution.use_row_number_estimates_to_optimize_partitioning false +datafusion.explain.format indent datafusion.explain.logical_plan_only false datafusion.explain.physical_plan_only false datafusion.explain.show_schema false @@ -329,6 +330,7 @@ datafusion.execution.split_file_groups_by_statistics false Attempt to eliminate datafusion.execution.target_partitions 7 Number of partitions for query execution. Increasing partitions can increase concurrency. Defaults to the number of CPU cores on the system datafusion.execution.time_zone +00:00 The default time zone Some functions, e.g. `EXTRACT(HOUR from SOME_TIME)`, shift the underlying datetime according to this time zone, and then extract the hour datafusion.execution.use_row_number_estimates_to_optimize_partitioning false Should DataFusion use row number estimates at the input to decide whether increasing parallelism is beneficial or not. By default, only exact row numbers (not estimates) are used for this decision. Setting this flag to `true` will likely produce better plans. if the source of statistics is accurate. We plan to make this the default in the future. +datafusion.explain.format indent Display format of explain. Default is "indent". When set to "tree", it will print the plan in a tree-rendered format. datafusion.explain.logical_plan_only false When set to true, the explain statement will only print logical plans datafusion.explain.physical_plan_only false When set to true, the explain statement will only print physical plans datafusion.explain.show_schema false When set to true, the explain statement will print schema information diff --git a/docs/source/user-guide/configs.md b/docs/source/user-guide/configs.md index f29fbb674592..635eb2b0a67f 100644 --- a/docs/source/user-guide/configs.md +++ b/docs/source/user-guide/configs.md @@ -122,6 +122,7 @@ Environment variables are read during `SessionConfig` initialisation so they mus | datafusion.explain.show_statistics | false | When set to true, the explain statement will print operator statistics for physical plans | | datafusion.explain.show_sizes | true | When set to true, the explain statement will print the partition sizes | | datafusion.explain.show_schema | false | When set to true, the explain statement will print schema information | +| datafusion.explain.format | indent | Display format of explain. Default is "indent". When set to "tree", it will print the plan in a tree-rendered format. | | datafusion.sql_parser.parse_float_as_decimal | false | When set to true, SQL parser will parse float as decimal type | | datafusion.sql_parser.enable_ident_normalization | true | When set to true, SQL parser will normalize ident (convert ident to lowercase when not quoted) | | datafusion.sql_parser.enable_options_value_normalization | false | When set to true, SQL parser will normalize options value (convert value to lowercase). Note that this option is ignored and will be removed in the future. All case-insensitive values are normalized automatically. |