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