vortex_array/expr/transform/
coerce.rs1use 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
16pub fn coerce_expression(expr: Expression, scope: &DType) -> VortexResult<Expression> {
22 let scope = scope.clone();
24 expr.transform_up(|node| {
25 if node.is::<Root>() || node.is::<Literal>() || node.children().is_empty() {
27 return Ok(Transformed::no(node));
28 }
29
30 let child_dtypes: Vec<DType> = node
32 .children()
33 .iter()
34 .map(|c| c.return_dtype(&scope))
35 .collect::<VortexResult<_>>()?;
36
37 let coerced_dtypes = node.scalar_fn().coerce_args(&child_dtypes)?;
39
40 if child_dtypes == coerced_dtypes {
42 return Ok(Transformed::no(node));
43 }
44
45 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 let expr = Binary.new_expr(Operator::Lt, [col("x"), col("y")]);
104 let coerced = coerce_expression(expr, &scope)?;
105
106 assert!(coerced.child(0).is::<Cast>());
108 assert_eq!(
110 coerced.child(0).return_dtype(&scope)?,
111 DType::Primitive(PType::I64, NonNullable)
112 );
113 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 let expr = Binary.new_expr(Operator::Lt, [col("x"), col("x")]);
123 let coerced = coerce_expression(expr, &scope)?;
124
125 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 let expr = Binary.new_expr(Operator::Add, [col("a"), col("b")]);
146 let coerced = coerce_expression(expr, &scope)?;
147
148 assert!(coerced.child(0).is::<Cast>());
150 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 let expr = Binary.new_expr(Operator::Add, [col("x"), lit(Scalar::from(1i32))]);
209 let coerced = coerce_expression(expr, &scope)?;
210
211 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}