Skip to main content

vortex_array/expr/
display.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt;
5use std::fmt::Display;
6use std::fmt::Formatter;
7
8use crate::expr::BoundExpression;
9use crate::expr::BoundKind;
10use crate::expr::Expression;
11use crate::expr::root;
12use crate::scalar_fn::ChildName;
13
14pub enum DisplayFormat {
15    Compact,
16    Tree,
17}
18
19/// Read-only expression-tree interface used by scalar functions for SQL-style formatting.
20///
21/// Both [`Expression`] and [`BoundExpression`] implement this interface, allowing scalar
22/// functions to format either representation without converting between them.
23pub trait ExprDisplay: Display {
24    /// Return the child at `index`.
25    fn display_child(&self, index: usize) -> &dyn ExprDisplay;
26
27    /// Return the number of children in this node.
28    fn display_children_count(&self) -> usize;
29}
30
31impl ExprDisplay for Expression {
32    fn display_child(&self, index: usize) -> &dyn ExprDisplay {
33        Expression::child(self, index)
34    }
35
36    fn display_children_count(&self) -> usize {
37        self.children().len()
38    }
39}
40
41impl ExprDisplay for BoundExpression {
42    fn display_child(&self, index: usize) -> &dyn ExprDisplay {
43        &self.children()[index]
44    }
45
46    fn display_children_count(&self) -> usize {
47        self.children().len()
48    }
49}
50
51trait DisplayTreeNode: Sized {
52    fn tree_children(&self) -> &[Self];
53
54    fn tree_child_name(&self, index: usize) -> ChildName;
55
56    fn fmt_tree_node(&self, f: &mut Formatter<'_>) -> fmt::Result;
57}
58
59impl DisplayTreeNode for Expression {
60    fn tree_children(&self) -> &[Self] {
61        Expression::children(self).as_slice()
62    }
63
64    fn tree_child_name(&self, index: usize) -> ChildName {
65        self.scalar_fn().signature().child_name(index)
66    }
67
68    fn fmt_tree_node(&self, f: &mut Formatter<'_>) -> fmt::Result {
69        Display::fmt(self.scalar_fn(), f)
70    }
71}
72
73impl DisplayTreeNode for BoundExpression {
74    fn tree_children(&self) -> &[Self] {
75        BoundExpression::children(self)
76    }
77
78    fn tree_child_name(&self, index: usize) -> ChildName {
79        match self.kind() {
80            BoundKind::Scalar { scalar_fn, .. } => scalar_fn.signature().child_name(index),
81            BoundKind::Root => unreachable!("the scope root has no children"),
82        }
83    }
84
85    fn fmt_tree_node(&self, f: &mut Formatter<'_>) -> fmt::Result {
86        match self.kind() {
87            BoundKind::Scalar { scalar_fn, .. } => Display::fmt(scalar_fn, f),
88            BoundKind::Root => Display::fmt(root().scalar_fn(), f),
89        }
90    }
91}
92
93struct NodeDisplay<'a, T>(&'a T);
94
95impl<T: DisplayTreeNode> Display for NodeDisplay<'_, T> {
96    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
97        self.0.fmt_tree_node(f)
98    }
99}
100
101pub struct DisplayTreeExpr<'a, T: ?Sized = Expression>(pub &'a T);
102
103impl<T: DisplayTreeNode> Display for DisplayTreeExpr<'_, T> {
104    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
105        pub use termtree::Tree;
106        fn make_tree<T: DisplayTreeNode>(expr: &T) -> Tree<String> {
107            let child_trees = expr
108                .tree_children()
109                .iter()
110                .enumerate()
111                .map(|(index, child)| {
112                    let child_tree = make_tree(child);
113                    Tree::new(format!(
114                        "{}: {}",
115                        expr.tree_child_name(index),
116                        child_tree.root
117                    ))
118                    .with_leaves(child_tree.leaves)
119                })
120                .collect::<Vec<_>>();
121
122            Tree::new(NodeDisplay(expr).to_string()).with_leaves(child_trees)
123        }
124
125        write!(f, "{}", make_tree(self.0))
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use crate::dtype::DType;
132    use crate::dtype::Nullability;
133    use crate::dtype::PType;
134    use crate::expr::and;
135    use crate::expr::between;
136    use crate::expr::cast;
137    use crate::expr::eq;
138    use crate::expr::get_item;
139    use crate::expr::gt;
140    use crate::expr::lit;
141    use crate::expr::not;
142    use crate::expr::pack;
143    use crate::expr::root;
144    use crate::expr::select;
145    use crate::expr::select_exclude;
146    use crate::scalar_fn::fns::between::BetweenOptions;
147    use crate::scalar_fn::fns::between::StrictComparison;
148
149    #[test]
150    fn tree_display_getitem() {
151        let expr = get_item("x", root());
152        println!("{}", expr.display_tree());
153    }
154
155    #[test]
156    fn tree_display_binary() {
157        let expr = gt(get_item("x", root()), lit(5));
158        println!("{}", expr.display_tree());
159    }
160
161    #[test]
162    fn test_child_names_debug() {
163        // Simple test to debug child names display
164        let binary_expr = gt(get_item("x", root()), lit(10));
165        println!("Binary expr tree:\n{}", binary_expr.display_tree());
166
167        let between_expr = between(
168            get_item("score", root()),
169            lit(0),
170            lit(100),
171            BetweenOptions {
172                lower_strict: StrictComparison::NonStrict,
173                upper_strict: StrictComparison::NonStrict,
174            },
175        );
176        println!("Between expr tree:\n{}", between_expr.display_tree());
177    }
178
179    #[test]
180    fn test_display_tree_root() {
181        use insta::assert_snapshot;
182        let root_expr = root();
183        assert_snapshot!(root_expr.display_tree().to_string(), @"vortex.root()");
184    }
185
186    #[test]
187    fn test_display_tree_literal() {
188        use insta::assert_snapshot;
189        let lit_expr = lit(42);
190        assert_snapshot!(lit_expr.display_tree().to_string(), @"vortex.literal(42i32)");
191    }
192
193    #[test]
194    fn test_display_tree_get_item() {
195        use insta::assert_snapshot;
196        let get_item_expr = get_item("my_field", root());
197        assert_snapshot!(get_item_expr.display_tree().to_string(), @r"
198        vortex.get_item(my_field)
199        └── input: vortex.root()
200        ");
201    }
202
203    #[test]
204    fn test_display_tree_binary() {
205        use insta::assert_snapshot;
206        let binary_expr = gt(get_item("x", root()), lit(10));
207        assert_snapshot!(binary_expr.display_tree().to_string(), @r"
208        vortex.binary(>)
209        ├── lhs: vortex.get_item(x)
210        │   └── input: vortex.root()
211        └── rhs: vortex.literal(10i32)
212        ");
213    }
214
215    #[test]
216    fn test_display_tree_complex_binary() {
217        use insta::assert_snapshot;
218        let complex_binary = and(
219            eq(get_item("name", root()), lit("alice")),
220            gt(get_item("age", root()), lit(18)),
221        );
222        assert_snapshot!(complex_binary.display_tree().to_string(), @r#"
223        vortex.binary(and)
224        ├── lhs: vortex.binary(=)
225        │   ├── lhs: vortex.get_item(name)
226        │   │   └── input: vortex.root()
227        │   └── rhs: vortex.literal("alice")
228        └── rhs: vortex.binary(>)
229            ├── lhs: vortex.get_item(age)
230            │   └── input: vortex.root()
231            └── rhs: vortex.literal(18i32)
232        "#);
233    }
234
235    #[test]
236    fn test_display_tree_select() {
237        use insta::assert_snapshot;
238        let select_expr = select(["name", "age"], root());
239        assert_snapshot!(select_expr.display_tree().to_string(), @r"
240        vortex.select({name, age})
241        └── child: vortex.root()
242        ");
243    }
244
245    #[test]
246    fn test_display_tree_select_exclude() {
247        use insta::assert_snapshot;
248        let select_exclude_expr = select_exclude(["internal_id", "metadata"], root());
249        assert_snapshot!(select_exclude_expr.display_tree().to_string(), @r"
250        vortex.select(~{internal_id, metadata})
251        └── child: vortex.root()
252        ");
253    }
254
255    #[test]
256    fn test_display_tree_cast() {
257        use insta::assert_snapshot;
258        let cast_expr = cast(
259            get_item("value", root()),
260            DType::Primitive(PType::I64, Nullability::NonNullable),
261        );
262        assert_snapshot!(cast_expr.display_tree().to_string(), @r"
263        vortex.cast(i64)
264        └── input: vortex.get_item(value)
265            └── input: vortex.root()
266        ");
267    }
268
269    #[test]
270    fn test_display_tree_not() {
271        use insta::assert_snapshot;
272        let not_expr = not(eq(get_item("active", root()), lit(true)));
273        assert_snapshot!(not_expr.display_tree().to_string(), @r"
274        vortex.not()
275        └── input: vortex.binary(=)
276            ├── lhs: vortex.get_item(active)
277            │   └── input: vortex.root()
278            └── rhs: vortex.literal(true)
279        ");
280    }
281
282    #[test]
283    fn test_display_tree_between() {
284        use insta::assert_snapshot;
285        let between_expr = between(
286            get_item("score", root()),
287            lit(0),
288            lit(100),
289            BetweenOptions {
290                lower_strict: StrictComparison::NonStrict,
291                upper_strict: StrictComparison::NonStrict,
292            },
293        );
294        assert_snapshot!(between_expr.display_tree().to_string(), @r"
295        vortex.between(lower_strict: <=, upper_strict: <=)
296        ├── array: vortex.get_item(score)
297        │   └── input: vortex.root()
298        ├── lower: vortex.literal(0i32)
299        └── upper: vortex.literal(100i32)
300        ");
301    }
302
303    #[test]
304    fn test_display_tree_nested() {
305        use insta::assert_snapshot;
306        let nested_expr = select(
307            ["result"],
308            cast(
309                between(
310                    get_item("score", root()),
311                    lit(50),
312                    lit(100),
313                    BetweenOptions {
314                        lower_strict: StrictComparison::Strict,
315                        upper_strict: StrictComparison::NonStrict,
316                    },
317                ),
318                DType::Bool(Nullability::NonNullable),
319            ),
320        );
321        assert_snapshot!(nested_expr.display_tree().to_string(), @r"
322        vortex.select({result})
323        └── child: vortex.cast(bool)
324            └── input: vortex.between(lower_strict: <, upper_strict: <=)
325                ├── array: vortex.get_item(score)
326                │   └── input: vortex.root()
327                ├── lower: vortex.literal(50i32)
328                └── upper: vortex.literal(100i32)
329        ");
330    }
331
332    #[test]
333    fn test_display_tree_pack() {
334        use insta::assert_snapshot;
335        let select_from_pack_expr = select(
336            ["fizz", "buzz"],
337            pack(
338                [
339                    ("fizz", root()),
340                    ("bar", lit(5)),
341                    ("buzz", eq(lit(42), get_item("answer", root()))),
342                ],
343                Nullability::Nullable,
344            ),
345        );
346        assert_snapshot!(select_from_pack_expr.display_tree().to_string(), @r"
347        vortex.select({fizz, buzz})
348        └── child: vortex.pack(names: [fizz, bar, buzz], nullability: Nullable)
349            ├── fizz: vortex.root()
350            ├── bar: vortex.literal(5i32)
351            └── buzz: vortex.binary(=)
352                ├── lhs: vortex.literal(42i32)
353                └── rhs: vortex.get_item(answer)
354                    └── input: vortex.root()
355        ");
356    }
357}