Path: blob/main/crates/polars-plan/src/plans/ir/format.rs
8430 views
use std::fmt::{self, Display, Formatter, Write};12use polars_core::frame::DataFrame;3use polars_core::schema::Schema;4use polars_io::RowIndex;5use polars_utils::format_list_truncated;6use polars_utils::slice_enum::Slice;7use recursive::recursive;89use self::ir::dot::ScanSourcesDisplay;10use crate::dsl::deletion::DeletionFilesList;11use crate::prelude::*;1213const INDENT_INCREMENT: usize = 2;1415pub struct IRDisplay<'a> {16lp: IRPlanRef<'a>,17}1819#[derive(Clone, Copy)]20pub struct ExprIRDisplay<'a> {21pub(crate) node: Node,22pub(crate) output_name: &'a OutputName,23pub(crate) expr_arena: &'a Arena<AExpr>,24}2526impl<'a> ExprIRDisplay<'a> {27pub fn display_node(node: Node, expr_arena: &'a Arena<AExpr>) -> Self {28Self {29node,30output_name: &OutputName::None,31expr_arena,32}33}34}3536/// Utility structure to display several [`ExprIR`]'s in a nice way37pub(crate) struct ExprIRSliceDisplay<'a, T: AsExpr> {38pub(crate) exprs: &'a [T],39pub(crate) expr_arena: &'a Arena<AExpr>,40}4142pub(crate) trait AsExpr {43fn node(&self) -> Node;44fn output_name(&self) -> &OutputName;45}4647impl AsExpr for Node {48fn node(&self) -> Node {49*self50}51fn output_name(&self) -> &OutputName {52&OutputName::None53}54}5556impl AsExpr for ExprIR {57fn node(&self) -> Node {58self.node()59}60fn output_name(&self) -> &OutputName {61self.output_name_inner()62}63}6465#[allow(clippy::too_many_arguments)]66fn write_scan(67f: &mut dyn fmt::Write,68name: &str,69sources: &ScanSources,70indent: usize,71n_columns: i64,72total_columns: usize,73row_estimation: Option<usize>,74predicate: &Option<ExprIRDisplay<'_>>,75pre_slice: Option<Slice>,76row_index: Option<&RowIndex>,77deletion_files: Option<&DeletionFilesList>,78) -> fmt::Result {79write!(80f,81"{:indent$}{name} SCAN {}",82"",83ScanSourcesDisplay(sources),84)?;8586let total_columns = total_columns - usize::from(row_index.is_some());87if n_columns > 0 {88write!(89f,90"\n{:indent$}PROJECT {n_columns}/{total_columns} COLUMNS",91"",92)?;93} else {94write!(f, "\n{:indent$}PROJECT */{total_columns} COLUMNS", "")?;95}96if let Some(predicate) = predicate {97write!(f, "\n{:indent$}SELECTION: {predicate}", "")?;98}99if let Some(pre_slice) = pre_slice {100write!(f, "\n{:indent$}SLICE: {pre_slice:?}", "")?;101}102if let Some(row_index) = row_index {103write!(f, "\n{:indent$}ROW_INDEX: {}", "", row_index.name)?;104if row_index.offset != 0 {105write!(f, " (offset: {})", row_index.offset)?;106}107}108if let Some(deletion_files) = deletion_files {109write!(f, "\n{deletion_files}")?;110}111if let Some(row_estimation) = row_estimation {112write!(f, "\n{:indent$}ESTIMATED ROWS: {row_estimation}", "")?;113}114Ok(())115}116117impl<'a> IRDisplay<'a> {118pub fn new(lp: IRPlanRef<'a>) -> Self {119Self { lp }120}121122fn root(&self) -> &IR {123self.lp.root()124}125126fn with_root(&self, root: Node) -> Self {127Self {128lp: self.lp.with_root(root),129}130}131132fn display_expr(&self, root: &'a ExprIR) -> ExprIRDisplay<'a> {133ExprIRDisplay {134node: root.node(),135output_name: root.output_name_inner(),136expr_arena: self.lp.expr_arena,137}138}139140fn display_expr_slice(&self, exprs: &'a [ExprIR]) -> ExprIRSliceDisplay<'a, ExprIR> {141ExprIRSliceDisplay {142exprs,143expr_arena: self.lp.expr_arena,144}145}146147#[recursive]148fn _format(&self, f: &mut Formatter, indent: usize) -> fmt::Result {149if indent != 0 {150writeln!(f)?;151}152153let sub_indent = indent + INDENT_INCREMENT;154use IR::*;155156let ir_node = self.root();157let output_schema = ir_node.schema(self.lp.lp_arena);158let output_schema = output_schema.as_ref();159match ir_node {160Union { inputs, options } => {161write_ir_non_recursive(f, ir_node, self.lp.expr_arena, output_schema, indent)?;162let name = if let Some(slice) = options.slice {163format!("SLICED UNION: {slice:?}")164} else {165"UNION".to_string()166};167168// 3 levels of indentation169// - 0 => UNION ... END UNION170// - 1 => PLAN 0, PLAN 1, ... PLAN N171// - 2 => actual formatting of plans172let sub_sub_indent = sub_indent + INDENT_INCREMENT;173for (i, plan) in inputs.iter().enumerate() {174write!(f, "\n{:sub_indent$}PLAN {i}:", "")?;175self.with_root(*plan)._format(f, sub_sub_indent)?;176}177write!(f, "\n{:indent$}END {name}", "")178},179HConcat { inputs, .. } => {180let sub_sub_indent = sub_indent + INDENT_INCREMENT;181write_ir_non_recursive(f, ir_node, self.lp.expr_arena, output_schema, indent)?;182for (i, plan) in inputs.iter().enumerate() {183write!(f, "\n{:sub_indent$}PLAN {i}:", "")?;184self.with_root(*plan)._format(f, sub_sub_indent)?;185}186write!(f, "\n{:indent$}END HCONCAT", "")187},188GroupBy { input, .. } => {189write_ir_non_recursive(f, ir_node, self.lp.expr_arena, output_schema, indent)?;190write!(f, "\n{:sub_indent$}FROM", "")?;191self.with_root(*input)._format(f, sub_indent)?;192Ok(())193},194Join {195input_left,196input_right,197left_on,198right_on,199options,200..201} => {202let left_on = self.display_expr_slice(left_on);203let right_on = self.display_expr_slice(right_on);204205// Fused cross + filter (show as nested loop join)206if let Some(JoinTypeOptionsIR::CrossAndFilter { predicate }) = &options.options {207let predicate = self.display_expr(predicate);208let name = "NESTED LOOP";209write!(f, "{:indent$}{name} JOIN ON {predicate}:", "")?;210write!(f, "\n{:indent$}LEFT PLAN:", "")?;211self.with_root(*input_left)._format(f, sub_indent)?;212write!(f, "\n{:indent$}RIGHT PLAN:", "")?;213self.with_root(*input_right)._format(f, sub_indent)?;214write!(f, "\n{:indent$}END {name} JOIN", "")215} else {216let how = &options.args.how;217write!(f, "{:indent$}{how} JOIN:", "")?;218write!(f, "\n{:indent$}LEFT PLAN ON: {left_on}", "")?;219self.with_root(*input_left)._format(f, sub_indent)?;220write!(f, "\n{:indent$}RIGHT PLAN ON: {right_on}", "")?;221self.with_root(*input_right)._format(f, sub_indent)?;222write!(f, "\n{:indent$}END {how} JOIN", "")223}224},225MapFunction { input, .. } => {226write_ir_non_recursive(f, ir_node, self.lp.expr_arena, output_schema, indent)?;227self.with_root(*input)._format(f, sub_indent)228},229SinkMultiple { inputs } => {230write_ir_non_recursive(f, ir_node, self.lp.expr_arena, output_schema, indent)?;231232// 3 levels of indentation233// - 0 => SINK_MULTIPLE ... END SINK_MULTIPLE234// - 1 => PLAN 0, PLAN 1, ... PLAN N235// - 2 => actual formatting of plans236let sub_sub_indent = sub_indent + 2;237for (i, plan) in inputs.iter().enumerate() {238write!(f, "\n{:sub_indent$}PLAN {i}:", "")?;239self.with_root(*plan)._format(f, sub_sub_indent)?;240}241write!(f, "\n{:indent$}END SINK_MULTIPLE", "")242},243#[cfg(feature = "merge_sorted")]244MergeSorted {245input_left,246input_right,247key: _,248} => {249write_ir_non_recursive(f, ir_node, self.lp.expr_arena, output_schema, indent)?;250write!(f, ":")?;251252write!(f, "\n{:indent$}LEFT PLAN:", "")?;253self.with_root(*input_left)._format(f, sub_indent)?;254write!(f, "\n{:indent$}RIGHT PLAN:", "")?;255self.with_root(*input_right)._format(f, sub_indent)?;256write!(f, "\n{:indent$}END MERGE_SORTED", "")257},258ir_node => {259write_ir_non_recursive(f, ir_node, self.lp.expr_arena, output_schema, indent)?;260for input in ir_node.inputs() {261self.with_root(input)._format(f, sub_indent)?;262}263Ok(())264},265}266}267}268269impl<'a> ExprIRDisplay<'a> {270fn with_slice<T: AsExpr>(&self, exprs: &'a [T]) -> ExprIRSliceDisplay<'a, T> {271ExprIRSliceDisplay {272exprs,273expr_arena: self.expr_arena,274}275}276277fn with_root<T: AsExpr>(&self, root: &'a T) -> Self {278Self {279node: root.node(),280output_name: root.output_name(),281expr_arena: self.expr_arena,282}283}284}285286impl Display for IRDisplay<'_> {287fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {288self._format(f, 0)289}290}291292impl fmt::Debug for IRDisplay<'_> {293fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {294Display::fmt(&self, f)295}296}297298impl<T: AsExpr> Display for ExprIRSliceDisplay<'_, T> {299fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {300// Display items in slice delimited by a comma301302use std::fmt::Write;303304let mut iter = self.exprs.iter();305306f.write_char('[')?;307if let Some(fst) = iter.next() {308let fst = ExprIRDisplay {309node: fst.node(),310output_name: fst.output_name(),311expr_arena: self.expr_arena,312};313write!(f, "{fst}")?;314}315316for expr in iter {317let expr = ExprIRDisplay {318node: expr.node(),319output_name: expr.output_name(),320expr_arena: self.expr_arena,321};322write!(f, ", {expr}")?;323}324325f.write_char(']')?;326327Ok(())328}329}330331impl<T: AsExpr> fmt::Debug for ExprIRSliceDisplay<'_, T> {332fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {333Display::fmt(self, f)334}335}336337impl Display for ExprIRDisplay<'_> {338#[recursive]339fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {340let root = self.expr_arena.get(self.node);341342use AExpr::*;343match root {344Element => f.write_str("element()"),345#[cfg(feature = "dynamic_group_by")]346Rolling {347function,348index_column,349period,350offset,351closed_window: _,352} => {353let function = self.with_root(function);354let index_column = self.with_root(index_column);355write!(356f,357"{function}.rolling(by='{index_column}', offset={offset}, period={period})",358)359},360Over {361function,362partition_by,363order_by,364mapping: _,365} => {366let function = self.with_root(function);367let partition_by = self.with_slice(partition_by);368if let Some((order_by, _)) = order_by {369let order_by = self.with_root(order_by);370write!(371f,372"{function}.over(partition_by: {partition_by}, order_by: {order_by})"373)374} else {375write!(f, "{function}.over({partition_by})")376}377},378Len => write!(f, "len()"),379Explode { expr, options } => {380let expr = self.with_root(expr);381write!(f, "{expr}.explode(")?;382match (options.empty_as_null, options.keep_nulls) {383(true, true) => {},384(true, false) => f.write_str("keep_nulls=false")?,385(false, true) => f.write_str("empty_as_null=false")?,386(false, false) => f.write_str("empty_as_null=false, keep_nulls=false")?,387}388f.write_char(')')389},390Column(name) => write!(f, "col(\"{name}\")"),391#[cfg(feature = "dtype-struct")]392StructField(name) => write!(f, "field(\"{name}\")"),393Literal(v) => write!(f, "{v:?}"),394BinaryExpr { left, op, right } => {395let left = self.with_root(left);396let right = self.with_root(right);397write!(f, "[({left}) {op:?} ({right})]")398},399Sort { expr, options } => {400let expr = self.with_root(expr);401if options.descending {402write!(f, "{expr}.sort(desc)")403} else {404write!(f, "{expr}.sort(asc)")405}406},407SortBy {408expr,409by,410sort_options,411} => {412let expr = self.with_root(expr);413let by = self.with_slice(by);414write!(f, "{expr}.sort_by(by={by}, sort_option={sort_options:?})",)415},416Filter { input, by } => {417let input = self.with_root(input);418let by = self.with_root(by);419420write!(f, "{input}.filter({by})")421},422Gather {423expr,424idx,425returns_scalar,426null_on_oob: _,427} => {428let expr = self.with_root(expr);429let idx = self.with_root(idx);430expr.fmt(f)?;431432if *returns_scalar {433write!(f, ".get({idx})")434} else {435write!(f, ".gather({idx})")436}437},438Agg(agg) => {439use IRAggExpr::*;440match agg {441Min {442input,443propagate_nans,444} => {445self.with_root(input).fmt(f)?;446if *propagate_nans {447write!(f, ".nan_min()")448} else {449write!(f, ".min()")450}451},452Max {453input,454propagate_nans,455} => {456self.with_root(input).fmt(f)?;457if *propagate_nans {458write!(f, ".nan_max()")459} else {460write!(f, ".max()")461}462},463Median(expr) => write!(f, "{}.median()", self.with_root(expr)),464Mean(expr) => write!(f, "{}.mean()", self.with_root(expr)),465First(expr) => write!(f, "{}.first()", self.with_root(expr)),466FirstNonNull(expr) => write!(f, "{}.first_non_null()", self.with_root(expr)),467Last(expr) => write!(f, "{}.last()", self.with_root(expr)),468LastNonNull(expr) => write!(f, "{}.last_non_null()", self.with_root(expr)),469Item { input, allow_empty } => {470self.with_root(input).fmt(f)?;471if *allow_empty {472write!(f, ".item(allow_empty=true)")473} else {474write!(f, ".item()")475}476},477Implode(expr) => write!(f, "{}.implode()", self.with_root(expr)),478NUnique(expr) => write!(f, "{}.n_unique()", self.with_root(expr)),479Sum(expr) => write!(f, "{}.sum()", self.with_root(expr)),480AggGroups(expr) => write!(f, "{}.groups()", self.with_root(expr)),481Count {482input,483include_nulls: false,484} => write!(f, "{}.count()", self.with_root(input)),485Count {486input,487include_nulls: true,488} => write!(f, "{}.len()", self.with_root(input)),489Var(expr, _) => write!(f, "{}.var()", self.with_root(expr)),490Std(expr, _) => write!(f, "{}.std()", self.with_root(expr)),491Quantile {492expr,493quantile,494method,495} => write!(496f,497"{}.quantile({}, interpolation='{}')",498self.with_root(expr),499self.with_root(quantile),500<&'static str>::from(method),501),502}503},504Cast {505expr,506dtype,507options,508} => {509self.with_root(expr).fmt(f)?;510if options.is_strict() {511write!(f, ".strict_cast({dtype:?})")512} else {513write!(f, ".cast({dtype:?})")514}515},516Ternary {517predicate,518truthy,519falsy,520} => {521let predicate = self.with_root(predicate);522let truthy = self.with_root(truthy);523let falsy = self.with_root(falsy);524write!(f, "when({predicate}).then({truthy}).otherwise({falsy})",)525},526Function {527input, function, ..528} => {529let fst = self.with_root(&input[0]);530fst.fmt(f)?;531if input.len() >= 2 {532write!(f, ".{function}({})", self.with_slice(&input[1..]))533} else {534write!(f, ".{function}()")535}536},537AnonymousFunction { input, fmt_str, .. } | AnonymousAgg { input, fmt_str, .. } => {538let fst = self.with_root(&input[0]);539fst.fmt(f)?;540if input.len() >= 2 {541write!(f, ".{fmt_str}({})", self.with_slice(&input[1..]))542} else {543write!(f, ".{fmt_str}()")544}545},546Eval {547expr,548evaluation,549variant,550} => {551let expr = self.with_root(expr);552let evaluation = self.with_root(evaluation);553match variant {554EvalVariant::List => write!(f, "{expr}.list.eval({evaluation})"),555EvalVariant::ListAgg => write!(f, "{expr}.list.agg({evaluation})"),556EvalVariant::Array { as_list: false } => {557write!(f, "{expr}.array.eval({evaluation})")558},559EvalVariant::Array { as_list: true } => {560write!(f, "{expr}.array.eval({evaluation}, as_list=true)")561},562EvalVariant::ArrayAgg => write!(f, "{expr}.array.agg({evaluation})"),563EvalVariant::Cumulative { min_samples } => write!(564f,565"{expr}.cumulative_eval({evaluation}, min_samples={min_samples})"566),567}568},569#[cfg(feature = "dtype-struct")]570StructEval { expr, evaluation } => {571let expr = self.with_root(expr);572let evaluation = self.with_slice(evaluation);573write!(f, "{expr}.struct.with_fields({evaluation})")574},575Slice {576input,577offset,578length,579} => {580let input = self.with_root(input);581let offset = self.with_root(offset);582let length = self.with_root(length);583584write!(f, "{input}.slice(offset={offset}, length={length})")585},586}?;587588match self.output_name {589OutputName::None => {},590OutputName::LiteralLhs(_) => {},591OutputName::ColumnLhs(_) => {},592#[cfg(feature = "dtype-struct")]593OutputName::Field(_) => {},594OutputName::Alias(name) => {595if root.to_name(self.expr_arena) != name {596write!(f, r#".alias("{name}")"#)?;597}598},599}600601Ok(())602}603}604605impl fmt::Debug for ExprIRDisplay<'_> {606fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {607Display::fmt(self, f)608}609}610611pub(crate) struct ColumnsDisplay<'a>(pub(crate) &'a Schema);612613impl fmt::Display for ColumnsDisplay<'_> {614fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {615let len = self.0.len();616let mut iter_names = self.0.iter_names().enumerate();617618const MAX_LEN: usize = 32;619const ADD_PER_ITEM: usize = 4;620621let mut current_len = 0;622623if let Some((_, fst)) = iter_names.next() {624write!(f, "\"{fst}\"")?;625626current_len += fst.len() + ADD_PER_ITEM;627}628629for (i, col) in iter_names {630current_len += col.len() + ADD_PER_ITEM;631632if current_len > MAX_LEN {633write!(f, ", ... {} other ", len - i)?;634if len - i == 1 {635f.write_str("column")?;636} else {637f.write_str("columns")?;638}639640break;641}642643write!(f, ", \"{col}\"")?;644}645646Ok(())647}648}649650impl fmt::Debug for Operator {651fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {652Display::fmt(self, f)653}654}655656impl fmt::Debug for LiteralValue {657fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {658use LiteralValue::*;659660match self {661Self::Scalar(sc) => write!(f, "{}", sc.value()),662Self::Series(s) => {663let name = s.name();664if name.is_empty() {665write!(f, "Series")666} else {667write!(f, "Series[{name}]")668}669},670Range(range) => fmt::Debug::fmt(range, f),671Dyn(d) => fmt::Debug::fmt(d, f),672}673}674}675676impl fmt::Debug for DynLiteralValue {677fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {678match self {679Self::Int(v) => write!(f, "dyn int: {v}"),680Self::Float(v) => write!(f, "dyn float: {v}"),681Self::Str(v) => write!(f, "dyn str: {v}"),682Self::List(_) => todo!(),683}684}685}686687impl fmt::Debug for RangeLiteralValue {688fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {689write!(f, "range({}, {})", self.low, self.high)690}691}692693pub fn write_ir_non_recursive(694f: &mut dyn fmt::Write,695ir: &IR,696expr_arena: &Arena<AExpr>,697output_schema: &Schema,698indent: usize,699) -> fmt::Result {700match ir {701#[cfg(feature = "python")]702IR::PythonScan { options } => {703let total_columns = options.schema.len();704let n_columns = options705.with_columns706.as_ref()707.map(|s| s.len() as i64)708.unwrap_or(-1);709710let predicate = match &options.predicate {711PythonPredicate::Polars(e) => Some(e.display(expr_arena)),712PythonPredicate::PyArrow(_) => None,713PythonPredicate::None => None,714};715716write_scan(717f,718"PYTHON",719&ScanSources::default(),720indent,721n_columns,722total_columns,723None,724&predicate,725options726.n_rows727.map(|len| polars_utils::slice_enum::Slice::Positive { offset: 0, len }),728None,729None,730)731},732IR::Slice {733input: _,734offset,735len,736} => {737write!(f, "{:indent$}SLICE[offset: {offset}, len: {len}]", "")738},739IR::Filter {740input: _,741predicate,742} => {743let predicate = predicate.display(expr_arena);744// this one is writeln because we don't increase indent (which inserts a line)745write!(f, "{:indent$}FILTER {predicate}", "")?;746write!(f, "\n{:indent$}FROM", "")747},748IR::Scan {749sources,750file_info,751predicate,752predicate_file_skip_applied: _,753scan_type,754unified_scan_args,755hive_parts: _,756output_schema: _,757} => {758let n_columns = unified_scan_args759.projection760.as_ref()761.map(|columns| columns.len() as i64)762.unwrap_or(-1);763764let row_estimation = if file_info.row_estimation.1 != usize::MAX {765Some(file_info.row_estimation.1)766} else {767None768};769770let predicate = predicate.as_ref().map(|p| p.display(expr_arena));771772write_scan(773f,774(&**scan_type).into(),775sources,776indent,777n_columns,778file_info.schema.len(),779row_estimation,780&predicate,781unified_scan_args.pre_slice.clone(),782unified_scan_args.row_index.as_ref(),783unified_scan_args.deletion_files.as_ref(),784)785},786IR::DataFrameScan {787df: _,788schema,789output_schema,790} => {791let total_columns = schema.len();792let (n_columns, projected) = if let Some(schema) = output_schema {793(794format!("{}", schema.len()),795format_list_truncated!(schema.iter_names(), 4, '"'),796)797} else {798("*".to_string(), "".to_string())799};800write!(801f,802"{:indent$}DF {}; PROJECT{} {}/{} COLUMNS",803"",804format_list_truncated!(schema.iter_names(), 4, '"'),805projected,806n_columns,807total_columns,808)809},810IR::SimpleProjection { input: _, columns } => {811let num_columns = columns.as_ref().len();812let total_columns = output_schema.len();813814let columns = ColumnsDisplay(columns.as_ref());815write!(816f,817"{:indent$}simple π {num_columns}/{total_columns} [{columns}]",818""819)820},821IR::Select {822input: _,823expr,824schema: _,825options: _,826} => {827// @NOTE: Maybe there should be a clear delimiter here?828let exprs = ExprIRSliceDisplay {829exprs: expr,830expr_arena,831};832write!(f, "{:indent$}SELECT {exprs}", "")?;833Ok(())834},835IR::Sort {836input: _,837by_column,838slice,839sort_options,840} => {841write!(f, "{:indent$}", "")?;842843f.write_str("SORT BY ")?;844845if slice.is_some()846|| sort_options.maintain_order847|| sort_options.descending.iter().any(|v| *v)848|| sort_options.nulls_last.iter().any(|v| *v)849{850f.write_char('[')?;851852let mut comma = false;853if let Some((o, l)) = slice {854write!(f, "slice: ({o}, {l})")?;855comma = true;856}857if sort_options.maintain_order {858if comma {859f.write_str(", ")?;860}861f.write_str("maintain_order: true")?;862comma = true;863}864if sort_options.descending.iter().any(|v| *v) {865if comma {866f.write_str(", ")?;867}868write!(f, "descending: {:?}", sort_options.descending.as_slice())?;869comma = true;870}871if sort_options.nulls_last.iter().any(|v| *v) {872if comma {873f.write_str(", ")?;874}875write!(f, "nulls_last: {:?}", sort_options.nulls_last.as_slice())?;876}877878f.write_str("] ")?;879}880881write!(882f,883"{}",884ExprIRSliceDisplay {885exprs: by_column,886expr_arena,887}888)889},890IR::Cache { input: _, id } => write!(f, "{:indent$}CACHE[id: {id}]", ""),891IR::GroupBy {892input: _,893keys,894aggs,895schema: _,896maintain_order,897options: _,898apply,899} => write_group_by(900f,901indent,902expr_arena,903keys,904aggs,905apply.as_ref(),906*maintain_order,907),908IR::Join {909input_left: _,910input_right: _,911schema: _,912left_on,913right_on,914options,915} => {916let left_on = ExprIRSliceDisplay {917exprs: left_on,918expr_arena,919};920let right_on = ExprIRSliceDisplay {921exprs: right_on,922expr_arena,923};924925// Fused cross + filter (show as nested loop join)926if let Some(JoinTypeOptionsIR::CrossAndFilter { predicate }) = &options.options {927let predicate = predicate.display(expr_arena);928write!(f, "{:indent$}NESTED_LOOP JOIN ON {predicate}", "")?;929} else {930let how = &options.args.how;931write!(f, "{:indent$}{how} JOIN", "")?;932write!(f, "\n{:indent$}LEFT PLAN ON: {left_on}", "")?;933write!(f, "\n{:indent$}RIGHT PLAN ON: {right_on}", "")?;934}935936Ok(())937},938IR::HStack {939input: _,940exprs,941schema: _,942options: _,943} => {944// @NOTE: Maybe there should be a clear delimiter here?945let exprs = ExprIRSliceDisplay { exprs, expr_arena };946947write!(f, "{:indent$} WITH_COLUMNS:", "",)?;948write!(f, "\n{:indent$} {exprs} ", "")949},950IR::Distinct { input: _, options } => {951write!(952f,953"{:indent$}UNIQUE[maintain_order: {:?}, keep_strategy: {:?}] BY {:?}",954"", options.maintain_order, options.keep_strategy, options.subset955)956},957IR::MapFunction { input: _, function } => write!(f, "{:indent$}{function}", ""),958IR::Union { inputs: _, options } => {959let name = if let Some(slice) = options.slice {960format!("SLICED UNION: {slice:?}")961} else {962"UNION".to_string()963};964write!(f, "{:indent$}{name}", "")965},966IR::HConcat {967inputs: _,968schema: _,969options: _,970} => write!(f, "{:indent$}HCONCAT", ""),971IR::ExtContext {972input: _,973contexts: _,974schema: _,975} => write!(f, "{:indent$}EXTERNAL_CONTEXT", ""),976IR::Sink { input: _, payload } => {977let name = match payload {978SinkTypeIR::Memory => "SINK (memory)",979SinkTypeIR::Callback { .. } => "SINK (callback)",980SinkTypeIR::File { .. } => "SINK (file)",981SinkTypeIR::Partitioned { .. } => "SINK (partition)",982};983write!(f, "{:indent$}{name}", "")984},985IR::SinkMultiple { inputs: _ } => write!(f, "{:indent$}SINK_MULTIPLE", ""),986#[cfg(feature = "merge_sorted")]987IR::MergeSorted {988input_left: _,989input_right: _,990key,991} => write!(f, "{:indent$}MERGE SORTED ON '{key}'", ""),992IR::Invalid => write!(f, "{:indent$}INVALID", ""),993}994}995996pub fn write_group_by(997f: &mut dyn fmt::Write,998indent: usize,999expr_arena: &Arena<AExpr>,1000keys: &[ExprIR],1001aggs: &[ExprIR],1002apply: Option<&PlanCallback<DataFrame, DataFrame>>,1003maintain_order: bool,1004) -> fmt::Result {1005let sub_indent = indent + INDENT_INCREMENT;1006let keys = ExprIRSliceDisplay {1007exprs: keys,1008expr_arena,1009};1010write!(1011f,1012"{:indent$}AGGREGATE[maintain_order: {}]",1013"", maintain_order1014)?;1015if apply.is_some() {1016write!(f, "\n{:sub_indent$}MAP_GROUPS BY {keys}", "")?;1017write!(f, "\n{:sub_indent$}FROM", "")?;1018} else {1019let aggs = ExprIRSliceDisplay {1020exprs: aggs,1021expr_arena,1022};1023write!(f, "\n{:sub_indent$}{aggs} BY {keys}", "")?;1024}10251026Ok(())1027}102810291030