vortex_array/scalar_fn/fns/cast/
mod.rs1mod kernel;
5
6use std::fmt::Display;
7use std::fmt::Formatter;
8
9pub use kernel::*;
10use prost::Message;
11use vortex_error::VortexExpect as _;
12use vortex_error::VortexResult;
13use vortex_error::vortex_bail;
14use vortex_error::vortex_err;
15use vortex_session::VortexSession;
16use vortex_session::registry::CachedId;
17
18use crate::AnyColumnar;
19use crate::ArrayRef;
20use crate::ArrayView;
21use crate::Canonical;
22use crate::CanonicalView;
23use crate::ColumnarView;
24use crate::ExecutionCtx;
25use crate::IntoArray;
26use crate::arrays::Bool;
27use crate::arrays::Constant;
28use crate::arrays::Decimal;
29use crate::arrays::Extension;
30use crate::arrays::FixedSizeList;
31use crate::arrays::ListView;
32use crate::arrays::Map;
33use crate::arrays::Null;
34use crate::arrays::Primitive;
35use crate::arrays::ScalarFnArray;
36use crate::arrays::VarBinView;
37use crate::arrays::struct_::compute::cast::struct_cast;
38use crate::builtins::ArrayBuiltins;
39use crate::dtype::DType;
40use crate::expr::BoundExpression;
41use crate::expr::bound;
42use crate::expr::display::ExprDisplay;
43use crate::proto::expr as pb;
44use crate::scalar_fn::Arity;
45use crate::scalar_fn::ChildName;
46use crate::scalar_fn::ExecutionArgs;
47use crate::scalar_fn::ReduceNode;
48use crate::scalar_fn::ReduceNodeValidity;
49use crate::scalar_fn::ScalarFnId;
50use crate::scalar_fn::ScalarFnVTable;
51use crate::scalar_fn::ScalarFnVTableExt;
52use crate::scalar_fn::fns::literal::Literal;
53use crate::scalar_fn::is_not_null_node;
54
55#[derive(Clone)]
57pub struct Cast;
58
59impl Cast {
60 #[expect(clippy::new_ret_no_self, reason = "constructs the lazy result array")]
62 pub fn new(input: ArrayRef, target_dtype: DType) -> ScalarFnArray {
63 ScalarFnArray::try_new(Cast.bind(target_dtype), [input])
64 .vortex_expect("Cast has one child and an infallible return dtype")
65 }
66}
67
68impl ScalarFnVTable for Cast {
69 type Options = DType;
70
71 fn id(&self) -> ScalarFnId {
72 static ID: CachedId = CachedId::new("vortex.cast");
73 *ID
74 }
75
76 fn serialize(&self, dtype: &DType) -> VortexResult<Option<Vec<u8>>> {
77 Ok(Some(
78 pb::CastOpts {
79 target: Some(dtype.try_into()?),
80 }
81 .encode_to_vec(),
82 ))
83 }
84
85 fn deserialize(
86 &self,
87 _metadata: &[u8],
88 session: &VortexSession,
89 ) -> VortexResult<Self::Options> {
90 let proto = pb::CastOpts::decode(_metadata)?.target;
91 DType::from_proto(
92 proto
93 .as_ref()
94 .ok_or_else(|| vortex_err!("Missing target dtype in Cast expression"))?,
95 session,
96 )
97 }
98
99 fn arity(&self, _options: &DType) -> Arity {
100 Arity::Exact(1)
101 }
102
103 fn child_name(&self, _instance: &DType, child_idx: usize) -> ChildName {
104 match child_idx {
105 0 => ChildName::from("input"),
106 _ => unreachable!("Invalid child index {} for Cast expression", child_idx),
107 }
108 }
109
110 fn fmt_sql(
111 &self,
112 dtype: &DType,
113 expr: &dyn ExprDisplay,
114 f: &mut Formatter<'_>,
115 ) -> std::fmt::Result {
116 write!(f, "cast(")?;
117 Display::fmt(expr.display_child(0), f)?;
118 write!(f, " as {}", dtype)?;
119 write!(f, ")")
120 }
121
122 fn return_dtype(&self, dtype: &DType, _arg_dtypes: &[DType]) -> VortexResult<DType> {
123 Ok(dtype.clone())
124 }
125
126 fn execute(
127 &self,
128 target_dtype: &DType,
129 args: &dyn ExecutionArgs,
130 ctx: &mut ExecutionCtx,
131 ) -> VortexResult<ArrayRef> {
132 let input = args.get(0)?;
133
134 let Some(columnar) = input.as_opt::<AnyColumnar>() else {
135 return input.execute::<ArrayRef>(ctx)?.cast(target_dtype.clone());
136 };
137
138 match columnar {
139 ColumnarView::Canonical(canonical) => {
140 match cast_canonical(canonical, target_dtype, ctx)? {
141 Some(result) => Ok(result),
142 None => vortex_bail!(
143 "No CastKernel to cast canonical array {} from {} to {}",
144 canonical.to_array_ref().encoding_id(),
145 canonical.to_array_ref().dtype(),
146 target_dtype,
147 ),
148 }
149 }
150 ColumnarView::Constant(constant) => match cast_constant(constant, target_dtype)? {
151 Some(result) => Ok(result),
152 None => input
154 .execute::<Canonical>(ctx)?
155 .into_array()
156 .cast(target_dtype.clone()),
157 },
158 }
159 }
160
161 fn reduce<T: ReduceNode>(&self, target_dtype: &DType, node: &T) -> VortexResult<Option<T>> {
162 let child = node.child(0);
164 if &child.node_dtype()? == target_dtype {
165 return Ok(Some(child));
166 }
167 Ok(None)
168 }
169
170 fn simplify(
171 &self,
172 target_dtype: &DType,
173 expr: &BoundExpression,
174 ) -> VortexResult<Option<BoundExpression>> {
175 let Some(scalar) = expr.child(0).as_opt::<Literal>() else {
176 return Ok(None);
177 };
178 Ok(scalar.cast(target_dtype).ok().map(bound::lit))
181 }
182
183 fn validity<T: ReduceNode>(
184 &self,
185 dtype: &DType,
186 node: &T,
187 ) -> VortexResult<ReduceNodeValidity<T>> {
188 Ok(ReduceNodeValidity::Reduced(if dtype.is_nullable() {
189 is_not_null_node(&node.child(0))?
190 } else {
191 node.new_constant(true.into())
192 }))
193 }
194
195 fn is_strict(&self, _instance: &DType) -> bool {
196 false
198 }
199}
200
201fn cast_canonical(
210 canonical: CanonicalView<'_>,
211 dtype: &DType,
212 ctx: &mut ExecutionCtx,
213) -> VortexResult<Option<ArrayRef>> {
214 match canonical {
215 CanonicalView::Null(a) => <Null as CastReduce>::cast(a, dtype),
216 CanonicalView::Bool(a) => <Bool as CastKernel>::cast(a, dtype, ctx),
217 CanonicalView::Primitive(a) => <Primitive as CastKernel>::cast(a, dtype, ctx),
218 CanonicalView::Decimal(a) => <Decimal as CastKernel>::cast(a, dtype, ctx),
219 CanonicalView::VarBinView(a) => <VarBinView as CastKernel>::cast(a, dtype, ctx),
220 CanonicalView::List(a) => <ListView as CastKernel>::cast(a, dtype, ctx),
221 CanonicalView::Map(a) => <Map as CastKernel>::cast(a, dtype, ctx),
222 CanonicalView::FixedSizeList(a) => <FixedSizeList as CastKernel>::cast(a, dtype, ctx),
223 CanonicalView::Struct(a) => struct_cast(a, dtype, ctx),
224 CanonicalView::Union(_) => {
225 vortex_bail!(
226 "TODO(connor)[Union]: implement Union casting with conformance coverage for \
227 outer nullability changes, including validation of nullable-to-nonnullable casts"
228 )
229 }
230 CanonicalView::Extension(a) => <Extension as CastReduce>::cast(a, dtype),
231 CanonicalView::Variant(_) => {
232 vortex_bail!("Variant arrays don't support casting")
233 }
234 }
235}
236
237fn cast_constant(array: ArrayView<Constant>, dtype: &DType) -> VortexResult<Option<ArrayRef>> {
239 <Constant as CastReduce>::cast(array, dtype)
240}
241
242#[cfg(test)]
243mod tests {
244 use vortex_buffer::buffer;
245 use vortex_error::VortexResult;
246 use vortex_error::vortex_err;
247
248 use super::Cast;
249 use crate::IntoArray;
250 use crate::VortexSessionExecute;
251 use crate::array_session;
252 use crate::arrays::StructArray;
253 use crate::assert_arrays_eq;
254 use crate::dtype::DType;
255 use crate::dtype::DecimalDType;
256 use crate::dtype::Nullability;
257 use crate::dtype::PType;
258 use crate::expr::Expression;
259 use crate::expr::cast;
260 use crate::expr::get_item;
261 use crate::expr::lit;
262 use crate::expr::root;
263 use crate::expr::test_harness;
264 use crate::scalar::DecimalValue;
265 use crate::scalar::Scalar;
266 use crate::scalar_fn::fns::literal::Literal;
267
268 #[test]
269 fn dtype() {
270 let dtype = test_harness::struct_dtype();
271 assert_eq!(
272 cast(root(), DType::Bool(Nullability::NonNullable))
273 .return_dtype(&dtype)
274 .unwrap(),
275 DType::Bool(Nullability::NonNullable)
276 );
277 }
278
279 #[test]
280 fn replace_children() -> VortexResult<()> {
281 let dtype = DType::Bool(Nullability::Nullable);
282 let expr = cast(root(), dtype.clone());
283 let replaced = expr.with_children(vec![get_item("a", root())])?;
284 assert_eq!(replaced, cast(get_item("a", root()), dtype));
285 Ok(())
286 }
287
288 #[test]
289 fn evaluate() {
290 let test_array = StructArray::from_fields(&[
291 ("a", buffer![0i32, 1, 2].into_array()),
292 ("b", buffer![4i64, 5, 6].into_array()),
293 ])
294 .unwrap()
295 .into_array();
296
297 let expr: Expression = cast(
298 get_item("a", root()),
299 DType::Primitive(PType::I64, Nullability::NonNullable),
300 );
301 let result = test_array.apply(&expr).unwrap();
302
303 assert_eq!(
304 result.dtype(),
305 &DType::Primitive(PType::I64, Nullability::NonNullable)
306 );
307 assert_arrays_eq!(
308 result,
309 buffer![0i64, 1, 2].into_array(),
310 &mut array_session().create_execution_ctx()
311 );
312 }
313
314 #[test]
315 fn simplify_folds_cast_of_literal() -> VortexResult<()> {
316 let expr = cast(
317 lit(3i32),
318 DType::Primitive(PType::F64, Nullability::NonNullable),
319 );
320 let optimized = expr.bind(&test_harness::struct_dtype())?.optimize()?;
321
322 let scalar = optimized
323 .as_opt::<Literal>()
324 .ok_or_else(|| vortex_err!("expected a bare literal, got {optimized}"))?;
325 assert_eq!(scalar, &Scalar::primitive(3.0f64, Nullability::NonNullable));
326 Ok(())
327 }
328
329 #[test]
330 fn simplify_folds_cast_of_decimal_literal() -> VortexResult<()> {
331 let decimal = Scalar::decimal(
332 DecimalValue::I128(319),
333 DecimalDType::new(3, 2),
334 Nullability::NonNullable,
335 );
336 let expr = cast(
337 lit(decimal),
338 DType::Primitive(PType::F64, Nullability::NonNullable),
339 );
340 let optimized = expr.bind(&test_harness::struct_dtype())?.optimize()?;
341
342 let scalar = optimized
343 .as_opt::<Literal>()
344 .ok_or_else(|| vortex_err!("expected a bare literal, got {optimized}"))?;
345 assert_eq!(
346 scalar,
347 &Scalar::primitive(3.19f64, Nullability::NonNullable)
348 );
349 Ok(())
350 }
351
352 #[test]
353 fn simplify_leaves_failing_cast_unchanged() -> VortexResult<()> {
354 let target = DType::Primitive(PType::F64, Nullability::NonNullable);
355 let expr = cast(
356 lit(Scalar::null(DType::Primitive(
357 PType::I32,
358 Nullability::Nullable,
359 ))),
360 target.clone(),
361 );
362 let optimized = expr.bind(&test_harness::struct_dtype())?.optimize()?;
363
364 assert!(optimized.as_opt::<Literal>().is_none());
365 assert_eq!(optimized.as_opt::<Cast>(), Some(&target));
366 Ok(())
367 }
368
369 #[test]
370 fn test_display() {
371 let expr = cast(
372 get_item("value", root()),
373 DType::Primitive(PType::I64, Nullability::NonNullable),
374 );
375 assert_eq!(expr.to_string(), "cast($.value as i64)");
376
377 let expr2 = cast(root(), DType::Bool(Nullability::Nullable));
378 assert_eq!(expr2.to_string(), "cast($ as bool?)");
379 }
380}