Path: blob/main/crates/polars-plan/src/plans/ir/format.rs
6940 views
use std::fmt::{self, Display, Formatter};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,73predicate: &Option<ExprIRDisplay<'_>>,74pre_slice: Option<Slice>,75row_index: Option<&RowIndex>,76deletion_files: Option<&DeletionFilesList>,77) -> fmt::Result {78write!(79f,80"{:indent$}{name} SCAN {}",81"",82ScanSourcesDisplay(sources),83)?;8485let total_columns = total_columns - usize::from(row_index.is_some());86if n_columns > 0 {87write!(88f,89"\n{:indent$}PROJECT {n_columns}/{total_columns} COLUMNS",90"",91)?;92} else {93write!(f, "\n{:indent$}PROJECT */{total_columns} COLUMNS", "")?;94}95if let Some(predicate) = predicate {96write!(f, "\n{:indent$}SELECTION: {predicate}", "")?;97}98if let Some(pre_slice) = pre_slice {99write!(f, "\n{:indent$}SLICE: {pre_slice:?}", "")?;100}101if let Some(row_index) = row_index {102write!(f, "\n{:indent$}ROW_INDEX: {}", "", row_index.name)?;103if row_index.offset != 0 {104write!(f, " (offset: {})", row_index.offset)?;105}106}107if let Some(deletion_files) = deletion_files {108write!(f, "\n{deletion_files}")?;109}110Ok(())111}112113impl<'a> IRDisplay<'a> {114pub fn new(lp: IRPlanRef<'a>) -> Self {115Self { lp }116}117118fn root(&self) -> &IR {119self.lp.root()120}121122fn with_root(&self, root: Node) -> Self {123Self {124lp: self.lp.with_root(root),125}126}127128fn display_expr(&self, root: &'a ExprIR) -> ExprIRDisplay<'a> {129ExprIRDisplay {130node: root.node(),131output_name: root.output_name_inner(),132expr_arena: self.lp.expr_arena,133}134}135136fn display_expr_slice(&self, exprs: &'a [ExprIR]) -> ExprIRSliceDisplay<'a, ExprIR> {137ExprIRSliceDisplay {138exprs,139expr_arena: self.lp.expr_arena,140}141}142143#[recursive]144fn _format(&self, f: &mut Formatter, indent: usize) -> fmt::Result {145if indent != 0 {146writeln!(f)?;147}148149let sub_indent = indent + INDENT_INCREMENT;150use IR::*;151152let ir_node = self.root();153let output_schema = ir_node.schema(self.lp.lp_arena);154let output_schema = output_schema.as_ref();155match ir_node {156Union { inputs, options } => {157write_ir_non_recursive(f, ir_node, self.lp.expr_arena, output_schema, indent)?;158let name = if let Some(slice) = options.slice {159format!("SLICED UNION: {slice:?}")160} else {161"UNION".to_string()162};163164// 3 levels of indentation165// - 0 => UNION ... END UNION166// - 1 => PLAN 0, PLAN 1, ... PLAN N167// - 2 => actual formatting of plans168let sub_sub_indent = sub_indent + INDENT_INCREMENT;169for (i, plan) in inputs.iter().enumerate() {170write!(f, "\n{:sub_indent$}PLAN {i}:", "")?;171self.with_root(*plan)._format(f, sub_sub_indent)?;172}173write!(f, "\n{:indent$}END {name}", "")174},175HConcat { inputs, .. } => {176let sub_sub_indent = sub_indent + INDENT_INCREMENT;177write_ir_non_recursive(f, ir_node, self.lp.expr_arena, output_schema, indent)?;178for (i, plan) in inputs.iter().enumerate() {179write!(f, "\n{:sub_indent$}PLAN {i}:", "")?;180self.with_root(*plan)._format(f, sub_sub_indent)?;181}182write!(f, "\n{:indent$}END HCONCAT", "")183},184GroupBy { input, .. } => {185write_ir_non_recursive(f, ir_node, self.lp.expr_arena, output_schema, indent)?;186write!(f, "\n{:sub_indent$}FROM", "")?;187self.with_root(*input)._format(f, sub_indent)?;188Ok(())189},190Join {191input_left,192input_right,193left_on,194right_on,195options,196..197} => {198let left_on = self.display_expr_slice(left_on);199let right_on = self.display_expr_slice(right_on);200201// Fused cross + filter (show as nested loop join)202if let Some(JoinTypeOptionsIR::CrossAndFilter { predicate }) = &options.options {203let predicate = self.display_expr(predicate);204let name = "NESTED LOOP";205write!(f, "{:indent$}{name} JOIN ON {predicate}:", "")?;206write!(f, "\n{:indent$}LEFT PLAN:", "")?;207self.with_root(*input_left)._format(f, sub_indent)?;208write!(f, "\n{:indent$}RIGHT PLAN:", "")?;209self.with_root(*input_right)._format(f, sub_indent)?;210write!(f, "\n{:indent$}END {name} JOIN", "")211} else {212let how = &options.args.how;213write!(f, "{:indent$}{how} JOIN:", "")?;214write!(f, "\n{:indent$}LEFT PLAN ON: {left_on}", "")?;215self.with_root(*input_left)._format(f, sub_indent)?;216write!(f, "\n{:indent$}RIGHT PLAN ON: {right_on}", "")?;217self.with_root(*input_right)._format(f, sub_indent)?;218write!(f, "\n{:indent$}END {how} JOIN", "")219}220},221MapFunction { input, .. } => {222write_ir_non_recursive(f, ir_node, self.lp.expr_arena, output_schema, indent)?;223self.with_root(*input)._format(f, sub_indent)224},225SinkMultiple { inputs } => {226write_ir_non_recursive(f, ir_node, self.lp.expr_arena, output_schema, indent)?;227228// 3 levels of indentation229// - 0 => SINK_MULTIPLE ... END SINK_MULTIPLE230// - 1 => PLAN 0, PLAN 1, ... PLAN N231// - 2 => actual formatting of plans232let sub_sub_indent = sub_indent + 2;233for (i, plan) in inputs.iter().enumerate() {234write!(f, "\n{:sub_indent$}PLAN {i}:", "")?;235self.with_root(*plan)._format(f, sub_sub_indent)?;236}237write!(f, "\n{:indent$}END SINK_MULTIPLE", "")238},239#[cfg(feature = "merge_sorted")]240MergeSorted {241input_left,242input_right,243key: _,244} => {245write_ir_non_recursive(f, ir_node, self.lp.expr_arena, output_schema, indent)?;246write!(f, ":")?;247248write!(f, "\n{:indent$}LEFT PLAN:", "")?;249self.with_root(*input_left)._format(f, sub_indent)?;250write!(f, "\n{:indent$}RIGHT PLAN:", "")?;251self.with_root(*input_right)._format(f, sub_indent)?;252write!(f, "\n{:indent$}END MERGE_SORTED", "")253},254ir_node => {255write_ir_non_recursive(f, ir_node, self.lp.expr_arena, output_schema, indent)?;256for input in ir_node.inputs() {257self.with_root(input)._format(f, sub_indent)?;258}259Ok(())260},261}262}263}264265impl<'a> ExprIRDisplay<'a> {266fn with_slice<T: AsExpr>(&self, exprs: &'a [T]) -> ExprIRSliceDisplay<'a, T> {267ExprIRSliceDisplay {268exprs,269expr_arena: self.expr_arena,270}271}272273fn with_root<T: AsExpr>(&self, root: &'a T) -> Self {274Self {275node: root.node(),276output_name: root.output_name(),277expr_arena: self.expr_arena,278}279}280}281282impl Display for IRDisplay<'_> {283fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {284self._format(f, 0)285}286}287288impl fmt::Debug for IRDisplay<'_> {289fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {290Display::fmt(&self, f)291}292}293294impl<T: AsExpr> Display for ExprIRSliceDisplay<'_, T> {295fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {296// Display items in slice delimited by a comma297298use std::fmt::Write;299300let mut iter = self.exprs.iter();301302f.write_char('[')?;303if let Some(fst) = iter.next() {304let fst = ExprIRDisplay {305node: fst.node(),306output_name: fst.output_name(),307expr_arena: self.expr_arena,308};309write!(f, "{fst}")?;310}311312for expr in iter {313let expr = ExprIRDisplay {314node: expr.node(),315output_name: expr.output_name(),316expr_arena: self.expr_arena,317};318write!(f, ", {expr}")?;319}320321f.write_char(']')?;322323Ok(())324}325}326327impl<T: AsExpr> fmt::Debug for ExprIRSliceDisplay<'_, T> {328fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {329Display::fmt(self, f)330}331}332333impl Display for ExprIRDisplay<'_> {334#[recursive]335fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {336let root = self.expr_arena.get(self.node);337338use AExpr::*;339match root {340Window {341function,342partition_by,343order_by,344options,345} => {346let function = self.with_root(function);347let partition_by = self.with_slice(partition_by);348match options {349#[cfg(feature = "dynamic_group_by")]350WindowType::Rolling(options) => {351write!(352f,353"{function}.rolling(by='{}', offset={}, period={})",354options.index_column, options.offset, options.period355)356},357_ => {358if let Some((order_by, _)) = order_by {359let order_by = self.with_root(order_by);360write!(361f,362"{function}.over(partition_by: {partition_by}, order_by: {order_by})"363)364} else {365write!(f, "{function}.over({partition_by})")366}367},368}369},370Len => write!(f, "len()"),371Explode { expr, skip_empty } => {372let expr = self.with_root(expr);373if *skip_empty {374write!(f, "{expr}.explode(skip_empty)")375} else {376write!(f, "{expr}.explode()")377}378},379Column(name) => write!(f, "col(\"{name}\")"),380Literal(v) => write!(f, "{v:?}"),381BinaryExpr { left, op, right } => {382let left = self.with_root(left);383let right = self.with_root(right);384write!(f, "[({left}) {op:?} ({right})]")385},386Sort { expr, options } => {387let expr = self.with_root(expr);388if options.descending {389write!(f, "{expr}.sort(desc)")390} else {391write!(f, "{expr}.sort(asc)")392}393},394SortBy {395expr,396by,397sort_options,398} => {399let expr = self.with_root(expr);400let by = self.with_slice(by);401write!(f, "{expr}.sort_by(by={by}, sort_option={sort_options:?})",)402},403Filter { input, by } => {404let input = self.with_root(input);405let by = self.with_root(by);406407write!(f, "{input}.filter({by})")408},409Gather {410expr,411idx,412returns_scalar,413} => {414let expr = self.with_root(expr);415let idx = self.with_root(idx);416expr.fmt(f)?;417418if *returns_scalar {419write!(f, ".get({idx})")420} else {421write!(f, ".gather({idx})")422}423},424Agg(agg) => {425use IRAggExpr::*;426match agg {427Min {428input,429propagate_nans,430} => {431self.with_root(input).fmt(f)?;432if *propagate_nans {433write!(f, ".nan_min()")434} else {435write!(f, ".min()")436}437},438Max {439input,440propagate_nans,441} => {442self.with_root(input).fmt(f)?;443if *propagate_nans {444write!(f, ".nan_max()")445} else {446write!(f, ".max()")447}448},449Median(expr) => write!(f, "{}.median()", self.with_root(expr)),450Mean(expr) => write!(f, "{}.mean()", self.with_root(expr)),451First(expr) => write!(f, "{}.first()", self.with_root(expr)),452Last(expr) => write!(f, "{}.last()", self.with_root(expr)),453Implode(expr) => write!(f, "{}.implode()", self.with_root(expr)),454NUnique(expr) => write!(f, "{}.n_unique()", self.with_root(expr)),455Sum(expr) => write!(f, "{}.sum()", self.with_root(expr)),456AggGroups(expr) => write!(f, "{}.groups()", self.with_root(expr)),457Count {458input,459include_nulls: false,460} => write!(f, "{}.count()", self.with_root(input)),461Count {462input,463include_nulls: true,464} => write!(f, "{}.len()", self.with_root(input)),465Var(expr, _) => write!(f, "{}.var()", self.with_root(expr)),466Std(expr, _) => write!(f, "{}.std()", self.with_root(expr)),467Quantile {468expr,469quantile,470method,471} => write!(472f,473"{}.quantile({}, interpolation='{}')",474self.with_root(expr),475self.with_root(quantile),476<&'static str>::from(method),477),478}479},480Cast {481expr,482dtype,483options,484} => {485self.with_root(expr).fmt(f)?;486if options.is_strict() {487write!(f, ".strict_cast({dtype:?})")488} else {489write!(f, ".cast({dtype:?})")490}491},492Ternary {493predicate,494truthy,495falsy,496} => {497let predicate = self.with_root(predicate);498let truthy = self.with_root(truthy);499let falsy = self.with_root(falsy);500write!(f, "when({predicate}).then({truthy}).otherwise({falsy})",)501},502Function {503input, function, ..504} => {505let fst = self.with_root(&input[0]);506fst.fmt(f)?;507if input.len() >= 2 {508write!(f, ".{function}({})", self.with_slice(&input[1..]))509} else {510write!(f, ".{function}()")511}512},513AnonymousFunction { input, fmt_str, .. } => {514let fst = self.with_root(&input[0]);515fst.fmt(f)?;516if input.len() >= 2 {517write!(f, ".{fmt_str}({})", self.with_slice(&input[1..]))518} else {519write!(f, ".{fmt_str}()")520}521},522Eval {523expr,524evaluation,525variant,526} => {527let expr = self.with_root(expr);528let evaluation = self.with_root(evaluation);529match variant {530EvalVariant::List => write!(f, "{expr}.list.eval({evaluation})"),531EvalVariant::Cumulative { min_samples } => write!(532f,533"{expr}.cumulative_eval({evaluation}, min_samples={min_samples})"534),535}536},537Slice {538input,539offset,540length,541} => {542let input = self.with_root(input);543let offset = self.with_root(offset);544let length = self.with_root(length);545546write!(f, "{input}.slice(offset={offset}, length={length})")547},548}?;549550match self.output_name {551OutputName::None => {},552OutputName::LiteralLhs(_) => {},553OutputName::ColumnLhs(_) => {},554#[cfg(feature = "dtype-struct")]555OutputName::Field(_) => {},556OutputName::Alias(name) => {557if root.to_name(self.expr_arena) != name {558write!(f, r#".alias("{name}")"#)?;559}560},561}562563Ok(())564}565}566567impl fmt::Debug for ExprIRDisplay<'_> {568fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {569Display::fmt(self, f)570}571}572573pub(crate) struct ColumnsDisplay<'a>(pub(crate) &'a Schema);574575impl fmt::Display for ColumnsDisplay<'_> {576fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {577let len = self.0.len();578let mut iter_names = self.0.iter_names().enumerate();579580const MAX_LEN: usize = 32;581const ADD_PER_ITEM: usize = 4;582583let mut current_len = 0;584585if let Some((_, fst)) = iter_names.next() {586write!(f, "\"{fst}\"")?;587588current_len += fst.len() + ADD_PER_ITEM;589}590591for (i, col) in iter_names {592current_len += col.len() + ADD_PER_ITEM;593594if current_len > MAX_LEN {595write!(f, ", ... {} other ", len - i)?;596if len - i == 1 {597f.write_str("column")?;598} else {599f.write_str("columns")?;600}601602break;603}604605write!(f, ", \"{col}\"")?;606}607608Ok(())609}610}611612impl fmt::Debug for Operator {613fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {614Display::fmt(self, f)615}616}617618impl fmt::Debug for LiteralValue {619fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {620use LiteralValue::*;621622match self {623Self::Scalar(sc) => write!(f, "{}", sc.value()),624Self::Series(s) => {625let name = s.name();626if name.is_empty() {627write!(f, "Series")628} else {629write!(f, "Series[{name}]")630}631},632Range(range) => fmt::Debug::fmt(range, f),633Dyn(d) => fmt::Debug::fmt(d, f),634}635}636}637638impl fmt::Debug for DynLiteralValue {639fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {640match self {641Self::Int(v) => write!(f, "dyn int: {v}"),642Self::Float(v) => write!(f, "dyn float: {v}"),643Self::Str(v) => write!(f, "dyn str: {v}"),644Self::List(_) => todo!(),645}646}647}648649impl fmt::Debug for RangeLiteralValue {650fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {651write!(f, "range({}, {})", self.low, self.high)652}653}654655pub fn write_ir_non_recursive(656f: &mut dyn fmt::Write,657ir: &IR,658expr_arena: &Arena<AExpr>,659output_schema: &Schema,660indent: usize,661) -> fmt::Result {662match ir {663#[cfg(feature = "python")]664IR::PythonScan { options } => {665let total_columns = options.schema.len();666let n_columns = options667.with_columns668.as_ref()669.map(|s| s.len() as i64)670.unwrap_or(-1);671672let predicate = match &options.predicate {673PythonPredicate::Polars(e) => Some(e.display(expr_arena)),674PythonPredicate::PyArrow(_) => None,675PythonPredicate::None => None,676};677678write_scan(679f,680"PYTHON",681&ScanSources::default(),682indent,683n_columns,684total_columns,685&predicate,686options687.n_rows688.map(|len| polars_utils::slice_enum::Slice::Positive { offset: 0, len }),689None,690None,691)692},693IR::Slice {694input: _,695offset,696len,697} => {698write!(f, "{:indent$}SLICE[offset: {offset}, len: {len}]", "")699},700IR::Filter {701input: _,702predicate,703} => {704let predicate = predicate.display(expr_arena);705// this one is writeln because we don't increase indent (which inserts a line)706write!(f, "{:indent$}FILTER {predicate}", "")?;707write!(f, "\n{:indent$}FROM", "")708},709IR::Scan {710sources,711file_info,712predicate,713scan_type,714unified_scan_args,715hive_parts: _,716output_schema: _,717} => {718let n_columns = unified_scan_args719.projection720.as_ref()721.map(|columns| columns.len() as i64)722.unwrap_or(-1);723724let predicate = predicate.as_ref().map(|p| p.display(expr_arena));725726write_scan(727f,728(&**scan_type).into(),729sources,730indent,731n_columns,732file_info.schema.len(),733&predicate,734unified_scan_args.pre_slice.clone(),735unified_scan_args.row_index.as_ref(),736unified_scan_args.deletion_files.as_ref(),737)738},739IR::DataFrameScan {740df: _,741schema,742output_schema,743} => {744let total_columns = schema.len();745let (n_columns, projected) = if let Some(schema) = output_schema {746(747format!("{}", schema.len()),748format_list_truncated!(schema.iter_names(), 4, '"'),749)750} else {751("*".to_string(), "".to_string())752};753write!(754f,755"{:indent$}DF {}; PROJECT{} {}/{} COLUMNS",756"",757format_list_truncated!(schema.iter_names(), 4, '"'),758projected,759n_columns,760total_columns,761)762},763IR::SimpleProjection { input: _, columns } => {764let num_columns = columns.as_ref().len();765let total_columns = output_schema.len();766767let columns = ColumnsDisplay(columns.as_ref());768write!(769f,770"{:indent$}simple π {num_columns}/{total_columns} [{columns}]",771""772)773},774IR::Select {775input: _,776expr,777schema: _,778options: _,779} => {780// @NOTE: Maybe there should be a clear delimiter here?781let exprs = ExprIRSliceDisplay {782exprs: expr,783expr_arena,784};785write!(f, "{:indent$}SELECT {exprs}", "")?;786Ok(())787},788IR::Sort {789input: _,790by_column,791slice: _,792sort_options: _,793} => {794let by_column = ExprIRSliceDisplay {795exprs: by_column,796expr_arena,797};798write!(f, "{:indent$}SORT BY {by_column}", "")799},800IR::Cache { input: _, id } => write!(f, "{:indent$}CACHE[id: {id}]", ""),801IR::GroupBy {802input: _,803keys,804aggs,805schema: _,806maintain_order,807options: _,808apply,809} => write_group_by(810f,811indent,812expr_arena,813keys,814aggs,815apply.as_ref(),816*maintain_order,817),818IR::Join {819input_left: _,820input_right: _,821schema: _,822left_on,823right_on,824options,825} => {826let left_on = ExprIRSliceDisplay {827exprs: left_on,828expr_arena,829};830let right_on = ExprIRSliceDisplay {831exprs: right_on,832expr_arena,833};834835// Fused cross + filter (show as nested loop join)836if let Some(JoinTypeOptionsIR::CrossAndFilter { predicate }) = &options.options {837let predicate = predicate.display(expr_arena);838write!(f, "{:indent$}NESTED_LOOP JOIN ON {predicate}", "")?;839} else {840let how = &options.args.how;841write!(f, "{:indent$}{how} JOIN", "")?;842write!(f, "\n{:indent$}LEFT PLAN ON: {left_on}", "")?;843write!(f, "\n{:indent$}RIGHT PLAN ON: {right_on}", "")?;844}845846Ok(())847},848IR::HStack {849input: _,850exprs,851schema: _,852options: _,853} => {854// @NOTE: Maybe there should be a clear delimiter here?855let exprs = ExprIRSliceDisplay { exprs, expr_arena };856857write!(f, "{:indent$} WITH_COLUMNS:", "",)?;858write!(f, "\n{:indent$} {exprs} ", "")859},860IR::Distinct { input: _, options } => {861write!(862f,863"{:indent$}UNIQUE[maintain_order: {:?}, keep_strategy: {:?}] BY {:?}",864"", options.maintain_order, options.keep_strategy, options.subset865)866},867IR::MapFunction { input: _, function } => write!(f, "{:indent$}{function}", ""),868IR::Union { inputs: _, options } => {869let name = if let Some(slice) = options.slice {870format!("SLICED UNION: {slice:?}")871} else {872"UNION".to_string()873};874write!(f, "{:indent$}{name}", "")875},876IR::HConcat {877inputs: _,878schema: _,879options: _,880} => write!(f, "{:indent$}HCONCAT", ""),881IR::ExtContext {882input: _,883contexts: _,884schema: _,885} => write!(f, "{:indent$}EXTERNAL_CONTEXT", ""),886IR::Sink { input: _, payload } => {887let name = match payload {888SinkTypeIR::Memory => "SINK (memory)",889SinkTypeIR::File { .. } => "SINK (file)",890SinkTypeIR::Partition { .. } => "SINK (partition)",891};892write!(f, "{:indent$}{name}", "")893},894IR::SinkMultiple { inputs: _ } => write!(f, "{:indent$}SINK_MULTIPLE", ""),895#[cfg(feature = "merge_sorted")]896IR::MergeSorted {897input_left: _,898input_right: _,899key,900} => write!(f, "{:indent$}MERGE SORTED ON '{key}'", ""),901IR::Invalid => write!(f, "{:indent$}INVALID", ""),902}903}904905pub fn write_group_by(906f: &mut dyn fmt::Write,907indent: usize,908expr_arena: &Arena<AExpr>,909keys: &[ExprIR],910aggs: &[ExprIR],911apply: Option<&PlanCallback<DataFrame, DataFrame>>,912maintain_order: bool,913) -> fmt::Result {914let sub_indent = indent + INDENT_INCREMENT;915let keys = ExprIRSliceDisplay {916exprs: keys,917expr_arena,918};919write!(920f,921"{:indent$}AGGREGATE[maintain_order: {}]",922"", maintain_order923)?;924if apply.is_some() {925write!(f, "\n{:sub_indent$}MAP_GROUPS BY {keys}", "")?;926write!(f, "\n{:sub_indent$}FROM", "")?;927} else {928let aggs = ExprIRSliceDisplay {929exprs: aggs,930expr_arena,931};932write!(f, "\n{:sub_indent$}{aggs} BY {keys}", "")?;933}934935Ok(())936}937938939