Skip to main content

vortex_array/expr/transform/
coerce.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Expression-level type coercion pass.
5
6use vortex_error::VortexResult;
7
8use crate::dtype::DType;
9use crate::expr::Expression;
10use crate::expr::cast;
11use crate::expr::traversal::NodeExt;
12use crate::expr::traversal::Transformed;
13use crate::scalar_fn::fns::literal::Literal;
14use crate::scalar_fn::fns::root::Root;
15
16/// Rewrite an expression tree to insert casts where a scalar function's `coerce_args` demands
17/// a different type than what the child currently produces.
18///
19/// The rewrite is bottom-up: children are coerced first, then each parent node checks whether
20/// its children match the coerced argument types.
21pub fn coerce_expression(expr: Expression, scope: &DType) -> VortexResult<Expression> {
22    // We capture scope by reference for the closure.
23    let scope = scope.clone();
24    expr.transform_up(|node| {
25        // Leaf nodes (Root, Literal) have no children to coerce.
26        if node.is::<Root>() || node.is::<Literal>() || node.children().is_empty() {
27            return Ok(Transformed::no(node));
28        }
29
30        // Compute the current child return types.
31        let child_dtypes: Vec<DType> = node
32            .children()
33            .iter()
34            .map(|c| c.return_dtype(&scope))
35            .collect::<VortexResult<_>>()?;
36
37        // Ask the scalar function what types it wants.
38        let coerced_dtypes = node.scalar_fn().coerce_args(&child_dtypes)?;
39
40        // If nothing changed, skip.
41        if child_dtypes == coerced_dtypes {
42            return Ok(Transformed::no(node));
43        }
44
45        // Build new children, inserting casts where needed.
46        let new_children: Vec<Expression> = node
47            .children()
48            .iter()
49            .zip(coerced_dtypes.iter())
50            .map(|(child, target)| {
51                let child_dtype = child.return_dtype(&scope)?;
52                if child_dtype.eq_ignore_nullability(target)
53                    && child_dtype.nullability() == target.nullability()
54                {
55                    Ok(child.clone())
56                } else {
57                    Ok(cast(child.clone(), target.clone()))
58                }
59            })
60            .collect::<VortexResult<_>>()?;
61
62        let new_expr = node.with_children(new_children)?;
63        Ok(Transformed::yes(new_expr))
64    })
65    .map(|t| t.into_inner())
66}
67
68#[cfg(test)]
69mod tests {
70    use vortex_error::VortexResult;
71
72    use crate::dtype::DType;
73    use crate::dtype::DecimalDType;
74    use crate::dtype::Nullability::NonNullable;
75    use crate::dtype::PType;
76    use crate::dtype::StructFields;
77    use crate::expr::col;
78    use crate::expr::lit;
79    use crate::expr::transform::coerce::coerce_expression;
80    use crate::scalar::Scalar;
81    use crate::scalar_fn::ScalarFnVTableExt;
82    use crate::scalar_fn::fns::binary::Binary;
83    use crate::scalar_fn::fns::cast::Cast;
84    use crate::scalar_fn::fns::operators::Operator;
85
86    fn test_scope() -> DType {
87        DType::Struct(
88            StructFields::new(
89                ["x", "y"].into(),
90                vec![
91                    DType::Primitive(PType::I32, NonNullable),
92                    DType::Primitive(PType::I64, NonNullable),
93                ],
94            ),
95            NonNullable,
96        )
97    }
98
99    #[test]
100    fn mixed_type_comparison_inserts_cast() -> VortexResult<()> {
101        let scope = test_scope();
102        // x (I32) < y (I64) => should cast x to I64
103        let expr = Binary.new_expr(Operator::Lt, [col("x"), col("y")]);
104        let coerced = coerce_expression(expr, &scope)?;
105
106        // The LHS child should now be a cast expression
107        assert!(coerced.child(0).is::<Cast>());
108        // The coerced LHS should return I64
109        assert_eq!(
110            coerced.child(0).return_dtype(&scope)?,
111            DType::Primitive(PType::I64, NonNullable)
112        );
113        // The RHS should be unchanged
114        assert!(!coerced.child(1).is::<Cast>());
115        Ok(())
116    }
117
118    #[test]
119    fn same_type_comparison_no_cast() -> VortexResult<()> {
120        let scope = test_scope();
121        // x (I32) < x (I32) => no cast needed
122        let expr = Binary.new_expr(Operator::Lt, [col("x"), col("x")]);
123        let coerced = coerce_expression(expr, &scope)?;
124
125        // Neither child should be a cast
126        assert!(!coerced.child(0).is::<Cast>());
127        assert!(!coerced.child(1).is::<Cast>());
128        Ok(())
129    }
130
131    #[test]
132    fn mixed_type_arithmetic_coerces_both() -> VortexResult<()> {
133        let scope = DType::Struct(
134            StructFields::new(
135                ["a", "b"].into(),
136                vec![
137                    DType::Primitive(PType::U8, NonNullable),
138                    DType::Primitive(PType::I32, NonNullable),
139                ],
140            ),
141            NonNullable,
142        );
143        // a (U8) + b (I32) => both should be coerced to I32
144        // U8 + I32: unsigned_signed_supertype(U8, I32) => max(1,4)=4 => I64
145        let expr = Binary.new_expr(Operator::Add, [col("a"), col("b")]);
146        let coerced = coerce_expression(expr, &scope)?;
147
148        // LHS (U8) should be cast
149        assert!(coerced.child(0).is::<Cast>());
150        // Both should return the same supertype
151        let lhs_dt = coerced.child(0).return_dtype(&scope)?;
152        let rhs_dt = coerced.child(1).return_dtype(&scope)?;
153        assert_eq!(lhs_dt, rhs_dt);
154        Ok(())
155    }
156
157    #[test]
158    fn decimal_arithmetic_coerces_precision_and_scale() -> VortexResult<()> {
159        let common_dtype = DType::Decimal(DecimalDType::new(4, 2), NonNullable);
160        let result_dtype = DType::Decimal(DecimalDType::new(5, 2), NonNullable);
161        let scope = DType::Struct(
162            StructFields::new(
163                ["a", "b"].into(),
164                vec![
165                    DType::Decimal(DecimalDType::new(3, 1), NonNullable),
166                    common_dtype,
167                ],
168            ),
169            NonNullable,
170        );
171        let expr = Binary.new_expr(Operator::Add, [col("a"), col("b")]);
172
173        let coerced = coerce_expression(expr, &scope)?;
174
175        assert!(coerced.child(0).is::<Cast>());
176        assert!(!coerced.child(1).is::<Cast>());
177        assert_eq!(coerced.return_dtype(&scope)?, result_dtype);
178        Ok(())
179    }
180
181    #[test]
182    fn boolean_operators_no_coercion() -> VortexResult<()> {
183        let scope = DType::Struct(
184            StructFields::new(
185                ["p", "q"].into(),
186                vec![DType::Bool(NonNullable), DType::Bool(NonNullable)],
187            ),
188            NonNullable,
189        );
190        let expr = Binary.new_expr(Operator::And, [col("p"), col("q")]);
191        let coerced = coerce_expression(expr, &scope)?;
192
193        assert!(!coerced.child(0).is::<Cast>());
194        assert!(!coerced.child(1).is::<Cast>());
195        Ok(())
196    }
197
198    #[test]
199    fn literal_coercion() -> VortexResult<()> {
200        let scope = DType::Struct(
201            StructFields::new(
202                ["x"].into(),
203                vec![DType::Primitive(PType::I64, NonNullable)],
204            ),
205            NonNullable,
206        );
207        // x (I64) + 1i32 => literal should be cast to I64
208        let expr = Binary.new_expr(Operator::Add, [col("x"), lit(Scalar::from(1i32))]);
209        let coerced = coerce_expression(expr, &scope)?;
210
211        // The RHS (literal) should be cast to I64
212        assert!(coerced.child(1).is::<Cast>());
213        assert_eq!(
214            coerced.child(1).return_dtype(&scope)?,
215            DType::Primitive(PType::I64, NonNullable)
216        );
217        Ok(())
218    }
219}