diff --git a/datafusion/common/src/display/mod.rs b/datafusion/common/src/display/mod.rs index a6a97b243f06a..7ded419a14f49 100644 --- a/datafusion/common/src/display/mod.rs +++ b/datafusion/common/src/display/mod.rs @@ -19,7 +19,9 @@ mod graphviz; pub mod human_readable; +mod tree; pub use graphviz::*; +pub use tree::*; use std::{ fmt::{self, Display, Formatter}, @@ -135,3 +137,83 @@ pub trait ToStringifiedPlan { /// Create a stringified plan with the specified type fn to_stringified(&self, plan_type: PlanType) -> StringifiedPlan; } + +pub trait DisplayAs { + /// Format according to `DisplayFormatType`, used when verbose representation looks + /// different from the default one + /// + /// Should not include a newline + fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> fmt::Result; +} + +impl DisplayAs for &dyn DisplayAs { + fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> fmt::Result { + (*self).fmt_as(t, f) + } +} + +impl DisplayAs for Arc { + fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> fmt::Result { + self.as_ref().fmt_as(t, f) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum DisplayFormatType { + /// Default, compact format. Example: `FilterExec: c12 < 10.0` + /// + /// This format is designed to provide a detailed textual description + /// of all parts of the plan. + Default, + /// Verbose, showing all available details. + /// + /// This form is even more detailed than [`Self::Default`] + Verbose, + /// TreeRender, displayed in the `tree` explain type. + /// + /// This format is inspired by DuckDB's explain plans. The information + /// presented should be "user friendly", and contain only the most relevant + /// information for understanding a plan. It should NOT contain the same level + /// of detail information as the [`Self::Default`] format. + /// + /// In this mode, each line has one of two formats: + /// + /// 1. A string without a `=`, which is printed in its own line + /// + /// 2. A string with a `=` that is treated as a `key=value pair`. Everything + /// before the first `=` is treated as the key, and everything after the + /// first `=` is treated as the value. If key is __main_content__ it will be omitted + /// + /// For example, if the output of `TreeRender` is this: + /// ```text + /// Parquet + /// partition_sizes=[1] + /// ``` + /// + /// It is rendered in the center of a box in the following way: + /// + /// ```text + /// ┌───────────────────────────┐ + /// │ DataSourceExec │ + /// │ -------------------- │ + /// │ partition_sizes: [1] │ + /// │ Parquet │ + /// └───────────────────────────┘ + /// ``` + /// For logical plan + /// ```text + /// __main_content__=table1.string_col != Utf8View("foo") + /// ``` + /// + /// Is rendered as: + /// + /// ```text + /// ┌───────────────────────────┐ + /// │ Filter │ + /// │ -------------------- │ + /// │ table1.string_col != │ + /// │ Utf8View("foo") │ + /// └───────────────────────────┘ + /// ``` + TreeRender, +} diff --git a/datafusion/common/src/display/tree.rs b/datafusion/common/src/display/tree.rs new file mode 100644 index 0000000000000..bc3426acb3dfe --- /dev/null +++ b/datafusion/common/src/display/tree.rs @@ -0,0 +1,698 @@ +// 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. + +//! Types for plan display + +use indexmap::IndexMap; + +use crate::display::{DisplayAs, DisplayFormatType}; +use crate::tree_node::{TreeNode, TreeNodeRecursion}; +use std::collections::BTreeMap; +use std::fmt::Formatter; +use std::sync::Arc; +use std::{cmp, fmt}; + +/// This module implements a tree-like art renderer for arbitrary struct that implement TreeNode trait, +/// 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). +impl RenderTree { + // 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 + + 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 + + /// 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(&self, f: &mut Formatter, y: usize) -> Result<(), fmt::Error> { + for x in 0..self.width { + if self.has_node(x, y) { + write!(f, "{}", Self::LTCORNER)?; + write!( + f, + "{}", + Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2 - 1) + )?; + if y == 0 { + // top level node: no node above this one + write!(f, "{}", Self::HORIZONTAL)?; + } else { + // render connection to node above this one + write!(f, "{}", Self::DMIDDLE)?; + } + write!( + f, + "{}", + Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2 - 1) + )?; + write!(f, "{}", Self::RTCORNER)?; + } else { + let mut has_adjacent_nodes = false; + for i in 0..(self.width - x) { + has_adjacent_nodes = has_adjacent_nodes || self.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!(f, "{}", &" ".repeat(Self::NODE_RENDER_WIDTH))?; + } + } + writeln!(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(&self, f: &mut Formatter, y: usize) -> Result<(), fmt::Error> { + let mut extra_info: Vec> = vec![vec![]; self.width]; + let mut extra_height = 0; + + for (x, extra_info_item) in extra_info.iter_mut().enumerate().take(self.width) { + if let Some(node) = self.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.div_ceil(2); + + // Render the actual node. + for render_y in 0..=extra_height { + for (x, _) in self.nodes.iter().enumerate().take(self.width) { + if x * Self::NODE_RENDER_WIDTH >= self.maximum_render_width { + break; + } + + let mut has_adjacent_nodes = false; + for i in 0..(self.width - x) { + has_adjacent_nodes = has_adjacent_nodes || self.has_node(x + i, y); + } + + if let Some(node) = self.get_node(x, y) { + write!(f, "{}", Self::VERTICAL)?; + + // Rigure out what to render. + let mut render_text = String::new(); + if render_y == 0 { + render_text.clone_from(&node.name); + } else if render_y <= extra_info[x].len() { + render_text.clone_from(&extra_info[x][render_y - 1]); + } + + render_text = Self::adjust_text_for_rendering( + &render_text, + Self::NODE_RENDER_WIDTH - 2, + ); + write!(f, "{render_text}")?; + + if render_y == halfway_point && node.child_positions.len() > 1 { + write!(f, "{}", Self::LMIDDLE)?; + } else { + write!(f, "{}", Self::VERTICAL)?; + } + } else if render_y == halfway_point { + let has_child_to_the_right = self.should_render_whitespace(x, y); + if self.has_node(x, y + 1) { + // Node right below this one. + write!( + f, + "{}", + Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2) + )?; + if has_child_to_the_right { + write!(f, "{}", Self::TMIDDLE)?; + // Have another child to the right, Keep rendering the line. + write!( + f, + "{}", + Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2) + )?; + } else { + write!(f, "{}", Self::RTCORNER)?; + if has_adjacent_nodes { + // Only a child below this one: fill the reset with spaces. + write!(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!( + f, + "{}", + Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH) + )?; + } else if has_adjacent_nodes { + // Empty spot: render spaces. + write!(f, "{}", " ".repeat(Self::NODE_RENDER_WIDTH))?; + } + } else if render_y >= halfway_point { + if self.has_node(x, y + 1) { + // Have a node below this empty spot: render a vertical line. + write!( + f, + "{}{}", + " ".repeat(Self::NODE_RENDER_WIDTH / 2), + Self::VERTICAL + )?; + if has_adjacent_nodes || self.should_render_whitespace(x, y) { + write!(f, "{}", " ".repeat(Self::NODE_RENDER_WIDTH / 2))?; + } + } else if has_adjacent_nodes || self.should_render_whitespace(x, y) { + // Empty spot: render spaces. + write!(f, "{}", " ".repeat(Self::NODE_RENDER_WIDTH))?; + } + } else if has_adjacent_nodes { + // Empty spot: render spaces. + write!(f, "{}", " ".repeat(Self::NODE_RENDER_WIDTH))?; + } + } + writeln!(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(&self, f: &mut Formatter, y: usize) -> Result<(), fmt::Error> { + for x in 0..=self.width { + if x * Self::NODE_RENDER_WIDTH >= self.maximum_render_width { + break; + } + let mut has_adjacent_nodes = false; + for i in 0..(self.width - x) { + has_adjacent_nodes = has_adjacent_nodes || self.has_node(x + i, y); + } + if self.get_node(x, y).is_some() { + write!(f, "{}", Self::LDCORNER)?; + write!( + f, + "{}", + Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2 - 1) + )?; + if self.has_node(x, y + 1) { + // node below this one: connect to that one + write!(f, "{}", Self::TMIDDLE)?; + } else { + // no node below this one: end the box + write!(f, "{}", Self::HORIZONTAL)?; + } + write!( + f, + "{}", + Self::HORIZONTAL.repeat(Self::NODE_RENDER_WIDTH / 2 - 1) + )?; + write!(f, "{}", Self::RDCORNER)?; + } else if self.has_node(x, y + 1) { + write!(f, "{}", &" ".repeat(Self::NODE_RENDER_WIDTH / 2))?; + write!(f, "{}", Self::VERTICAL)?; + if has_adjacent_nodes || self.should_render_whitespace(x, y) { + write!(f, "{}", &" ".repeat(Self::NODE_RENDER_WIDTH / 2))?; + } + } else if has_adjacent_nodes || self.should_render_whitespace(x, y) { + write!(f, "{}", &" ".repeat(Self::NODE_RENDER_WIDTH))?; + } + } + writeln!(f)?; + + Ok(()) + } + + fn extra_info_separator() -> String { + "-".repeat(Self::NODE_RENDER_WIDTH - 9) + } + + fn remove_padding(s: &str) -> String { + s.trim().to_string() + } + + fn split_up_extra_info( + extra_info: &IndexMap, + 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); + 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'); + let hide_key = key == "__main_content__"; + + if str.is_empty() { + str = if hide_key { str } else { key.to_string() }; + } else if !is_multiline && total_size < available_width { + str = if hide_key { + str + } else { + format!("{key}: {str}") + }; + is_inlined = true; + } else { + str = if hide_key { + str + } else { + format!("{key}:\n{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 { + Self::split_string_buffer(&split, result); + } + if result.len() > max_lines { + result.truncate(max_lines); + result.push("...".to_string()); + } + + 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.is_multiple_of(2) { 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(&self, x: usize, y: usize) -> bool { + let mut found_children = 0; + + for i in (0..=x).rev() { + let node = self.get_node(i, y); + if self.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 + } + + fn split_string_buffer(source: &str, result: &mut Vec) { + let mut character_pos = 0; + let mut start_pos = 0; + let mut render_width = 0; + let mut last_possible_split = 0; + + let chars: Vec = source.chars().collect(); + + while character_pos < chars.len() { + // Treating each char as width 1 for simplification + let char_width = 1; + + // Does the next character make us exceed the line length? + if render_width + char_width > Self::NODE_RENDER_WIDTH - 2 { + if start_pos + 8 > last_possible_split { + // The last character we can split on is one of the first 8 characters of the line + // to not create very small lines we instead split on the current character + last_possible_split = character_pos; + } + + result.push(source[start_pos..last_possible_split].to_string()); + render_width = character_pos - last_possible_split; + start_pos = last_possible_split; + character_pos = last_possible_split; + } + + // check if we can split on this character + if Self::can_split_on_this_char(chars[character_pos]) { + last_possible_split = character_pos; + } + + character_pos += 1; + render_width += char_width; + } + + if source.len() > start_pos { + // append the remainder of the input + result.push(source[start_pos..].to_string()); + } + } + + fn can_split_on_this_char(c: char) -> bool { + (!c.is_ascii_digit() && !c.is_ascii_uppercase() && !c.is_ascii_lowercase()) + && c != '_' + } +} + +/// Trait to connect TreeNode and DisplayAs, which is used to render +/// tree of any TreeNode implementation +pub trait FormattedTreeNode: TreeNode + DisplayAs { + fn node_name(&self) -> String { + "".to_string() + } +} + +/// Represents a 2D coordinate in the rendered tree. +/// Used to track positions of nodes and their connections. +pub struct Coordinate { + /// Horizontal position in the tree + #[expect(dead_code)] + pub x: usize, + /// Vertical position in the tree + #[expect(dead_code)] + pub y: usize, +} + +impl Coordinate { + 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`. + name: String, + /// Additional info being displayed along with the nameA. + extra_text: IndexMap, + /// Positions of child nodes in the rendered tree. + child_positions: Vec, +} + +impl RenderTreeNode { + pub fn new(name: String, extra_text: IndexMap) -> Self { + RenderTreeNode { + name, + extra_text, + child_positions: vec![], + } + } + + pub 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 + nodes: Vec>>, + /// Total width of the rendered tree + width: usize, + /// Total height of the rendered tree + height: usize, + /// Maximum total width of the rendered tree + maximum_render_width: usize, +} + +impl fmt::Display for RenderTree { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + for y in 0..self.height { + // Start by rendering the top layer. + self.render_top_layer(f, y)?; + // Now we render the content of the boxes + self.render_box_content(f, y)?; + // Render the bottom layer of each of the boxes + self.render_bottom_layer(f, y)?; + } + + Ok(()) + } +} + +impl RenderTree { + /// 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 + fn create_tree(node: &T, max_width: usize) -> Self { + let (width, height) = get_tree_width_height(node); + + let mut result = Self::new(max_width, width, height); + + create_tree_recursive(&mut result, node, 0, 0); + + result + } + + pub fn new(max_width: usize, width: usize, height: usize) -> Self { + RenderTree { + nodes: vec![None; (width + 1) * (height + 1)], + width, + height, + maximum_render_width: max_width, + } + } + + 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); + } + } + + 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 + } + + pub fn fmt_display(node: &T) -> impl fmt::Display + '_ { + struct Wrapper<'a, T: ?Sized + DisplayAs> { + node: &'a T, + } + + impl fmt::Display for Wrapper<'_, T> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.node.fmt_as(DisplayFormatType::TreeRender, f) + } + } + Wrapper { node } + } +} + +/// 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 +pub fn get_tree_width_height(plan: &T) -> (usize, usize) { + let is_empty_ref = &mut true; + let width = &mut 0; + let height = &mut 0; + plan.apply_children(|c| { + *is_empty_ref = false; + let (child_width, child_height) = get_tree_width_height(c); + *width += child_width; + *height = cmp::max(*height, child_height); + Ok(TreeNodeRecursion::Continue) + }) + .unwrap(); + if *is_empty_ref { + return (1, 1); + } + + *height += 1; + + (*width, *height) +} + +/// 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 +pub fn create_tree_recursive( + result: &mut RenderTree, + plan: &T, + x: usize, + y: usize, +) -> usize { + let display_info = RenderTree::fmt_display(plan).to_string(); + let mut extra_info = IndexMap::new(); + + // Parse the key-value pairs from the formatted string. + // See DisplayFormatType::TreeRender for details + for line in display_info.lines() { + if let Some((key, value)) = line.split_once('=') { + extra_info.insert(key.to_string(), value.to_string()); + } else { + extra_info.insert(line.to_string(), "".to_string()); + } + } + + let mut rendered_node = RenderTreeNode::new(plan.node_name(), extra_info); + let is_empty_ref = &mut true; + let width_ref = &mut 0; + + TreeNode::apply_children(plan, |n| { + *is_empty_ref = false; + let child_x = x + *width_ref; + let child_y = y + 1; + rendered_node.add_child_position(child_x, child_y); + *width_ref += create_tree_recursive(result, n, child_x, child_y); + Ok(TreeNodeRecursion::Continue) + }) + .unwrap(); + + if *is_empty_ref { + result.set_node(x, y, Arc::new(rendered_node)); + return 1; + } + + result.set_node(x, y, Arc::new(rendered_node)); + + *width_ref +} + +// render the whole tree +pub fn render_tree<'a, T: FormattedTreeNode>( + n: &'a T, + max_width: usize, +) -> impl fmt::Display + use<'a, T> { + RenderTree::create_tree(n, max_width) +} diff --git a/datafusion/core/src/physical_planner.rs b/datafusion/core/src/physical_planner.rs index 3b2c7a78e898e..5113bb4929602 100644 --- a/datafusion/core/src/physical_planner.rs +++ b/datafusion/core/src/physical_planner.rs @@ -2639,6 +2639,10 @@ impl DefaultPhysicalPlanner { session_state, |_plan, _optimizer| {}, )?; + stringified_plans.push(StringifiedPlan::new( + FinalLogicalPlan, + format!("{}", e.plan.display_tree(config.tree_maximum_render_width)), + )); stringified_plans.push(StringifiedPlan::new( FinalPhysicalPlan, @@ -3252,6 +3256,7 @@ mod tests { use datafusion_functions_aggregate::count::count_all; use datafusion_functions_aggregate::expr_fn::sum; use datafusion_physical_expr::EquivalenceProperties; + use datafusion_physical_plan::execution_plan::{Boundedness, EmissionType}; fn make_session_state() -> SessionState { diff --git a/datafusion/expr/src/logical_plan/plan.rs b/datafusion/expr/src/logical_plan/plan.rs index c572b202f03ce..2e10ebe4dcf1b 100644 --- a/datafusion/expr/src/logical_plan/plan.rs +++ b/datafusion/expr/src/logical_plan/plan.rs @@ -52,6 +52,7 @@ use crate::{ use arrow::datatypes::{DataType, Field, FieldRef, Schema, SchemaRef}; use datafusion_common::cse::{NormalizeEq, Normalizeable}; +use datafusion_common::display::render_tree; use datafusion_common::format::ExplainFormat; use datafusion_common::metadata::check_metadata_with_storage_equal; use datafusion_common::tree_node::{ @@ -1524,6 +1525,58 @@ impl LogicalPlan { _ => Ok(vec![]), } } + + /// Sample + /// ```text + /// ┌───────────────────────────┐ + /// │ │ + /// │ -------------------- │ + /// │ Projection: outer_table.a,│ + /// │ outer_table.b, │ + /// │ outer_table.c │ + /// └─────────────┬─────────────┘ + /// ┌─────────────┴─────────────┐ + /// │ │ + /// │ -------------------- │ + /// │ Filter: __exists_sq_1 │ + /// │ .output AND │ + /// │ __exists_sq_2 │ + /// │ .output │ + /// └─────────────┬─────────────┘ + /// ┌─────────────┴─────────────┐ + /// │ │ + /// │ -------------------- │ + /// │ Projection: outer_table.a,│ + /// │ outer_table.b, │ + /// │ outer_table.c, │ + /// │ __exists_sq_1.output, │ + /// │ mark AS __exists_sq_2 │ + /// │ .output │ + /// └─────────────┬─────────────┘ + /// ┌─────────────┴─────────────┐ + /// │ │ + /// │ -------------------- │ + /// │ LeftMark Join │ + /// │ (ComparisonJo │ + /// │ in): Filter: outer_table ├────────────────────────────────────────────────────────────────────────┐ + /// │ .c IS NOT DISTINCT FROM │ │ + /// │ delim_scan_2 │ │ + /// │ .outer_table_c │ │ + /// └─────────────┬─────────────┘ │ + /// ┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ + /// │ │ │ │ + /// │ -------------------- │ │ -------------------- │ + /// │ Projection: outer_table.a,│ │Filter: inner_table_lv1.c :│ + /// │ outer_table.b, │ │ outer_table_dscan_2 │ + /// │ outer_table.c, │ │ .outer_table_c │ + /// │ mark AS __exists_sq_1 │ │ │ + /// │ .output │ │ │ + /// └─────────────┬─────────────┘ └─────────────┬─────────────┘ + /// + /// ``` + pub fn display_tree(&self, max_width: usize) -> impl Display + '_ { + render_tree(self, max_width) + } } impl LogicalPlan { diff --git a/datafusion/expr/src/logical_plan/tree_node.rs b/datafusion/expr/src/logical_plan/tree_node.rs index ef9382a57209a..8328de3c0dae4 100644 --- a/datafusion/expr/src/logical_plan/tree_node.rs +++ b/datafusion/expr/src/logical_plan/tree_node.rs @@ -37,6 +37,8 @@ //! * [`LogicalPlan::with_new_exprs`]: Create a new plan with different expressions //! * [`LogicalPlan::expressions`]: Return a copy of the plan's expressions +use std::fmt::Formatter; + use crate::{ Aggregate, Analyze, CreateMemoryTable, CreateView, DdlStatement, Distinct, DistinctOn, DmlStatement, Execute, Explain, Expr, Extension, Filter, Join, Limit, @@ -44,6 +46,7 @@ use crate::{ Statement, Subquery, SubqueryAlias, TableScan, Union, Unnest, UserDefinedLogicalNode, Values, Window, dml::CopyTo, }; +use datafusion_common::display::{DisplayAs, DisplayFormatType, FormattedTreeNode}; use datafusion_common::tree_node::TreeNodeRefContainer; use crate::expr::{Exists, InSubquery, SetComparison}; @@ -53,6 +56,32 @@ use datafusion_common::tree_node::{ }; use datafusion_common::{Result, internal_err}; +impl FormattedTreeNode for LogicalPlan { + fn node_name(&self) -> String { + self.display() + .to_string() + .split(":") + .next() + .unwrap_or("") + .to_string() + } +} + +impl DisplayAs for LogicalPlan { + fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> std::fmt::Result { + if let DisplayFormatType::TreeRender = t { + let full = self.display().to_string(); + let second = full + .split_once(':') + .map(|(_, second)| second.trim_start()) + .unwrap_or(""); + return write!(f, "__main_content__={second}"); + } + // DisplayAs only used to render tree for now + unimplemented!() + } +} + impl TreeNode for LogicalPlan { fn apply_children<'n, F: FnMut(&'n Self) -> Result>( &'n self, diff --git a/datafusion/physical-plan/src/display.rs b/datafusion/physical-plan/src/display.rs index 756a68b1a958d..a5f197a09a51e 100644 --- a/datafusion/physical-plan/src/display.rs +++ b/datafusion/physical-plan/src/display.rs @@ -18,67 +18,32 @@ //! Implementation of physical plan display. See //! [`crate::displayable`] for examples of how to format -use std::collections::{BTreeMap, HashMap}; use std::fmt; use std::fmt::Formatter; use arrow::datatypes::SchemaRef; use datafusion_common::display::{GraphvizBuilder, PlanType, StringifiedPlan}; + +#[deprecated( + since = "54.0.0", + note = "please use datafusion_common::display::DisplayAs" +)] +pub use datafusion_common::display::DisplayAs; + +#[deprecated( + since = "54.0.0", + note = "please use datafusion_common::display::DisplayFormatType" +)] +pub use datafusion_common::display::DisplayFormatType; use datafusion_expr::display_schema; use datafusion_physical_expr::LexOrdering; use crate::metrics::{MetricCategory, MetricType}; -use crate::render_tree::RenderTree; +use crate::render_tree::{self}; use super::{ExecutionPlan, ExecutionPlanVisitor, accept}; -/// Options for controlling how each [`ExecutionPlan`] should format itself -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum DisplayFormatType { - /// Default, compact format. Example: `FilterExec: c12 < 10.0` - /// - /// This format is designed to provide a detailed textual description - /// of all parts of the plan. - Default, - /// Verbose, showing all available details. - /// - /// This form is even more detailed than [`Self::Default`] - Verbose, - /// TreeRender, displayed in the `tree` explain type. - /// - /// This format is inspired by DuckDB's explain plans. The information - /// presented should be "user friendly", and contain only the most relevant - /// information for understanding a plan. It should NOT contain the same level - /// of detail information as the [`Self::Default`] format. - /// - /// In this mode, each line has one of two formats: - /// - /// 1. A string without a `=`, which is printed in its own line - /// - /// 2. A string with a `=` that is treated as a `key=value pair`. Everything - /// before the first `=` is treated as the key, and everything after the - /// first `=` is treated as the value. - /// - /// For example, if the output of `TreeRender` is this: - /// ```text - /// Parquet - /// partition_sizes=[1] - /// ``` - /// - /// It is rendered in the center of a box in the following way: - /// - /// ```text - /// ┌───────────────────────────┐ - /// │ DataSourceExec │ - /// │ -------------------- │ - /// │ partition_sizes: [1] │ - /// │ Parquet │ - /// └───────────────────────────┘ - /// ``` - TreeRender, -} - /// Wraps an `ExecutionPlan` with various methods for formatting /// /// @@ -330,23 +295,7 @@ impl<'a> DisplayableExecutionPlan<'a> { /// /// See [`DisplayFormatType::TreeRender`] for more details. pub fn tree_render(&self) -> impl fmt::Display + 'a { - struct Wrapper<'a> { - plan: &'a dyn ExecutionPlan, - maximum_render_width: usize, - } - impl fmt::Display for Wrapper<'_> { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let mut visitor = TreeRenderVisitor { - f, - maximum_render_width: self.maximum_render_width, - }; - visitor.visit(self.plan) - } - } - Wrapper { - plan: self.inner, - maximum_render_width: self.tree_maximum_render_width, - } + render_tree::render_tree(self.inner, self.tree_maximum_render_width) } /// Return a single-line summary of the root of the plan @@ -611,503 +560,6 @@ 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>, - /// Maximum total width of the rendered tree - maximum_render_width: usize, -} - -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 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 self.maximum_render_width > 0 - && x * Self::NODE_RENDER_WIDTH >= self.maximum_render_width - { - break; - } - - 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.div_ceil(2); - - // Render the actual node. - for render_y in 0..=extra_height { - for (x, _) in root.nodes.iter().enumerate().take(root.width) { - if self.maximum_render_width > 0 - && 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)?; - - // Figure out what to render. - let mut render_text = if render_y == 0 { - node.name.clone() - } else if render_y <= extra_info[x].len() { - extra_info[x][render_y - 1].clone() - } else { - String::new() - }; - - 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 self.maximum_render_width > 0 - && 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); - 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 str.is_empty() { - str = key.to_string(); - } else if !is_multiline && total_size < available_width { - str = format!("{key}: {str}"); - is_inlined = true; - } else { - str = format!("{key}:\n{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 { - Self::split_string_buffer(&split, result); - } - if result.len() > max_lines { - result.truncate(max_lines); - result.push("...".to_string()); - } - - 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.is_multiple_of(2) { 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 - } - - fn split_string_buffer(source: &str, result: &mut Vec) { - let mut character_pos = 0; - let mut start_pos = 0; - let mut render_width = 0; - let mut last_possible_split = 0; - - let chars: Vec = source.chars().collect(); - - while character_pos < chars.len() { - // Treating each char as width 1 for simplification - let char_width = 1; - - // Does the next character make us exceed the line length? - if render_width + char_width > Self::NODE_RENDER_WIDTH - 2 { - if start_pos + 8 > last_possible_split { - // The last character we can split on is one of the first 8 characters of the line - // to not create very small lines we instead split on the current character - last_possible_split = character_pos; - } - - result.push(source[start_pos..last_possible_split].to_string()); - render_width = character_pos - last_possible_split; - start_pos = last_possible_split; - character_pos = last_possible_split; - } - - // check if we can split on this character - if Self::can_split_on_this_char(chars[character_pos]) { - last_possible_split = character_pos; - } - - character_pos += 1; - render_width += char_width; - } - - if source.len() > start_pos { - // append the remainder of the input - result.push(source[start_pos..].to_string()); - } - } - - fn can_split_on_this_char(c: char) -> bool { - (!c.is_ascii_digit() && !c.is_ascii_uppercase() && !c.is_ascii_lowercase()) - && c != '_' - } -} - -/// 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 - /// different from the default one - /// - /// Should not include a newline - fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> fmt::Result; -} - /// A new type wrapper to display `T` implementing`DisplayAs` using the `Default` mode pub struct DefaultDisplay(pub T); diff --git a/datafusion/physical-plan/src/execution_plan.rs b/datafusion/physical-plan/src/execution_plan.rs index 1a67ea0ded11b..21458cad18a26 100644 --- a/datafusion/physical-plan/src/execution_plan.rs +++ b/datafusion/physical-plan/src/execution_plan.rs @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -pub use crate::display::{DefaultDisplay, DisplayAs, DisplayFormatType, VerboseDisplay}; +pub use crate::display::{DefaultDisplay, VerboseDisplay}; use crate::filter_pushdown::{ ChildPushdownResult, FilterDescription, FilterPushdownPhase, FilterPushdownPropagation, @@ -24,6 +24,17 @@ pub use crate::metrics::Metric; pub use crate::ordering::InputOrderMode; use crate::sort_pushdown::SortOrderPushdownResult; pub use crate::stream::EmptyRecordBatchStream; +#[deprecated( + since = "54.0.0", + note = "please use datafusion_common::display::DisplayAs" +)] +pub use datafusion_common::display::DisplayAs; + +#[deprecated( + since = "54.0.0", + note = "please use datafusion_common::display::DisplayFormatType" +)] +pub use datafusion_common::display::DisplayFormatType; use arrow_schema::Schema; pub use datafusion_common::hash_utils; diff --git a/datafusion/physical-plan/src/lib.rs b/datafusion/physical-plan/src/lib.rs index 3005e975424b4..3cffb99bcc298 100644 --- a/datafusion/physical-plan/src/lib.rs +++ b/datafusion/physical-plan/src/lib.rs @@ -29,18 +29,7 @@ //! //! Entrypoint of this crate is trait [ExecutionPlan]. -pub use datafusion_common::hash_utils; -pub use datafusion_common::utils::project_schema; -pub use datafusion_common::{ColumnStatistics, Statistics, internal_err}; -pub use datafusion_execution::{RecordBatchStream, SendableRecordBatchStream}; -pub use datafusion_expr::{Accumulator, ColumnarValue}; -use datafusion_physical_expr::PhysicalSortExpr; -pub use datafusion_physical_expr::window::WindowExpr; -pub use datafusion_physical_expr::{ - Distribution, Partitioning, PhysicalExpr, expressions, -}; - -pub use crate::display::{DefaultDisplay, DisplayAs, DisplayFormatType, VerboseDisplay}; +pub use crate::display::{DefaultDisplay, VerboseDisplay}; pub use crate::execution_plan::{ ExecutionPlan, ExecutionPlanProperties, PlanProperties, collect, collect_partitioned, displayable, execute_input_stream, execute_stream, execute_stream_partitioned, @@ -53,6 +42,17 @@ pub use crate::stream::EmptyRecordBatchStream; pub use crate::topk::TopK; pub use crate::visitor::{ExecutionPlanVisitor, accept, visit_execution_plan}; pub use crate::work_table::WorkTable; +pub use datafusion_common::display::{DisplayAs, DisplayFormatType}; +pub use datafusion_common::hash_utils; +pub use datafusion_common::utils::project_schema; +pub use datafusion_common::{ColumnStatistics, Statistics, internal_err}; +pub use datafusion_execution::{RecordBatchStream, SendableRecordBatchStream}; +pub use datafusion_expr::{Accumulator, ColumnarValue}; +use datafusion_physical_expr::PhysicalSortExpr; +pub use datafusion_physical_expr::window::WindowExpr; +pub use datafusion_physical_expr::{ + Distribution, Partitioning, PhysicalExpr, expressions, +}; pub use spill::spill_manager::SpillManager; mod ordering; diff --git a/datafusion/physical-plan/src/render_tree.rs b/datafusion/physical-plan/src/render_tree.rs index 40e2763698093..88b68526a52bb 100644 --- a/datafusion/physical-plan/src/render_tree.rs +++ b/datafusion/physical-plan/src/render_tree.rs @@ -21,115 +21,22 @@ //! 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 core::fmt; +use std::{cmp, fmt::Formatter, sync::Arc}; -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. -pub struct Coordinate { - /// Horizontal position in the tree - #[expect(dead_code)] - pub x: usize, - /// Vertical position in the tree - #[expect(dead_code)] - 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); +pub use datafusion_common::display::{RenderTree, RenderTreeNode}; +use indexmap::IndexMap; - 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()) - } +use crate::{DisplayFormatType, ExecutionPlan}; - fn get_position(&self, x: usize, y: usize) -> usize { - y * self.width + x - } +pub(crate) fn render_tree<'a>( + plan: &dyn ExecutionPlan, + max_width: usize, +) -> impl fmt::Display + 'a { + let (width, height) = get_tree_width_height(plan); + let mut result = RenderTree::new(max_width, width, height); + create_tree_recursive(&mut result, plan, 0, 0); + result } /// Calculates the required dimensions of the tree. @@ -196,7 +103,7 @@ fn create_tree_recursive( y: usize, ) -> usize { let display_info = fmt_display(plan).to_string(); - let mut extra_info = HashMap::new(); + let mut extra_info = IndexMap::new(); // Parse the key-value pairs from the formatted string. // See DisplayFormatType::TreeRender for details diff --git a/datafusion/sqllogictest/test_files/explain_tree.slt b/datafusion/sqllogictest/test_files/explain_tree.slt index a7d3bead0e8e5..3b30debe2f4c3 100644 --- a/datafusion/sqllogictest/test_files/explain_tree.slt +++ b/datafusion/sqllogictest/test_files/explain_tree.slt @@ -164,6 +164,27 @@ UNION ALL SELECT * FROM table1 query TT explain SELECT int_col FROM table1 WHERE string_col != 'foo'; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ table1.int_col │ +05)└─────────────┬─────────────┘ +06)┌─────────────┴─────────────┐ +07)│ Filter │ +08)│ -------------------- │ +09)│ table1.string_col != │ +10)│ Utf8View("foo") │ +11)└─────────────┬─────────────┘ +12)┌─────────────┴─────────────┐ +13)│ TableScan │ +14)│ -------------------- │ +15)│ table1 projection=[int_col│ +16)│ , string_col], │ +17)│ partial_filter │ +18)│ s=[table1.string_col != │ +19)│ Utf8View("foo")] │ +20)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ FilterExec │ @@ -191,6 +212,22 @@ physical_plan query TT explain SELECT string_col, SUM(bigint_col) FROM table1 GROUP BY string_col; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Aggregate │ +03)│ -------------------- │ +04)│ groupBy=[[table1 │ +05)│ .string_col]], │ +06)│ aggr=[[sum(table1 │ +07)│ .bigint_col)]] │ +08)└─────────────┬─────────────┘ +09)┌─────────────┴─────────────┐ +10)│ TableScan │ +11)│ -------------------- │ +12)│ table1 projection= │ +13)│ [string_col, │ +14)│ bigint_col] │ +15)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ AggregateExec │ @@ -242,6 +279,18 @@ physical_plan query TT explain SELECT int_col FROM table1 LIMIT 3,2; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Limit │ +03)│ -------------------- │ +04)│ skip=3, fetch=2 │ +05)└─────────────┬─────────────┘ +06)┌─────────────┴─────────────┐ +07)│ TableScan │ +08)│ -------------------- │ +09)│ table1 projection=[int_col│ +10)│ ], fetch=5 │ +11)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ GlobalLimitExec │ @@ -259,6 +308,20 @@ physical_plan query TT explain SELECT * FROM limit_table LIMIT 10; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Limit │ +03)│ -------------------- │ +04)│ skip=0, fetch=10 │ +05)└─────────────┬─────────────┘ +06)┌─────────────┴─────────────┐ +07)│ TableScan │ +08)│ -------------------- │ +09)│ limit_table projection= │ +10)│ [int_col, string_col, │ +11)│ bigint_col, date_col], │ +12)│ fetch=10 │ +13)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ CoalescePartitionsExec │ @@ -282,6 +345,28 @@ ON (table1.int_col = table2.int_col) AND (((table1.int_col + table2.int_col) % 2) = 0) ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ table1.string_col, table2 │ +05)│ .date_col │ +06)└─────────────┬─────────────┘ +07)┌─────────────┴─────────────┐ +08)│ Inner Join │ +09)│ -------------------- │ +10)│ table1.int_col = table2 │ +11)│ .int_col Filter: CAST ├──────────────┐ +12)│ (table1.int_col + table2 │ │ +13)│ .int_col AS Int64) % │ │ +14)│ Int64(2) = Int64(0) │ │ +15)└─────────────┬─────────────┘ │ +16)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +17)│ TableScan ││ TableScan │ +18)│ -------------------- ││ -------------------- │ +19)│ table1 projection=[int_col││ table2 projection=[int_col│ +20)│ , string_col] ││ , date_col] │ +21)└───────────────────────────┘└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ HashJoinExec │ @@ -309,6 +394,7 @@ physical_plan 24)-----------------------------│ format: csv │ 25)-----------------------------└───────────────────────────┘ + # 3 Joins query TT explain SELECT @@ -319,6 +405,38 @@ FROM table1 JOIN table2 ON table1.int_col = table2.int_col JOIN table3 ON table2.int_col = table3.int_col; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ table1.string_col, table2 │ +05)│ .date_col, table3 │ +06)│ .date_col │ +07)└─────────────┬─────────────┘ +08)┌─────────────┴─────────────┐ +09)│ Inner Join │ +10)│ -------------------- │ +11)│ table2.int_col = table3 ├───────────────────────────────────────────┐ +12)│ .int_col │ │ +13)└─────────────┬─────────────┘ │ +14)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +15)│ Projection │ │ TableScan │ +16)│ -------------------- │ │ -------------------- │ +17)│ table1.string_col, table2 │ │ table3 projection=[int_col│ +18)│ .int_col, table2.date_col │ │ , date_col] │ +19)└─────────────┬─────────────┘ └───────────────────────────┘ +20)┌─────────────┴─────────────┐ +21)│ Inner Join │ +22)│ -------------------- │ +23)│ table1.int_col = table2 ├──────────────┐ +24)│ .int_col │ │ +25)└─────────────┬─────────────┘ │ +26)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +27)│ TableScan ││ TableScan │ +28)│ -------------------- ││ -------------------- │ +29)│ table1 projection=[int_col││ table2 projection=[int_col│ +30)│ , string_col] ││ , date_col] │ +31)└───────────────────────────┘└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ HashJoinExec │ @@ -355,6 +473,38 @@ explain SELECT int_col FROM table1 WHERE string_col != 'foo' AND string_col != 'bar' AND string_col != 'a really long string constant' ; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ table1.int_col │ +05)└─────────────┬─────────────┘ +06)┌─────────────┴─────────────┐ +07)│ Filter │ +08)│ -------------------- │ +09)│ table1.string_col != │ +10)│ Utf8View("foo") │ +11)│ AND table1.string_col │ +12)│ != Utf8View("bar") AND │ +13)│ table1.string_col != │ +14)│ Utf8View("a really │ +15)│ long string constant │ +16)│ ") │ +17)└─────────────┬─────────────┘ +18)┌─────────────┴─────────────┐ +19)│ TableScan │ +20)│ -------------------- │ +21)│ table1 projection=[int_col│ +22)│ , string_col], │ +23)│ partial_filter │ +24)│ s=[table1.string_col != │ +25)│ Utf8View("foo"), │ +26)│ table1.string_col ! │ +27)│ = Utf8View("bar"), table1 │ +28)│ .string_col != Utf8View( │ +29)│ "a really long string │ +30)│ constant")] │ +31)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ FilterExec │ @@ -387,6 +537,80 @@ query TT explain SELECT int_col FROM table1 WHERE string_col != 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ table1.int_col │ +05)└─────────────┬─────────────┘ +06)┌─────────────┴─────────────┐ +07)│ Filter │ +08)│ -------------------- │ +09)│ table1.string_col != │ +10)│ Utf8View( │ +11)│ "aaaaaaaaaaaaaaaa │ +12)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +13)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +14)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +15)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +16)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +17)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +18)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +19)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +20)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +21)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +22)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +23)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +24)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +25)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +26)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +27)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +28)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +29)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +30)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +31)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +32)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +33)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +34)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +35)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +36)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +37)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +38)│ ... │ +39)└─────────────┬─────────────┘ +40)┌─────────────┴─────────────┐ +41)│ TableScan │ +42)│ -------------------- │ +43)│ table1 projection=[int_col│ +44)│ , string_col], │ +45)│ partial_filter │ +46)│ s=[table1.string_col != │ +47)│ Utf8View( │ +48)│ "aaaaaaaaaaaaa │ +49)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +50)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +51)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +52)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +53)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +54)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +55)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +56)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +57)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +58)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +59)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +60)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +61)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +62)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +63)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +64)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +65)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +66)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +67)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +68)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +69)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +70)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +71)│aaaaaaaaaaaaaaaaaaaaaaaaaaa│ +72)│ ... │ +73)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ FilterExec │ @@ -443,6 +667,30 @@ query TT explain SELECT int_col FROM table1 WHERE string_col != 'aaaaaaaaaaaaa'; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ table1.int_col │ +05)└─────────────┬─────────────┘ +06)┌─────────────┴─────────────┐ +07)│ Filter │ +08)│ -------------------- │ +09)│ table1.string_col != │ +10)│ Utf8View( │ +11)│ "aaaaaaaaaaaaa") │ +12)└─────────────┬─────────────┘ +13)┌─────────────┴─────────────┐ +14)│ TableScan │ +15)│ -------------------- │ +16)│ table1 projection=[int_col│ +17)│ , string_col], │ +18)│ partial_filter │ +19)│ s=[table1.string_col != │ +20)│ Utf8View( │ +21)│ "aaaaaaaaaaaaa │ +22)│ ")] │ +23)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ FilterExec │ @@ -471,6 +719,31 @@ query TT explain SELECT int_col FROM table1 WHERE string_col != 'aaaaaaaaaaaaaaa'; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ table1.int_col │ +05)└─────────────┬─────────────┘ +06)┌─────────────┴─────────────┐ +07)│ Filter │ +08)│ -------------------- │ +09)│ table1.string_col != │ +10)│ Utf8View( │ +11)│ "aaaaaaaaaaaaaaa │ +12)│ ") │ +13)└─────────────┬─────────────┘ +14)┌─────────────┴─────────────┐ +15)│ TableScan │ +16)│ -------------------- │ +17)│ table1 projection=[int_col│ +18)│ , string_col], │ +19)│ partial_filter │ +20)│ s=[table1.string_col != │ +21)│ Utf8View( │ +22)│ "aaaaaaaaaaaaa │ +23)│ aa")] │ +24)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ FilterExec │ @@ -500,6 +773,27 @@ physical_plan query TT explain SELECT int_col FROM table1 WHERE string_col != 'foo'; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ table1.int_col │ +05)└─────────────┬─────────────┘ +06)┌─────────────┴─────────────┐ +07)│ Filter │ +08)│ -------------------- │ +09)│ table1.string_col != │ +10)│ Utf8View("foo") │ +11)└─────────────┬─────────────┘ +12)┌─────────────┴─────────────┐ +13)│ TableScan │ +14)│ -------------------- │ +15)│ table1 projection=[int_col│ +16)│ , string_col], │ +17)│ partial_filter │ +18)│ s=[table1.string_col != │ +19)│ Utf8View("foo")] │ +20)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ FilterExec │ @@ -528,6 +822,27 @@ physical_plan query TT explain SELECT int_col FROM table2 WHERE string_col != 'foo'; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ table2.int_col │ +05)└─────────────┬─────────────┘ +06)┌─────────────┴─────────────┐ +07)│ Filter │ +08)│ -------------------- │ +09)│ table2.string_col != │ +10)│ Utf8View("foo") │ +11)└─────────────┬─────────────┘ +12)┌─────────────┴─────────────┐ +13)│ TableScan │ +14)│ -------------------- │ +15)│ table2 projection=[int_col│ +16)│ , string_col], │ +17)│ partial_filter │ +18)│ s=[table2.string_col != │ +19)│ Utf8View("foo")] │ +20)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ FilterExec │ @@ -558,6 +873,24 @@ physical_plan query TT explain SELECT int_col FROM table3 WHERE string_col != 'foo'; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ table3.int_col │ +05)└─────────────┬─────────────┘ +06)┌─────────────┴─────────────┐ +07)│ Filter │ +08)│ -------------------- │ +09)│ table3.string_col != │ +10)│ Utf8View("foo") │ +11)└─────────────┬─────────────┘ +12)┌─────────────┴─────────────┐ +13)│ TableScan │ +14)│ -------------------- │ +15)│ table3 projection=[int_col│ +16)│ , string_col] │ +17)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ FilterExec │ @@ -577,6 +910,27 @@ physical_plan query TT explain SELECT int_col FROM table4 WHERE string_col != 'foo'; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ table4.int_col │ +05)└─────────────┬─────────────┘ +06)┌─────────────┴─────────────┐ +07)│ Filter │ +08)│ -------------------- │ +09)│ table4.string_col != Utf8(│ +10)│ "foo") │ +11)└─────────────┬─────────────┘ +12)┌─────────────┴─────────────┐ +13)│ TableScan │ +14)│ -------------------- │ +15)│ table4 projection=[int_col│ +16)│ , string_col], │ +17)│ partial_filter │ +18)│ s=[table4.string_col != │ +19)│ Utf8("foo")] │ +20)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ FilterExec │ @@ -604,6 +958,27 @@ physical_plan query TT explain SELECT int_col FROM table5 WHERE string_col != 'foo'; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ table5.int_col │ +05)└─────────────┬─────────────┘ +06)┌─────────────┴─────────────┐ +07)│ Filter │ +08)│ -------------------- │ +09)│ table5.string_col != │ +10)│ Utf8View("foo") │ +11)└─────────────┬─────────────┘ +12)┌─────────────┴─────────────┐ +13)│ TableScan │ +14)│ -------------------- │ +15)│ table5 projection=[int_col│ +16)│ , string_col], │ +17)│ partial_filter │ +18)│ s=[table5.string_col != │ +19)│ Utf8View("foo")] │ +20)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ FilterExec │ @@ -632,6 +1007,32 @@ physical_plan query TT explain select count(*) over() from table1; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ count(Int64(1)) ROWS │ +05)│ BETWEEN UNBOUNDED │ +06)│ PRECEDING AND UNBOUNDED │ +07)│ FOLLOWING AS count(*) │ +08)│ ROWS BETWEEN │ +09)│ UNBOUNDED │ +10)│ PRECEDING AND │ +11)│ UNBOUNDED FOLLOWING │ +12)└─────────────┬─────────────┘ +13)┌─────────────┴─────────────┐ +14)│ WindowAggr │ +15)│ -------------------- │ +16)│ windowExpr=[[count(Int64(1│ +17)│ )) ROWS BETWEEN UNBOUNDED │ +18)│ PRECEDING AND UNBOUNDED │ +19)│ FOLLOWING]] │ +20)└─────────────┬─────────────┘ +21)┌─────────────┴─────────────┐ +22)│ TableScan │ +23)│ -------------------- │ +24)│ table1 projection=[] │ +25)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ ProjectionExec │ @@ -667,6 +1068,45 @@ explain SELECT SUM(v1) OVER (ORDER BY v1 ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS rolling_sum FROM generate_series(1, 1000) AS t1(v1); ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ t1.v1, sum(t1.v1) ORDER BY│ +05)│ [t1.v1 ASC NULLS LAST] │ +06)│ ROWS BETWEEN 1 │ +07)│ PRECEDING AND │ +08)│ CURRENT ROW AS │ +09)│ rolling_sum │ +10)└─────────────┬─────────────┘ +11)┌─────────────┴─────────────┐ +12)│ WindowAggr │ +13)│ -------------------- │ +14)│ windowExpr=[[sum(t1.v1) │ +15)│ ORDER BY [t1.v1 ASC │ +16)│ NULLS LAST] ROWS │ +17)│ BETWEEN 1 │ +18)│ PRECEDING AND │ +19)│ CURRENT ROW]] │ +20)└─────────────┬─────────────┘ +21)┌─────────────┴─────────────┐ +22)│ SubqueryAlias │ +23)│ -------------------- │ +24)│ t1 │ +25)└─────────────┬─────────────┘ +26)┌─────────────┴─────────────┐ +27)│ Projection │ +28)│ -------------------- │ +29)│ generate_series().value AS│ +30)│ v1 │ +31)└─────────────┬─────────────┘ +32)┌─────────────┴─────────────┐ +33)│ TableScan │ +34)│ -------------------- │ +35)│ generate_series() │ +36)│ projection= │ +37)│ [value] │ +38)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ ProjectionExec │ @@ -715,6 +1155,40 @@ explain select row_number() over () from table1 ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ count(Int64(1)) ROWS │ +05)│ BETWEEN UNBOUNDED │ +06)│ PRECEDING AND UNBOUNDED │ +07)│ FOLLOWING AS count(*) │ +08)│ ROWS BETWEEN │ +09)│ UNBOUNDED │ +10)│ PRECEDING AND │ +11)│ UNBOUNDED FOLLOWING │ +12)│ , row_number() ROWS │ +13)│ BETWEEN UNBOUNDED │ +14)│ PRECEDING AND UNBOUNDED │ +15)│ FOLLOWING │ +16)└─────────────┬─────────────┘ +17)┌─────────────┴─────────────┐ +18)│ WindowAggr │ +19)│ -------------------- │ +20)│ windowExpr=[[count(Int64(1│ +21)│ )) ROWS BETWEEN UNBOUNDED │ +22)│ PRECEDING AND UNBOUNDED │ +23)│ FOLLOWING, row_number() │ +24)│ ROWS BETWEEN UNBOUNDED │ +25)│ PRECEDING AND │ +26)│ UNBOUNDED │ +27)│ FOLLOWING]] │ +28)└─────────────┬─────────────┘ +29)┌─────────────┴─────────────┐ +30)│ TableScan │ +31)│ -------------------- │ +32)│ table1 projection=[] │ +33)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ ProjectionExec │ @@ -757,6 +1231,20 @@ physical_plan query TT explain SELECT * FROM table1 ORDER BY string_col; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Sort │ +03)│ -------------------- │ +04)│ table1.string_col ASC │ +05)│ NULLS LAST │ +06)└─────────────┬─────────────┘ +07)┌─────────────┴─────────────┐ +08)│ TableScan │ +09)│ -------------------- │ +10)│ table1 projection=[int_col│ +11)│ , string_col, bigint_col, │ +12)│ date_col] │ +13)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ SortExec │ @@ -774,6 +1262,20 @@ physical_plan query TT explain SELECT * FROM table1 ORDER BY string_col LIMIT 1; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Sort │ +03)│ -------------------- │ +04)│ table1.string_col ASC │ +05)│ NULLS LAST, fetch=1 │ +06)└─────────────┬─────────────┘ +07)┌─────────────┴─────────────┐ +08)│ TableScan │ +09)│ -------------------- │ +10)│ table1 projection=[int_col│ +11)│ , string_col, bigint_col, │ +12)│ date_col] │ +13)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ SortExec(TopK) │ @@ -793,6 +1295,23 @@ physical_plan query TT explain SELECT int_col, bigint_col, int_col+bigint_col AS sum_col FROM table1; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ table1.int_col, table1 │ +05)│ .bigint_col, CAST │ +06)│ (table1.int_col AS │ +07)│ Int64) + table1 │ +08)│ .bigint_col AS │ +09)│ sum_col │ +10)└─────────────┬─────────────┘ +11)┌─────────────┴─────────────┐ +12)│ TableScan │ +13)│ -------------------- │ +14)│ table1 projection=[int_col│ +15)│ , bigint_col] │ +16)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ ProjectionExec │ @@ -828,6 +1347,46 @@ explain select row_number() over (ORDER BY int_col ASC) from table1 ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ rank() ORDER BY [table1 │ +05)│ .int_col DESC NULLS │ +06)│ FIRST] RANGE BETWEEN │ +07)│ UNBOUNDED PRECEDING AND │ +08)│ CURRENT ROW, row_number( │ +09)│ ) ORDER BY [table1 │ +10)│ .int_col ASC NULLS │ +11)│ LAST] RANGE BETWEEN │ +12)│ UNBOUNDED PRECEDING │ +13)│ AND CURRENT ROW │ +14)└─────────────┬─────────────┘ +15)┌─────────────┴─────────────┐ +16)│ WindowAggr │ +17)│ -------------------- │ +18)│ windowExpr=[[row_number() │ +19)│ ORDER BY [table1 │ +20)│ .int_col ASC NULLS │ +21)│ LAST] RANGE BETWEEN │ +22)│ UNBOUNDED PRECEDING │ +23)│ AND CURRENT ROW]] │ +24)└─────────────┬─────────────┘ +25)┌─────────────┴─────────────┐ +26)│ WindowAggr │ +27)│ -------------------- │ +28)│ windowExpr=[[rank() ORDER │ +29)│ BY [table1.int_col DESC │ +30)│ NULLS FIRST] RANGE │ +31)│ BETWEEN UNBOUNDED │ +32)│ PRECEDING AND CURRENT │ +33)│ ROW]] │ +34)└─────────────┬─────────────┘ +35)┌─────────────┴─────────────┐ +36)│ TableScan │ +37)│ -------------------- │ +38)│table1 projection=[int_col]│ +39)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ ProjectionExec │ @@ -902,6 +1461,23 @@ physical_plan query TT explain SELECT int_col, bigint_col, int_col+bigint_col AS sum_col FROM table2; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ table2.int_col, table2 │ +05)│ .bigint_col, CAST │ +06)│ (table2.int_col AS │ +07)│ Int64) + table2 │ +08)│ .bigint_col AS │ +09)│ sum_col │ +10)└─────────────┬─────────────┘ +11)┌─────────────┴─────────────┐ +12)│ TableScan │ +13)│ -------------------- │ +14)│ table2 projection=[int_col│ +15)│ , bigint_col] │ +16)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ DataSourceExec │ @@ -914,6 +1490,23 @@ physical_plan query TT explain SELECT int_col, bigint_col, int_col+bigint_col AS sum_col FROM table3; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ table3.int_col, table3 │ +05)│ .bigint_col, CAST │ +06)│ (table3.int_col AS │ +07)│ Int64) + table3 │ +08)│ .bigint_col AS │ +09)│ sum_col │ +10)└─────────────┬─────────────┘ +11)┌─────────────┴─────────────┐ +12)│ TableScan │ +13)│ -------------------- │ +14)│ table3 projection=[int_col│ +15)│ , bigint_col] │ +16)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ ProjectionExec │ @@ -939,6 +1532,22 @@ physical_plan query TT explain SELECT int_col, bigint_col, int_col+bigint_col AS sum_col FROM table4; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ table4.int_col, table4 │ +05)│ .bigint_col, table4 │ +06)│ .int_col + table4 │ +07)│ .bigint_col AS │ +08)│ sum_col │ +09)└─────────────┬─────────────┘ +10)┌─────────────┴─────────────┐ +11)│ TableScan │ +12)│ -------------------- │ +13)│ table4 projection=[int_col│ +14)│ , bigint_col] │ +15)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ ProjectionExec │ @@ -972,6 +1581,23 @@ physical_plan query TT explain SELECT int_col, bigint_col, int_col+bigint_col AS sum_col FROM table5; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│ table5.int_col, table5 │ +05)│ .bigint_col, CAST │ +06)│ (table5.int_col AS │ +07)│ Int64) + table5 │ +08)│ .bigint_col AS │ +09)│ sum_col │ +10)└─────────────┬─────────────┘ +11)┌─────────────┴─────────────┐ +12)│ TableScan │ +13)│ -------------------- │ +14)│ table5 projection=[int_col│ +15)│ , bigint_col] │ +16)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ ProjectionExec │ @@ -1007,6 +1633,24 @@ EXPLAIN SELECT * FROM annotated_data_infinite2 ORDER BY a, b, d; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Sort │ +03)│ -------------------- │ +04)│ annotated_data_infinite2.a│ +05)│ ASC NULLS LAST, │ +06)│ annotated_data_i │ +07)│ nfinite2.b ASC NULLS LAST,│ +08)│ annotated_data_infinite2 │ +09)│ .d ASC NULLS LAST │ +10)└─────────────┬─────────────┘ +11)┌─────────────┴─────────────┐ +12)│ TableScan │ +13)│ -------------------- │ +14)│ annotated_data_infinite2 │ +15)│ projection=[a0, a, b, │ +16)│ c, d] │ +17)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ PartialSortExec │ @@ -1028,6 +1672,25 @@ FROM annotated_data_infinite2 ORDER BY a, b, d LIMIT 50; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Sort │ +03)│ -------------------- │ +04)│ annotated_data_infinite2.a│ +05)│ ASC NULLS LAST, │ +06)│ annotated_data_i │ +07)│ nfinite2.b ASC NULLS LAST,│ +08)│ annotated_data_infinite2 │ +09)│ .d ASC NULLS LAST, fetch │ +10)│ =50 │ +11)└─────────────┬─────────────┘ +12)┌─────────────┴─────────────┐ +13)│ TableScan │ +14)│ -------------------- │ +15)│ annotated_data_infinite2 │ +16)│ projection=[a0, a, b, │ +17)│ c, d] │ +18)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ PartialSortExec │ @@ -1049,6 +1712,22 @@ physical_plan query TT explain select * from table1 inner join table2 on table1.int_col = table2.int_col and table1.string_col = table2.string_col; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Inner Join │ +03)│ -------------------- │ +04)│ table1.int_col = table2 │ +05)│ .int_col, table1 ├──────────────┐ +06)│ .string_col = │ │ +07)│ table2.string_col │ │ +08)└─────────────┬─────────────┘ │ +09)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +10)│ TableScan ││ TableScan │ +11)│ -------------------- ││ -------------------- │ +12)│ table1 projection=[int_col││ table2 projection=[int_col│ +13)│ , string_col, bigint_col, ││ , string_col, bigint_col, │ +14)│ date_col] ││ date_col] │ +15)└───────────────────────────┘└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ HashJoinExec │ @@ -1078,6 +1757,22 @@ physical_plan query TT explain select * from table1 left outer join table2 on table1.int_col = table2.int_col and table1.string_col = table2.string_col; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Left Join │ +03)│ -------------------- │ +04)│ table1.int_col = table2 │ +05)│ .int_col, table1 ├──────────────┐ +06)│ .string_col = │ │ +07)│ table2.string_col │ │ +08)└─────────────┬─────────────┘ │ +09)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +10)│ TableScan ││ TableScan │ +11)│ -------------------- ││ -------------------- │ +12)│ table1 projection=[int_col││ table2 projection=[int_col│ +13)│ , string_col, bigint_col, ││ , string_col, bigint_col, │ +14)│ date_col] ││ date_col] │ +15)└───────────────────────────┘└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ HashJoinExec │ @@ -1109,6 +1804,23 @@ physical_plan query TT explain select int_col from table1 where exists (select count(*) from table2); ---- +logical_plan +01)┌───────────────────────────┐ +02)│ LeftSemi Join │ +03)│ -------------------- ├──────────────┐ +04)└─────────────┬─────────────┘ │ +05)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +06)│ TableScan ││ SubqueryAlias │ +07)│ -------------------- ││ -------------------- │ +08)│ table1 projection=[int_col││ __correlated_sq_1 │ +09)│ ], partial_filters= ││ │ +10)│ [Boolean(true)] ││ │ +11)└───────────────────────────┘└─────────────┬─────────────┘ +12)-----------------------------┌─────────────┴─────────────┐ +13)-----------------------------│ EmptyRelation │ +14)-----------------------------│ -------------------- │ +15)-----------------------------│ rows=1 │ +16)-----------------------------└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ NestedLoopJoinExec │ @@ -1126,6 +1838,18 @@ physical_plan query TT explain select * from table1 cross join table2 ; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Cross Join │ +03)│ -------------------- ├──────────────┐ +04)└─────────────┬─────────────┘ │ +05)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +06)│ TableScan ││ TableScan │ +07)│ -------------------- ││ -------------------- │ +08)│ table1 projection=[int_col││ table2 projection=[int_col│ +09)│ , string_col, bigint_col, ││ , string_col, bigint_col, │ +10)│ date_col] ││ date_col] │ +11)└───────────────────────────┘└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ CrossJoinExec ├──────────────┐ @@ -1144,6 +1868,24 @@ set datafusion.optimizer.prefer_hash_join = false; query TT explain select * from hashjoin_datatype_table_t1 t1 join hashjoin_datatype_table_t2 t2 on t1.c1 = t2.c1 ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Inner Join │ +03)│ -------------------- ├──────────────┐ +04)│ t1.c1 = t2.c1 │ │ +05)└─────────────┬─────────────┘ │ +06)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +07)│ SubqueryAlias ││ SubqueryAlias │ +08)│ -------------------- ││ -------------------- │ +09)│ t1 ││ t2 │ +10)└─────────────┬─────────────┘└─────────────┬─────────────┘ +11)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +12)│ TableScan ││ TableScan │ +13)│ -------------------- ││ -------------------- │ +14)│ hashjoin_datatype_table_t1││ hashjoin_datatype_table_t2│ +15)│ projection=[c1, c2, c3, ││ projection=[c1, c2, c3, │ +16)│ c4] ││ c4] │ +17)└───────────────────────────┘└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ SortMergeJoinExec │ @@ -1212,6 +1954,33 @@ SELECT count(*) FROM ( SELECT distinct name FROM t2 ) GROUP BY name ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│count(Int64(1)) AS count(*)│ +05)└─────────────┬─────────────┘ +06)┌─────────────┴─────────────┐ +07)│ Aggregate │ +08)│ -------------------- │ +09)│ groupBy=[[name]], aggr=[ │ +10)│ [count(Int64(1))]] │ +11)└─────────────┬─────────────┘ +12)┌─────────────┴─────────────┐ +13)│ Union │ +14)│ -------------------- ├──────────────┐ +15)└─────────────┬─────────────┘ │ +16)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +17)│ Aggregate ││ Aggregate │ +18)│ -------------------- ││ -------------------- │ +19)│ groupBy=[[t1.name]], aggr=││ groupBy=[[t2.name]], aggr=│ +20)│ [[]] ││ [[]] │ +21)└─────────────┬─────────────┘└─────────────┬─────────────┘ +22)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +23)│ TableScan ││ TableScan │ +24)│ -------------------- ││ -------------------- │ +25)│ t1 projection=[name] ││ t2 projection=[name] │ +26)└───────────────────────────┘└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ ProjectionExec │ @@ -1269,6 +2038,22 @@ SELECT id, name FROM t1 UNION ALL SELECT id, name FROM t2; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Union │ +03)│ -------------------- ├──────────────┐ +04)└─────────────┬─────────────┘ │ +05)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +06)│ TableScan ││ Projection │ +07)│ -------------------- ││ -------------------- │ +08)│ t1 projection=[id, name] ││ CAST(t2.id AS Int32) AS id│ +09)│ ││ , t2.name │ +10)└───────────────────────────┘└─────────────┬─────────────┘ +11)-----------------------------┌─────────────┴─────────────┐ +12)-----------------------------│ TableScan │ +13)-----------------------------│ -------------------- │ +14)-----------------------------│ t2 projection=[id, name] │ +15)-----------------------------└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ UnionExec ├──────────────┐ @@ -1313,6 +2098,24 @@ explain SELECT * FROM data WHERE ticker = 'A' ORDER BY "date", "time"; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Sort │ +03)│ -------------------- │ +04)│ data.date ASC NULLS LAST, │ +05)│ data.time ASC NULLS LAST │ +06)└─────────────┬─────────────┘ +07)┌─────────────┴─────────────┐ +08)│ Filter │ +09)│ -------------------- │ +10)│data.ticker = Utf8View("A")│ +11)└─────────────┬─────────────┘ +12)┌─────────────┴─────────────┐ +13)│ TableScan │ +14)│ -------------------- │ +15)│ data projection=[date, │ +16)│ ticker, time] │ +17)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ SortPreservingMergeExec │ @@ -1348,6 +2151,25 @@ explain SELECT * FROM data WHERE ticker = 'A' AND CAST(time AS DATE) = date ORDER BY "time" ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Sort │ +03)│ -------------------- │ +04)│ data.time ASC NULLS LAST │ +05)└─────────────┬─────────────┘ +06)┌─────────────┴─────────────┐ +07)│ Filter │ +08)│ -------------------- │ +09)│ data.ticker = Utf8View("A"│ +10)│ ) AND CAST(data.time AS │ +11)│ Date32) = data.date │ +12)└─────────────┬─────────────┘ +13)┌─────────────┴─────────────┐ +14)│ TableScan │ +15)│ -------------------- │ +16)│ data projection=[date, │ +17)│ ticker, time] │ +18)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ SortPreservingMergeExec │ @@ -1383,6 +2205,25 @@ explain SELECT * FROM data WHERE ticker = 'A' AND CAST(time AS DATE) = date ORDER BY "date" ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Sort │ +03)│ -------------------- │ +04)│ data.date ASC NULLS LAST │ +05)└─────────────┬─────────────┘ +06)┌─────────────┴─────────────┐ +07)│ Filter │ +08)│ -------------------- │ +09)│ data.ticker = Utf8View("A"│ +10)│ ) AND CAST(data.time AS │ +11)│ Date32) = data.date │ +12)└─────────────┬─────────────┘ +13)┌─────────────┴─────────────┐ +14)│ TableScan │ +15)│ -------------------- │ +16)│ data projection=[date, │ +17)│ ticker, time] │ +18)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ SortPreservingMergeExec │ @@ -1418,6 +2259,25 @@ explain SELECT * FROM data WHERE ticker = 'A' AND CAST(time AS DATE) = date ORDER BY "ticker" ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Sort │ +03)│ -------------------- │ +04)│ data.ticker ASC NULLS LAST│ +05)└─────────────┬─────────────┘ +06)┌─────────────┴─────────────┐ +07)│ Filter │ +08)│ -------------------- │ +09)│ data.ticker = Utf8View("A"│ +10)│ ) AND CAST(data.time AS │ +11)│ Date32) = data.date │ +12)└─────────────┬─────────────┘ +13)┌─────────────┴─────────────┐ +14)│ TableScan │ +15)│ -------------------- │ +16)│ data projection=[date, │ +17)│ ticker, time] │ +18)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ CoalescePartitionsExec │ @@ -1452,6 +2312,26 @@ explain SELECT * FROM data WHERE ticker = 'A' AND CAST(time AS DATE) = date ORDER BY "time", "date"; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Sort │ +03)│ -------------------- │ +04)│ data.time ASC NULLS LAST, │ +05)│ data.date ASC NULLS LAST │ +06)└─────────────┬─────────────┘ +07)┌─────────────┴─────────────┐ +08)│ Filter │ +09)│ -------------------- │ +10)│ data.ticker = Utf8View("A"│ +11)│ ) AND CAST(data.time AS │ +12)│ Date32) = data.date │ +13)└─────────────┬─────────────┘ +14)┌─────────────┴─────────────┐ +15)│ TableScan │ +16)│ -------------------- │ +17)│ data projection=[date, │ +18)│ ticker, time] │ +19)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ SortPreservingMergeExec │ @@ -1491,6 +2371,26 @@ WHERE date = '2006-01-02' ORDER BY "ticker", "time" LIMIT 5; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Sort │ +03)│ -------------------- │ +04)│ data.ticker ASC NULLS LAST│ +05)│ , data.time ASC NULLS │ +06)│ LAST, fetch=5 │ +07)└─────────────┬─────────────┘ +08)┌─────────────┴─────────────┐ +09)│ Filter │ +10)│ -------------------- │ +11)│ data.date = Date32("2006 │ +12)│ -01-02") │ +13)└─────────────┬─────────────┘ +14)┌─────────────┴─────────────┐ +15)│ TableScan │ +16)│ -------------------- │ +17)│ data projection=[date, │ +18)│ ticker, time] │ +19)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ SortPreservingMergeExec │ @@ -1533,6 +2433,25 @@ explain SELECT * FROM data WHERE date = '2006-01-02' ORDER BY "ticker", "time"; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Sort │ +03)│ -------------------- │ +04)│ data.ticker ASC NULLS LAST│ +05)│ , data.time ASC NULLS LAST│ +06)└─────────────┬─────────────┘ +07)┌─────────────┴─────────────┐ +08)│ Filter │ +09)│ -------------------- │ +10)│ data.date = Date32("2006 │ +11)│ -01-02") │ +12)└─────────────┬─────────────┘ +13)┌─────────────┴─────────────┐ +14)│ TableScan │ +15)│ -------------------- │ +16)│ data projection=[date, │ +17)│ ticker, time] │ +18)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ SortPreservingMergeExec │ @@ -1575,6 +2494,32 @@ EXPLAIN WITH RECURSIVE nodes AS ( ) SELECT * FROM nodes ---- +logical_plan +01)┌───────────────────────────┐ +02)│ SubqueryAlias │ +03)│ -------------------- │ +04)│ nodes │ +05)└─────────────┬─────────────┘ +06)┌─────────────┴─────────────┐ +07)│ RecursiveQuery │ +08)│ -------------------- ├──────────────┐ +09)│ is_distinct=false │ │ +10)└─────────────┬─────────────┘ │ +11)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +12)│ Projection ││ Projection │ +13)│ -------------------- ││ -------------------- │ +14)│ Int64(1) AS id ││ nodes.id + Int64(1) AS id │ +15)└─────────────┬─────────────┘└─────────────┬─────────────┘ +16)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +17)│ EmptyRelation ││ Filter │ +18)│ -------------------- ││ -------------------- │ +19)│ rows=1 ││ nodes.id < Int64(10) │ +20)└───────────────────────────┘└─────────────┬─────────────┘ +21)-----------------------------┌─────────────┴─────────────┐ +22)-----------------------------│ TableScan │ +23)-----------------------------│ -------------------- │ +24)-----------------------------│ nodes projection=[id] │ +25)-----------------------------└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ RecursiveQueryExec ├──────────────┐ @@ -1613,6 +2558,27 @@ query TT explain 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/1.json'; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ CopyTo │ +03)│ -------------------- │ +04)│ format=json output_url │ +05)│ =test_files/scratch │ +06)│ /explain_tree/1.json │ +07)│ options: () │ +08)└─────────────┬─────────────┘ +09)┌─────────────┴─────────────┐ +10)│ Values │ +11)│ -------------------- │ +12)│ (Int64(1), Utf8("foo"), │ +13)│ Int64(1), Utf8("2023 │ +14)│ -01-01")), (Int64(2), │ +15)│ Utf8("bar"), Int64(2) │ +16)│ , Utf8("2023-01-02")), │ +17)│ (Int64(3), Utf8("baz"), │ +18)│ Int64(3), Utf8("2023-01 │ +19)│ -03")) │ +20)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ DataSinkExec │ @@ -1636,6 +2602,27 @@ query TT explain 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/2.csv'; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ CopyTo │ +03)│ -------------------- │ +04)│ format=csv output_url │ +05)│ =test_files/scratch │ +06)│ /explain_tree/2.csv │ +07)│ options: () │ +08)└─────────────┬─────────────┘ +09)┌─────────────┴─────────────┐ +10)│ Values │ +11)│ -------------------- │ +12)│ (Int64(1), Utf8("foo"), │ +13)│ Int64(1), Utf8("2023 │ +14)│ -01-01")), (Int64(2), │ +15)│ Utf8("bar"), Int64(2) │ +16)│ , Utf8("2023-01-02")), │ +17)│ (Int64(3), Utf8("baz"), │ +18)│ Int64(3), Utf8("2023-01 │ +19)│ -03")) │ +20)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ DataSinkExec │ @@ -1659,6 +2646,27 @@ query TT explain 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/3.arrow'; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ CopyTo │ +03)│ -------------------- │ +04)│ format=arrow output_url │ +05)│ =test_files/scratch │ +06)│ /explain_tree/3.arrow │ +07)│ options: () │ +08)└─────────────┬─────────────┘ +09)┌─────────────┴─────────────┐ +10)│ Values │ +11)│ -------------------- │ +12)│ (Int64(1), Utf8("foo"), │ +13)│ Int64(1), Utf8("2023 │ +14)│ -01-01")), (Int64(2), │ +15)│ Utf8("bar"), Int64(2) │ +16)│ , Utf8("2023-01-02")), │ +17)│ (Int64(3), Utf8("baz"), │ +18)│ Int64(3), Utf8("2023-01 │ +19)│ -03")) │ +20)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ DataSinkExec │ @@ -1686,6 +2694,37 @@ CREATE TABLE IF NOT EXISTS t1 (a INT) AS VALUES(1),(2),(3),(4),(5),(6),(7),(8),( query TT EXPLAIN SELECT COUNT(*) FROM (SELECT a FROM t1 WHERE a > 3 LIMIT 3 OFFSET 6); ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│count(Int64(1)) AS count(*)│ +05)└─────────────┬─────────────┘ +06)┌─────────────┴─────────────┐ +07)│ Aggregate │ +08)│ -------------------- │ +09)│ groupBy=[[]], aggr=[[count│ +10)│ (Int64(1))]] │ +11)└─────────────┬─────────────┘ +12)┌─────────────┴─────────────┐ +13)│ Projection │ +14)│ -------------------- │ +15)└─────────────┬─────────────┘ +16)┌─────────────┴─────────────┐ +17)│ Limit │ +18)│ -------------------- │ +19)│ skip=6, fetch=3 │ +20)└─────────────┬─────────────┘ +21)┌─────────────┴─────────────┐ +22)│ Filter │ +23)│ -------------------- │ +24)│ t1.a > Int32(3) │ +25)└─────────────┬─────────────┘ +26)┌─────────────┴─────────────┐ +27)│ TableScan │ +28)│ -------------------- │ +29)│ t1 projection=[a] │ +30)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ ProjectionExec │ @@ -1748,6 +2787,14 @@ drop table t1; query TT EXPLAIN SELECT * FROM generate_series(1, 100) ---- +logical_plan +01)┌───────────────────────────┐ +02)│ TableScan │ +03)│ -------------------- │ +04)│ generate_series() │ +05)│ projection= │ +06)│ [value] │ +07)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ LazyMemoryExec │ @@ -1761,6 +2808,23 @@ physical_plan query TT EXPLAIN SELECT c1, c2, c3 FROM sink_table WHERE c3 > 0 LIMIT 5; ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Limit │ +03)│ -------------------- │ +04)│ skip=0, fetch=5 │ +05)└─────────────┬─────────────┘ +06)┌─────────────┴─────────────┐ +07)│ Filter │ +08)│ -------------------- │ +09)│ sink_table.c3 > Int16(0) │ +10)└─────────────┬─────────────┘ +11)┌─────────────┴─────────────┐ +12)│ TableScan │ +13)│ -------------------- │ +14)│ sink_table projection=[c1,│ +15)│ c2, c3] │ +16)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ CoalescePartitionsExec │ @@ -1793,6 +2857,33 @@ physical_plan query TT EXPLAIN select count(*) from (values ('a', 'b'), ('c', 'd')) as t (c1, c2) ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Projection │ +03)│ -------------------- │ +04)│count(Int64(1)) AS count(*)│ +05)└─────────────┬─────────────┘ +06)┌─────────────┴─────────────┐ +07)│ Aggregate │ +08)│ -------------------- │ +09)│ groupBy=[[]], aggr=[[count│ +10)│ (Int64(1))]] │ +11)└─────────────┬─────────────┘ +12)┌─────────────┴─────────────┐ +13)│ SubqueryAlias │ +14)│ -------------------- │ +15)│ t │ +16)└─────────────┬─────────────┘ +17)┌─────────────┴─────────────┐ +18)│ Projection │ +19)│ -------------------- │ +20)└─────────────┬─────────────┘ +21)┌─────────────┴─────────────┐ +22)│ Values │ +23)│ -------------------- │ +24)│ (Utf8("a"), Utf8("b")), │ +25)│ (Utf8("c"), Utf8("d")) │ +26)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ ProjectionExec │ @@ -1806,6 +2897,39 @@ physical_plan query TT EXPLAIN select count(*) from (values ('a', 'b'), ('c', 'd')) as t (c1, c2) order by 1 limit 10 ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Sort │ +03)│ -------------------- │ +04)│ count(*) ASC NULLS LAST, │ +05)│ fetch=10 │ +06)└─────────────┬─────────────┘ +07)┌─────────────┴─────────────┐ +08)│ Projection │ +09)│ -------------------- │ +10)│count(Int64(1)) AS count(*)│ +11)└─────────────┬─────────────┘ +12)┌─────────────┴─────────────┐ +13)│ Aggregate │ +14)│ -------------------- │ +15)│ groupBy=[[]], aggr=[[count│ +16)│ (Int64(1))]] │ +17)└─────────────┬─────────────┘ +18)┌─────────────┴─────────────┐ +19)│ SubqueryAlias │ +20)│ -------------------- │ +21)│ t │ +22)└─────────────┬─────────────┘ +23)┌─────────────┴─────────────┐ +24)│ Projection │ +25)│ -------------------- │ +26)└─────────────┬─────────────┘ +27)┌─────────────┴─────────────┐ +28)│ Values │ +29)│ -------------------- │ +30)│ (Utf8("a"), Utf8("b")), │ +31)│ (Utf8("c"), Utf8("d")) │ +32)└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ ProjectionExec │ @@ -1826,11 +2950,66 @@ CREATE TABLE t (k int) query TT EXPLAIN SELECT * FROM t t1, t t2, t t3, t t4, t t5, t t6, t t7, t t8, t t9, t t10 ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Cross Join │ +03)│ -------------------- ├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +04)└─────────────┬─────────────┘ +05)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +06)│ Cross Join │ +07)│ -------------------- ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +08)│ │ │ +09)└─────────────┬─────────────┘ │ +10)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +11)│ Cross Join │ │ SubqueryAlias │ +12)│ -------------------- ├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ -------------------- │ +13)│ │ │ │ t9 │ +14)└─────────────┬─────────────┘ │ └─────────────┬─────────────┘ +15)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +16)│ Cross Join │ │ SubqueryAlias ││ TableScan │ +17)│ -------------------- ├───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ -------------------- ││ -------------------- │ +18)│ │ │ │ t8 ││ t projection=[k] │ +19)└─────────────┬─────────────┘ │ └─────────────┬─────────────┘└───────────────────────────┘ +20)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +21)│ Cross Join │ │ SubqueryAlias ││ TableScan │ +22)│ -------------------- ├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ -------------------- ││ -------------------- │ +23)│ │ │ │ t7 ││ t projection=[k] │ +24)└─────────────┬─────────────┘ │ └─────────────┬─────────────┘└───────────────────────────┘ +25)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +26)│ Cross Join │ │ SubqueryAlias ││ TableScan │ +27)│ -------------------- ├─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ -------------------- ││ -------------------- │ +28)│ │ │ │ t6 ││ t projection=[k] │ +29)└─────────────┬─────────────┘ │ └─────────────┬─────────────┘└───────────────────────────┘ +30)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +31)│ Cross Join │ │ SubqueryAlias ││ TableScan │ +32)│ -------------------- ├────────────────────────────────────────────────────────────────────────┐ │ -------------------- ││ -------------------- │ +33)│ │ │ │ t5 ││ t projection=[k] │ +34)└─────────────┬─────────────┘ │ └─────────────┬─────────────┘└───────────────────────────┘ +35)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +36)│ Cross Join │ │ SubqueryAlias ││ TableScan │ +37)│ -------------------- ├───────────────────────────────────────────┐ │ -------------------- ││ -------------------- │ +38)│ │ │ │ t4 ││ t projection=[k] │ +39)└─────────────┬─────────────┘ │ └─────────────┬─────────────┘└───────────────────────────┘ +40)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +41)│ Cross Join │ │ SubqueryAlias ││ TableScan │ +42)│ -------------------- ├──────────────┐ │ -------------------- ││ -------------------- │ +43)│ │ │ │ t3 ││ t projection=[k] │ +44)└─────────────┬─────────────┘ │ └─────────────┬─────────────┘└───────────────────────────┘ +45)┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +46)│ SubqueryAlias ││ SubqueryAlias ││ TableScan │ +47)│ -------------------- ││ -------------------- ││ -------------------- │ +48)│ t1 ││ t2 ││ t projection=[k] │ +49)└─────────────┬─────────────┘└─────────────┬─────────────┘└───────────────────────────┘ +50)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +51)│ TableScan ││ TableScan │ +52)│ -------------------- ││ -------------------- │ +53)│ t projection=[k] ││ t projection=[k] │ +54)└───────────────────────────┘└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ 02)│ CrossJoinExec ├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 03)└─────────────┬─────────────┘ -04)┌─────────────┴─────────────┐ +04)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ 05)│ CrossJoinExec │ 06)│ │ 07)│ ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -1901,73 +3080,118 @@ SET datafusion.explain.tree_maximum_render_width = 0 query TT EXPLAIN SELECT * FROM t t1, t t2, t t3, t t4, t t5, t t6, t t7, t t8, t t9, t t10 ---- +logical_plan +01)┌───────────────────────────┐ +02) +03) +04) +05)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +06) +07) +08) +09) +10)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +11) +12) +13) +14) +15)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +16) +17) +18) +19) +20)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +21) +22) +23) +24) +25)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +26) +27) +28) +29) +30)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +31) +32) +33) +34) +35)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +36) +37) +38) +39) +40)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +41) +42) +43) +44) +45)┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +46) +47) +48) +49) +50)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ physical_plan 01)┌───────────────────────────┐ -02)│ CrossJoinExec ├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -03)└─────────────┬─────────────┘ │ +02) +03) 04)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -05)│ CrossJoinExec │ │ DataSourceExec │ -06)│ │ │ -------------------- │ -07)│ ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ bytes: 0 │ -08)│ │ │ │ format: memory │ -09)│ │ │ │ rows: 0 │ -10)└─────────────┬─────────────┘ │ └───────────────────────────┘ +05) +06) +07) +08) +09) +10) 11)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -12)│ CrossJoinExec │ │ DataSourceExec │ -13)│ │ │ -------------------- │ -14)│ ├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ bytes: 0 │ -15)│ │ │ │ format: memory │ -16)│ │ │ │ rows: 0 │ -17)└─────────────┬─────────────┘ │ └───────────────────────────┘ +12) +13) +14) +15) +16) +17) 18)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -19)│ CrossJoinExec │ │ DataSourceExec │ -20)│ │ │ -------------------- │ -21)│ ├───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ bytes: 0 │ -22)│ │ │ │ format: memory │ -23)│ │ │ │ rows: 0 │ -24)└─────────────┬─────────────┘ │ └───────────────────────────┘ +19) +20) +21) +22) +23) +24) 25)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -26)│ CrossJoinExec │ │ DataSourceExec │ -27)│ │ │ -------------------- │ -28)│ ├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ bytes: 0 │ -29)│ │ │ │ format: memory │ -30)│ │ │ │ rows: 0 │ -31)└─────────────┬─────────────┘ │ └───────────────────────────┘ +26) +27) +28) +29) +30) +31) 32)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -33)│ CrossJoinExec │ │ DataSourceExec │ -34)│ │ │ -------------------- │ -35)│ ├─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ bytes: 0 │ -36)│ │ │ │ format: memory │ -37)│ │ │ │ rows: 0 │ -38)└─────────────┬─────────────┘ │ └───────────────────────────┘ +33) +34) +35) +36) +37) +38) 39)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -40)│ CrossJoinExec │ │ DataSourceExec │ -41)│ │ │ -------------------- │ -42)│ ├────────────────────────────────────────────────────────────────────────┐ │ bytes: 0 │ -43)│ │ │ │ format: memory │ -44)│ │ │ │ rows: 0 │ -45)└─────────────┬─────────────┘ │ └───────────────────────────┘ +40) +41) +42) +43) +44) +45) 46)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -47)│ CrossJoinExec │ │ DataSourceExec │ -48)│ │ │ -------------------- │ -49)│ ├───────────────────────────────────────────┐ │ bytes: 0 │ -50)│ │ │ │ format: memory │ -51)│ │ │ │ rows: 0 │ -52)└─────────────┬─────────────┘ │ └───────────────────────────┘ +47) +48) +49) +50) +51) +52) 53)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -54)│ CrossJoinExec │ │ DataSourceExec │ -55)│ │ │ -------------------- │ -56)│ ├──────────────┐ │ bytes: 0 │ -57)│ │ │ │ format: memory │ -58)│ │ │ │ rows: 0 │ -59)└─────────────┬─────────────┘ │ └───────────────────────────┘ +54) +55) +56) +57) +58) +59) 60)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -61)│ DataSourceExec ││ DataSourceExec │ -62)│ -------------------- ││ -------------------- │ -63)│ bytes: 0 ││ bytes: 0 │ -64)│ format: memory ││ format: memory │ -65)│ rows: 0 ││ rows: 0 │ -66)└───────────────────────────┘└───────────────────────────┘ # Setting the tree_maximum_render_size to a smaller size statement ok @@ -1976,66 +3200,121 @@ SET datafusion.explain.tree_maximum_render_width = 60 query TT EXPLAIN SELECT * FROM t t1, t t2, t t3, t t4, t t5, t t6, t t7, t t8, t t9, t t10 ---- +logical_plan +01)┌───────────────────────────┐ +02)│ Cross Join │ +03)│ -------------------- ├────────────────────────────────────────────────────────── +04)└─────────────┬─────────────┘ +05)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +06)│ Cross Join │ +07)│ -------------------- ├────────────────────────────────────────────────────────── +08)│ │ +09)└─────────────┬─────────────┘ +10)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +11)│ Cross Join │ +12)│ -------------------- ├────────────────────────────────────────────────────────── +13)│ │ +14)└─────────────┬─────────────┘ +15)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +16)│ Cross Join │ +17)│ -------------------- ├────────────────────────────────────────────────────────── +18)│ │ +19)└─────────────┬─────────────┘ +20)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +21)│ Cross Join │ +22)│ -------------------- ├────────────────────────────────────────────────────────── +23)│ │ +24)└─────────────┬─────────────┘ +25)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +26)│ Cross Join │ +27)│ -------------------- ├────────────────────────────────────────────────────────── +28)│ │ +29)└─────────────┬─────────────┘ +30)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +31)│ Cross Join │ +32)│ -------------------- ├────────────────────────────────────────────────────────── +33)│ │ +34)└─────────────┬─────────────┘ +35)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +36)│ Cross Join │ +37)│ -------------------- ├───────────────────────────────────────────┐ +38)│ │ │ +39)└─────────────┬─────────────┘ │ +40)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +41)│ Cross Join │ │ SubqueryAlias │ +42)│ -------------------- ├──────────────┐ │ -------------------- │ +43)│ │ │ │ t3 │ +44)└─────────────┬─────────────┘ │ └─────────────┬─────────────┘ +45)┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +46)│ SubqueryAlias ││ SubqueryAlias ││ TableScan │ +47)│ -------------------- ││ -------------------- ││ -------------------- │ +48)│ t1 ││ t2 ││ t projection=[k] │ +49)└─────────────┬─────────────┘└─────────────┬─────────────┘└───────────────────────────┘ +50)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +51)│ TableScan ││ TableScan │ +52)│ -------------------- ││ -------------------- │ +53)│ t projection=[k] ││ t projection=[k] │ +54)└───────────────────────────┘└───────────────────────────┘ physical_plan 01)┌───────────────────────────┐ -02)│ CrossJoinExec ├────────────────────────────────────────────────────────── -03)└─────────────┬─────────────┘ -04)┌─────────────┴─────────────┐ -05)│ CrossJoinExec │ -06)│ │ -07)│ ├────────────────────────────────────────────────────────── -08)│ │ -09)│ │ -10)└─────────────┬─────────────┘ -11)┌─────────────┴─────────────┐ -12)│ CrossJoinExec │ -13)│ │ -14)│ ├────────────────────────────────────────────────────────── -15)│ │ -16)│ │ -17)└─────────────┬─────────────┘ -18)┌─────────────┴─────────────┐ -19)│ CrossJoinExec │ -20)│ │ -21)│ ├────────────────────────────────────────────────────────── -22)│ │ -23)│ │ -24)└─────────────┬─────────────┘ -25)┌─────────────┴─────────────┐ -26)│ CrossJoinExec │ -27)│ │ -28)│ ├────────────────────────────────────────────────────────── -29)│ │ -30)│ │ -31)└─────────────┬─────────────┘ -32)┌─────────────┴─────────────┐ -33)│ CrossJoinExec │ -34)│ │ -35)│ ├────────────────────────────────────────────────────────── -36)│ │ -37)│ │ -38)└─────────────┬─────────────┘ -39)┌─────────────┴─────────────┐ -40)│ CrossJoinExec │ -41)│ │ -42)│ ├────────────────────────────────────────────────────────── -43)│ │ -44)│ │ -45)└─────────────┬─────────────┘ -46)┌─────────────┴─────────────┐ -47)│ CrossJoinExec │ -48)│ │ -49)│ ├───────────────────────────────────────────┐ -50)│ │ │ -51)│ │ │ -52)└─────────────┬─────────────┘ │ -53)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -54)│ CrossJoinExec │ │ DataSourceExec │ -55)│ │ │ -------------------- │ -56)│ ├──────────────┐ │ bytes: 0 │ -57)│ │ │ │ format: memory │ -58)│ │ │ │ rows: 0 │ -59)└─────────────┬─────────────┘ │ └───────────────────────────┘ +02)│ CrossJoinExec ├────────────────────────────────────────────────────────── +03)└─────────────┬─────────────┘ +04)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +05)│ CrossJoinExec │ +06)│ │ +07)│ ├────────────────────────────────────────────────────────── +08)│ │ +09)│ │ +10)└─────────────┬─────────────┘ +11)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +12)│ CrossJoinExec │ +13)│ │ +14)│ ├────────────────────────────────────────────────────────── +15)│ │ +16)│ │ +17)└─────────────┬─────────────┘ +18)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +19)│ CrossJoinExec │ +20)│ │ +21)│ ├────────────────────────────────────────────────────────── +22)│ │ +23)│ │ +24)└─────────────┬─────────────┘ +25)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +26)│ CrossJoinExec │ +27)│ │ +28)│ ├────────────────────────────────────────────────────────── +29)│ │ +30)│ │ +31)└─────────────┬─────────────┘ +32)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +33)│ CrossJoinExec │ +34)│ │ +35)│ ├────────────────────────────────────────────────────────── +36)│ │ +37)│ │ +38)└─────────────┬─────────────┘ +39)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +40)│ CrossJoinExec │ +41)│ │ +42)│ ├────────────────────────────────────────────────────────── +43)│ │ +44)│ │ +45)└─────────────┬─────────────┘ +46)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +47)│ CrossJoinExec │ +48)│ │ +49)│ ├───────────────────────────────────────────┐ +50)│ │ │ +51)│ │ │ +52)└─────────────┬─────────────┘ │ +53)┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +54)│ CrossJoinExec │ │ DataSourceExec │ +55)│ │ │ -------------------- │ +56)│ ├──────────────┐ │ bytes: 0 │ +57)│ │ │ │ format: memory │ +58)│ │ │ │ rows: 0 │ +59)└─────────────┬─────────────┘ │ └───────────────────────────┘ 60)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 61)│ DataSourceExec ││ DataSourceExec │ 62)│ -------------------- ││ -------------------- │