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