1use std::fmt::{self, Display, Formatter};
2
3use polars_core::schema::Schema;
4use polars_io::RowIndex;
5use polars_utils::format_list_truncated;
6use polars_utils::slice_enum::Slice;
7use recursive::recursive;
8
9use self::ir::dot::ScanSourcesDisplay;
10use crate::prelude::*;
11
12const INDENT_INCREMENT: usize = 2;
13
14pub struct IRDisplay<'a> {
15 is_streaming: bool,
16 lp: IRPlanRef<'a>,
17}
18
19#[derive(Clone, Copy)]
20pub struct ExprIRDisplay<'a> {
21 pub(crate) node: Node,
22 pub(crate) output_name: &'a OutputName,
23 pub(crate) expr_arena: &'a Arena<AExpr>,
24}
25
26impl<'a> ExprIRDisplay<'a> {
27 pub fn display_node(node: Node, expr_arena: &'a Arena<AExpr>) -> Self {
28 Self {
29 node,
30 output_name: &OutputName::None,
31 expr_arena,
32 }
33 }
34}
35
36pub(crate) struct ExprIRSliceDisplay<'a, T: AsExpr> {
38 pub(crate) exprs: &'a [T],
39 pub(crate) expr_arena: &'a Arena<AExpr>,
40}
41
42pub(crate) trait AsExpr {
43 fn node(&self) -> Node;
44 fn output_name(&self) -> &OutputName;
45}
46
47impl AsExpr for Node {
48 fn node(&self) -> Node {
49 *self
50 }
51 fn output_name(&self) -> &OutputName {
52 &OutputName::None
53 }
54}
55
56impl AsExpr for ExprIR {
57 fn node(&self) -> Node {
58 self.node()
59 }
60 fn output_name(&self) -> &OutputName {
61 self.output_name_inner()
62 }
63}
64
65#[allow(clippy::too_many_arguments)]
66fn write_scan(
67 f: &mut dyn fmt::Write,
68 name: &str,
69 sources: &ScanSources,
70 indent: usize,
71 n_columns: i64,
72 total_columns: usize,
73 predicate: &Option<ExprIRDisplay<'_>>,
74 pre_slice: Option<Slice>,
75 row_index: Option<&RowIndex>,
76) -> fmt::Result {
77 write!(
78 f,
79 "{:indent$}{name} SCAN {}",
80 "",
81 ScanSourcesDisplay(sources)
82 )?;
83
84 let total_columns = total_columns - usize::from(row_index.is_some());
85 if n_columns > 0 {
86 write!(
87 f,
88 "\n{:indent$}PROJECT {n_columns}/{total_columns} COLUMNS",
89 "",
90 )?;
91 } else {
92 write!(f, "\n{:indent$}PROJECT */{total_columns} COLUMNS", "")?;
93 }
94 if let Some(predicate) = predicate {
95 write!(f, "\n{:indent$}SELECTION: {predicate}", "")?;
96 }
97 if let Some(pre_slice) = pre_slice {
98 write!(f, "\n{:indent$}SLICE: {pre_slice:?}", "")?;
99 }
100 if let Some(row_index) = row_index {
101 write!(f, "\n{:indent$}ROW_INDEX: {}", "", row_index.name)?;
102 if row_index.offset != 0 {
103 write!(f, " (offset: {})", row_index.offset)?;
104 }
105 }
106 Ok(())
107}
108
109impl<'a> IRDisplay<'a> {
110 pub fn new(lp: IRPlanRef<'a>) -> Self {
111 if let Some(streaming_lp) = lp.extract_streaming_plan() {
112 return Self::new_streaming(streaming_lp);
113 }
114
115 Self {
116 is_streaming: false,
117 lp,
118 }
119 }
120
121 fn new_streaming(lp: IRPlanRef<'a>) -> Self {
122 Self {
123 is_streaming: true,
124 lp,
125 }
126 }
127
128 fn root(&self) -> &IR {
129 self.lp.root()
130 }
131
132 fn with_root(&self, root: Node) -> Self {
133 Self {
134 is_streaming: false,
135 lp: self.lp.with_root(root),
136 }
137 }
138
139 fn display_expr(&self, root: &'a ExprIR) -> ExprIRDisplay<'a> {
140 ExprIRDisplay {
141 node: root.node(),
142 output_name: root.output_name_inner(),
143 expr_arena: self.lp.expr_arena,
144 }
145 }
146
147 fn display_expr_slice(&self, exprs: &'a [ExprIR]) -> ExprIRSliceDisplay<'a, ExprIR> {
148 ExprIRSliceDisplay {
149 exprs,
150 expr_arena: self.lp.expr_arena,
151 }
152 }
153
154 #[recursive]
155 fn _format(&self, f: &mut Formatter, indent: usize) -> fmt::Result {
156 let indent = if self.is_streaming {
157 writeln!(f, "{:indent$}STREAMING:", "")?;
158 indent + INDENT_INCREMENT
159 } else {
160 if indent != 0 {
161 writeln!(f)?;
162 }
163 indent
164 };
165
166 let sub_indent = indent + INDENT_INCREMENT;
167 use IR::*;
168
169 let ir_node = self.root();
170 let schema = ir_node.schema(self.lp.lp_arena);
171 let schema = schema.as_ref();
172 match ir_node {
173 Union { inputs, options } => {
174 write_ir_non_recursive(f, ir_node, self.lp.expr_arena, schema, indent)?;
175 let name = if let Some(slice) = options.slice {
176 format!("SLICED UNION: {slice:?}")
177 } else {
178 "UNION".to_string()
179 };
180
181 let sub_sub_indent = sub_indent + INDENT_INCREMENT;
186 for (i, plan) in inputs.iter().enumerate() {
187 write!(f, "\n{:sub_indent$}PLAN {i}:", "")?;
188 self.with_root(*plan)._format(f, sub_sub_indent)?;
189 }
190 write!(f, "\n{:indent$}END {name}", "")
191 },
192 HConcat { inputs, .. } => {
193 let sub_sub_indent = sub_indent + INDENT_INCREMENT;
194 write_ir_non_recursive(f, ir_node, self.lp.expr_arena, schema, indent)?;
195 for (i, plan) in inputs.iter().enumerate() {
196 write!(f, "\n{:sub_indent$}PLAN {i}:", "")?;
197 self.with_root(*plan)._format(f, sub_sub_indent)?;
198 }
199 write!(f, "\n{:indent$}END HCONCAT", "")
200 },
201 GroupBy { input, .. } => {
202 write_ir_non_recursive(f, ir_node, self.lp.expr_arena, schema, indent)?;
203 write!(f, "\n{:sub_indent$}FROM", "")?;
204 self.with_root(*input)._format(f, sub_indent)?;
205 Ok(())
206 },
207 Join {
208 input_left,
209 input_right,
210 left_on,
211 right_on,
212 options,
213 ..
214 } => {
215 let left_on = self.display_expr_slice(left_on);
216 let right_on = self.display_expr_slice(right_on);
217
218 if let Some(JoinTypeOptionsIR::Cross { predicate }) = &options.options {
220 let predicate = self.display_expr(predicate);
221 let name = "NESTED LOOP";
222 write!(f, "{:indent$}{name} JOIN ON {predicate}:", "")?;
223 write!(f, "\n{:indent$}LEFT PLAN:", "")?;
224 self.with_root(*input_left)._format(f, sub_indent)?;
225 write!(f, "\n{:indent$}RIGHT PLAN:", "")?;
226 self.with_root(*input_right)._format(f, sub_indent)?;
227 write!(f, "\n{:indent$}END {name} JOIN", "")
228 } else {
229 let how = &options.args.how;
230 write!(f, "{:indent$}{how} JOIN:", "")?;
231 write!(f, "\n{:indent$}LEFT PLAN ON: {left_on}", "")?;
232 self.with_root(*input_left)._format(f, sub_indent)?;
233 write!(f, "\n{:indent$}RIGHT PLAN ON: {right_on}", "")?;
234 self.with_root(*input_right)._format(f, sub_indent)?;
235 write!(f, "\n{:indent$}END {how} JOIN", "")
236 }
237 },
238 MapFunction {
239 input, function, ..
240 } => {
241 if let Some(streaming_lp) = function.to_streaming_lp() {
242 IRDisplay::new_streaming(streaming_lp)._format(f, indent)
243 } else {
244 write_ir_non_recursive(f, ir_node, self.lp.expr_arena, schema, indent)?;
245 self.with_root(*input)._format(f, sub_indent)
246 }
247 },
248 SinkMultiple { inputs } => {
249 write_ir_non_recursive(f, ir_node, self.lp.expr_arena, schema, indent)?;
250
251 let sub_sub_indent = sub_indent + 2;
256 for (i, plan) in inputs.iter().enumerate() {
257 write!(f, "\n{:sub_indent$}PLAN {i}:", "")?;
258 self.with_root(*plan)._format(f, sub_sub_indent)?;
259 }
260 write!(f, "\n{:indent$}END SINK_MULTIPLE", "")
261 },
262 #[cfg(feature = "merge_sorted")]
263 MergeSorted {
264 input_left,
265 input_right,
266 key: _,
267 } => {
268 write_ir_non_recursive(f, ir_node, self.lp.expr_arena, schema, indent)?;
269 write!(f, ":")?;
270
271 write!(f, "\n{:indent$}LEFT PLAN:", "")?;
272 self.with_root(*input_left)._format(f, sub_indent)?;
273 write!(f, "\n{:indent$}RIGHT PLAN:", "")?;
274 self.with_root(*input_right)._format(f, sub_indent)?;
275 write!(f, "\n{:indent$}END MERGE_SORTED", "")
276 },
277 ir_node => {
278 write_ir_non_recursive(f, ir_node, self.lp.expr_arena, schema, indent)?;
279 for input in ir_node.get_inputs().iter() {
280 self.with_root(*input)._format(f, sub_indent)?;
281 }
282 Ok(())
283 },
284 }
285 }
286}
287
288impl<'a> ExprIRDisplay<'a> {
289 fn with_slice<T: AsExpr>(&self, exprs: &'a [T]) -> ExprIRSliceDisplay<'a, T> {
290 ExprIRSliceDisplay {
291 exprs,
292 expr_arena: self.expr_arena,
293 }
294 }
295
296 fn with_root<T: AsExpr>(&self, root: &'a T) -> Self {
297 Self {
298 node: root.node(),
299 output_name: root.output_name(),
300 expr_arena: self.expr_arena,
301 }
302 }
303}
304
305impl Display for IRDisplay<'_> {
306 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
307 self._format(f, 0)
308 }
309}
310
311impl fmt::Debug for IRDisplay<'_> {
312 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
313 Display::fmt(&self, f)
314 }
315}
316
317impl<T: AsExpr> Display for ExprIRSliceDisplay<'_, T> {
318 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
319 use std::fmt::Write;
322
323 let mut iter = self.exprs.iter();
324
325 f.write_char('[')?;
326 if let Some(fst) = iter.next() {
327 let fst = ExprIRDisplay {
328 node: fst.node(),
329 output_name: fst.output_name(),
330 expr_arena: self.expr_arena,
331 };
332 write!(f, "{fst}")?;
333 }
334
335 for expr in iter {
336 let expr = ExprIRDisplay {
337 node: expr.node(),
338 output_name: expr.output_name(),
339 expr_arena: self.expr_arena,
340 };
341 write!(f, ", {expr}")?;
342 }
343
344 f.write_char(']')?;
345
346 Ok(())
347 }
348}
349
350impl<T: AsExpr> fmt::Debug for ExprIRSliceDisplay<'_, T> {
351 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
352 Display::fmt(self, f)
353 }
354}
355
356impl Display for ExprIRDisplay<'_> {
357 #[recursive]
358 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
359 let root = self.expr_arena.get(self.node);
360
361 use AExpr::*;
362 match root {
363 Window {
364 function,
365 partition_by,
366 order_by,
367 options,
368 } => {
369 let function = self.with_root(function);
370 let partition_by = self.with_slice(partition_by);
371 match options {
372 #[cfg(feature = "dynamic_group_by")]
373 WindowType::Rolling(options) => {
374 write!(
375 f,
376 "{function}.rolling(by='{}', offset={}, period={})",
377 options.index_column, options.offset, options.period
378 )
379 },
380 _ => {
381 if let Some((order_by, _)) = order_by {
382 let order_by = self.with_root(order_by);
383 write!(
384 f,
385 "{function}.over(partition_by: {partition_by}, order_by: {order_by})"
386 )
387 } else {
388 write!(f, "{function}.over({partition_by})")
389 }
390 },
391 }
392 },
393 Len => write!(f, "len()"),
394 Explode { expr, skip_empty } => {
395 let expr = self.with_root(expr);
396 if *skip_empty {
397 write!(f, "{expr}.explode(skip_empty)")
398 } else {
399 write!(f, "{expr}.explode()")
400 }
401 },
402 Alias(expr, name) => {
403 let expr = self.with_root(expr);
404 write!(f, "{expr}.alias(\"{name}\")")
405 },
406 Column(name) => write!(f, "col(\"{name}\")"),
407 Literal(v) => write!(f, "{v:?}"),
408 BinaryExpr { left, op, right } => {
409 let left = self.with_root(left);
410 let right = self.with_root(right);
411 write!(f, "[({left}) {op:?} ({right})]")
412 },
413 Sort { expr, options } => {
414 let expr = self.with_root(expr);
415 if options.descending {
416 write!(f, "{expr}.sort(desc)")
417 } else {
418 write!(f, "{expr}.sort(asc)")
419 }
420 },
421 SortBy {
422 expr,
423 by,
424 sort_options,
425 } => {
426 let expr = self.with_root(expr);
427 let by = self.with_slice(by);
428 write!(f, "{expr}.sort_by(by={by}, sort_option={sort_options:?})",)
429 },
430 Filter { input, by } => {
431 let input = self.with_root(input);
432 let by = self.with_root(by);
433
434 write!(f, "{input}.filter({by})")
435 },
436 Gather {
437 expr,
438 idx,
439 returns_scalar,
440 } => {
441 let expr = self.with_root(expr);
442 let idx = self.with_root(idx);
443 expr.fmt(f)?;
444
445 if *returns_scalar {
446 write!(f, ".get({idx})")
447 } else {
448 write!(f, ".gather({idx})")
449 }
450 },
451 Agg(agg) => {
452 use IRAggExpr::*;
453 match agg {
454 Min {
455 input,
456 propagate_nans,
457 } => {
458 self.with_root(input).fmt(f)?;
459 if *propagate_nans {
460 write!(f, ".nan_min()")
461 } else {
462 write!(f, ".min()")
463 }
464 },
465 Max {
466 input,
467 propagate_nans,
468 } => {
469 self.with_root(input).fmt(f)?;
470 if *propagate_nans {
471 write!(f, ".nan_max()")
472 } else {
473 write!(f, ".max()")
474 }
475 },
476 Median(expr) => write!(f, "{}.median()", self.with_root(expr)),
477 Mean(expr) => write!(f, "{}.mean()", self.with_root(expr)),
478 First(expr) => write!(f, "{}.first()", self.with_root(expr)),
479 Last(expr) => write!(f, "{}.last()", self.with_root(expr)),
480 Implode(expr) => write!(f, "{}.implode()", self.with_root(expr)),
481 NUnique(expr) => write!(f, "{}.n_unique()", self.with_root(expr)),
482 Sum(expr) => write!(f, "{}.sum()", self.with_root(expr)),
483 AggGroups(expr) => write!(f, "{}.groups()", self.with_root(expr)),
484 Count(expr, _) => write!(f, "{}.count()", self.with_root(expr)),
485 Var(expr, _) => write!(f, "{}.var()", self.with_root(expr)),
486 Std(expr, _) => write!(f, "{}.std()", self.with_root(expr)),
487 Quantile { expr, .. } => write!(f, "{}.quantile()", self.with_root(expr)),
488 }
489 },
490 Cast {
491 expr,
492 dtype,
493 options,
494 } => {
495 self.with_root(expr).fmt(f)?;
496 if options.is_strict() {
497 write!(f, ".strict_cast({dtype:?})")
498 } else {
499 write!(f, ".cast({dtype:?})")
500 }
501 },
502 Ternary {
503 predicate,
504 truthy,
505 falsy,
506 } => {
507 let predicate = self.with_root(predicate);
508 let truthy = self.with_root(truthy);
509 let falsy = self.with_root(falsy);
510 write!(f, "when({predicate}).then({truthy}).otherwise({falsy})",)
511 },
512 Function {
513 input, function, ..
514 } => {
515 let fst = self.with_root(&input[0]);
516 fst.fmt(f)?;
517 if input.len() >= 2 {
518 write!(f, ".{function}({})", self.with_slice(&input[1..]))
519 } else {
520 write!(f, ".{function}()")
521 }
522 },
523 AnonymousFunction { input, options, .. } => {
524 let fst = self.with_root(&input[0]);
525 fst.fmt(f)?;
526 if input.len() >= 2 {
527 write!(f, ".{}({})", options.fmt_str, self.with_slice(&input[1..]))
528 } else {
529 write!(f, ".{}()", options.fmt_str)
530 }
531 },
532 Slice {
533 input,
534 offset,
535 length,
536 } => {
537 let input = self.with_root(input);
538 let offset = self.with_root(offset);
539 let length = self.with_root(length);
540
541 write!(f, "{input}.slice(offset={offset}, length={length})")
542 },
543 }?;
544
545 match self.output_name {
546 OutputName::None => {},
547 OutputName::LiteralLhs(_) => {},
548 OutputName::ColumnLhs(_) => {},
549 #[cfg(feature = "dtype-struct")]
550 OutputName::Field(_) => {},
551 OutputName::Alias(name) => write!(f, r#".alias("{name}")"#)?,
552 }
553
554 Ok(())
555 }
556}
557
558impl fmt::Debug for ExprIRDisplay<'_> {
559 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
560 Display::fmt(self, f)
561 }
562}
563
564pub(crate) struct ColumnsDisplay<'a>(pub(crate) &'a Schema);
565
566impl fmt::Display for ColumnsDisplay<'_> {
567 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
568 let len = self.0.len();
569 let mut iter_names = self.0.iter_names().enumerate();
570
571 const MAX_LEN: usize = 32;
572 const ADD_PER_ITEM: usize = 4;
573
574 let mut current_len = 0;
575
576 if let Some((_, fst)) = iter_names.next() {
577 write!(f, "\"{fst}\"")?;
578
579 current_len += fst.len() + ADD_PER_ITEM;
580 }
581
582 for (i, col) in iter_names {
583 current_len += col.len() + ADD_PER_ITEM;
584
585 if current_len > MAX_LEN {
586 write!(f, ", ... {} other ", len - i)?;
587 if len - i == 1 {
588 f.write_str("column")?;
589 } else {
590 f.write_str("columns")?;
591 }
592
593 break;
594 }
595
596 write!(f, ", \"{col}\"")?;
597 }
598
599 Ok(())
600 }
601}
602
603impl fmt::Debug for Operator {
604 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
605 Display::fmt(self, f)
606 }
607}
608
609impl fmt::Debug for LiteralValue {
610 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
611 use LiteralValue::*;
612
613 match self {
614 Self::Scalar(sc) => write!(f, "{}", sc.value()),
615 Self::Series(s) => {
616 let name = s.name();
617 if name.is_empty() {
618 write!(f, "Series")
619 } else {
620 write!(f, "Series[{name}]")
621 }
622 },
623 Range(range) => fmt::Debug::fmt(range, f),
624 Dyn(d) => fmt::Debug::fmt(d, f),
625 }
626 }
627}
628
629impl fmt::Debug for DynLiteralValue {
630 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
631 match self {
632 Self::Int(v) => write!(f, "dyn int: {v}"),
633 Self::Float(v) => write!(f, "dyn float: {}", v),
634 Self::Str(v) => write!(f, "dyn str: {v}"),
635 Self::List(_) => todo!(),
636 }
637 }
638}
639
640impl fmt::Debug for RangeLiteralValue {
641 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
642 write!(f, "range({}, {})", self.low, self.high)
643 }
644}
645
646pub fn write_ir_non_recursive(
647 f: &mut dyn fmt::Write,
648 ir: &IR,
649 expr_arena: &Arena<AExpr>,
650 schema: &Schema,
651 indent: usize,
652) -> fmt::Result {
653 match ir {
654 #[cfg(feature = "python")]
655 IR::PythonScan { options } => {
656 let total_columns = options.schema.len();
657 let n_columns = options
658 .with_columns
659 .as_ref()
660 .map(|s| s.len() as i64)
661 .unwrap_or(-1);
662
663 let predicate = match &options.predicate {
664 PythonPredicate::Polars(e) => Some(e.display(expr_arena)),
665 PythonPredicate::PyArrow(_) => None,
666 PythonPredicate::None => None,
667 };
668
669 write_scan(
670 f,
671 "PYTHON",
672 &ScanSources::default(),
673 indent,
674 n_columns,
675 total_columns,
676 &predicate,
677 options
678 .n_rows
679 .map(|len| polars_utils::slice_enum::Slice::Positive { offset: 0, len }),
680 None,
681 )
682 },
683 IR::Slice {
684 input: _,
685 offset,
686 len,
687 } => {
688 write!(f, "{:indent$}SLICE[offset: {offset}, len: {len}]", "")
689 },
690 IR::Filter {
691 input: _,
692 predicate,
693 } => {
694 let predicate = predicate.display(expr_arena);
695 write!(f, "{:indent$}FILTER {predicate}", "")?;
697 write!(f, "\n{:indent$}FROM", "")
698 },
699 IR::Scan {
700 sources,
701 file_info,
702 predicate,
703 scan_type,
704 unified_scan_args,
705 hive_parts: _,
706 output_schema: _,
707 } => {
708 let n_columns = unified_scan_args
709 .projection
710 .as_ref()
711 .map(|columns| columns.len() as i64)
712 .unwrap_or(-1);
713
714 let predicate = predicate.as_ref().map(|p| p.display(expr_arena));
715
716 write_scan(
717 f,
718 (&**scan_type).into(),
719 sources,
720 indent,
721 n_columns,
722 file_info.schema.len(),
723 &predicate,
724 unified_scan_args.pre_slice.clone(),
725 unified_scan_args.row_index.as_ref(),
726 )
727 },
728 IR::DataFrameScan {
729 df: _,
730 schema,
731 output_schema,
732 } => {
733 let total_columns = schema.len();
734 let (n_columns, projected) = if let Some(schema) = output_schema {
735 (
736 format!("{}", schema.len()),
737 format_list_truncated!(schema.iter_names(), 4, '"'),
738 )
739 } else {
740 ("*".to_string(), "".to_string())
741 };
742 write!(
743 f,
744 "{:indent$}DF {}; PROJECT{} {}/{} COLUMNS",
745 "",
746 format_list_truncated!(schema.iter_names(), 4, '"'),
747 projected,
748 n_columns,
749 total_columns,
750 )
751 },
752 IR::SimpleProjection { input: _, columns } => {
753 let num_columns = columns.as_ref().len();
754 let total_columns = schema.len();
755
756 let columns = ColumnsDisplay(columns.as_ref());
757 write!(
758 f,
759 "{:indent$}simple π {num_columns}/{total_columns} [{columns}]",
760 ""
761 )
762 },
763 IR::Select {
764 input: _,
765 expr,
766 schema: _,
767 options: _,
768 } => {
769 let exprs = ExprIRSliceDisplay {
771 exprs: expr,
772 expr_arena,
773 };
774 write!(f, "{:indent$}SELECT {exprs}", "")?;
775 Ok(())
776 },
777 IR::Sort {
778 input: _,
779 by_column,
780 slice: _,
781 sort_options: _,
782 } => {
783 let by_column = ExprIRSliceDisplay {
784 exprs: by_column,
785 expr_arena,
786 };
787 write!(f, "{:indent$}SORT BY {by_column}", "")
788 },
789 IR::Cache {
790 input: _,
791 id,
792 cache_hits,
793 } => write!(
794 f,
795 "{:indent$}CACHE[id: {:x}, cache_hits: {}]",
796 "", *id, *cache_hits
797 ),
798 IR::GroupBy {
799 input: _,
800 keys,
801 aggs,
802 schema: _,
803 maintain_order: _,
804 options: _,
805 apply,
806 } => write_group_by(f, indent, expr_arena, keys, aggs, apply.as_deref()),
807 IR::Join {
808 input_left: _,
809 input_right: _,
810 schema: _,
811 left_on,
812 right_on,
813 options,
814 } => {
815 let left_on = ExprIRSliceDisplay {
816 exprs: left_on,
817 expr_arena,
818 };
819 let right_on = ExprIRSliceDisplay {
820 exprs: right_on,
821 expr_arena,
822 };
823
824 if let Some(JoinTypeOptionsIR::Cross { predicate }) = &options.options {
826 let predicate = predicate.display(expr_arena);
827 write!(f, "{:indent$}NESTED_LOOP JOIN ON {predicate}", "")?;
828 } else {
829 let how = &options.args.how;
830 write!(f, "{:indent$}{how} JOIN", "")?;
831 write!(f, "\n{:indent$}LEFT PLAN ON: {left_on}", "")?;
832 write!(f, "\n{:indent$}RIGHT PLAN ON: {right_on}", "")?;
833 }
834
835 Ok(())
836 },
837 IR::HStack {
838 input: _,
839 exprs,
840 schema: _,
841 options: _,
842 } => {
843 let exprs = ExprIRSliceDisplay { exprs, expr_arena };
845
846 write!(f, "{:indent$} WITH_COLUMNS:", "",)?;
847 write!(f, "\n{:indent$} {exprs} ", "")
848 },
849 IR::Distinct { input: _, options } => {
850 write!(
851 f,
852 "{:indent$}UNIQUE[maintain_order: {:?}, keep_strategy: {:?}] BY {:?}",
853 "", options.maintain_order, options.keep_strategy, options.subset
854 )
855 },
856 IR::MapFunction { input: _, function } => write!(f, "{:indent$}{function}", ""),
857 IR::Union { inputs: _, options } => {
858 let name = if let Some(slice) = options.slice {
859 format!("SLICED UNION: {slice:?}")
860 } else {
861 "UNION".to_string()
862 };
863 write!(f, "{:indent$}{name}", "")
864 },
865 IR::HConcat {
866 inputs: _,
867 schema: _,
868 options: _,
869 } => write!(f, "{:indent$}HCONCAT", ""),
870 IR::ExtContext {
871 input: _,
872 contexts: _,
873 schema: _,
874 } => write!(f, "{:indent$}EXTERNAL_CONTEXT", ""),
875 IR::Sink { input: _, payload } => {
876 let name = match payload {
877 SinkTypeIR::Memory => "SINK (memory)",
878 SinkTypeIR::File { .. } => "SINK (file)",
879 SinkTypeIR::Partition { .. } => "SINK (partition)",
880 };
881 write!(f, "{:indent$}{name}", "")
882 },
883 IR::SinkMultiple { inputs: _ } => write!(f, "{:indent$}SINK_MULTIPLE", ""),
884 #[cfg(feature = "merge_sorted")]
885 IR::MergeSorted {
886 input_left: _,
887 input_right: _,
888 key,
889 } => write!(f, "{:indent$}MERGE SORTED ON '{key}'", ""),
890 IR::Invalid => write!(f, "{:indent$}INVALID", ""),
891 }
892}
893
894pub fn write_group_by(
895 f: &mut dyn fmt::Write,
896 indent: usize,
897 expr_arena: &Arena<AExpr>,
898 keys: &[ExprIR],
899 aggs: &[ExprIR],
900 apply: Option<&dyn DataFrameUdf>,
901) -> fmt::Result {
902 let sub_indent = indent + INDENT_INCREMENT;
903 let keys = ExprIRSliceDisplay {
904 exprs: keys,
905 expr_arena,
906 };
907 write!(f, "{:indent$}AGGREGATE", "")?;
908 if apply.is_some() {
909 write!(f, "\n{:sub_indent$}MAP_GROUPS BY {keys}", "")?;
910 write!(f, "\n{:sub_indent$}FROM", "")?;
911 } else {
912 let aggs = ExprIRSliceDisplay {
913 exprs: aggs,
914 expr_arena,
915 };
916 write!(f, "\n{:sub_indent$}{aggs} BY {keys}", "")?;
917 }
918
919 Ok(())
920}