Skip to main content

vortex_array/scalar_fn/fns/fill_null/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4mod kernel;
5
6pub use kernel::*;
7use vortex_error::VortexResult;
8use vortex_error::vortex_bail;
9use vortex_error::vortex_ensure;
10use vortex_error::vortex_err;
11use vortex_session::VortexSession;
12use vortex_session::registry::CachedId;
13
14use crate::AnyColumnar;
15use crate::ArrayRef;
16use crate::Canonical;
17use crate::CanonicalView;
18use crate::ColumnarView;
19use crate::ExecutionCtx;
20use crate::IntoArray;
21use crate::arrays::Bool;
22use crate::arrays::Constant;
23use crate::arrays::Decimal;
24use crate::arrays::Primitive;
25use crate::arrays::ScalarFnArray;
26use crate::arrays::VarBinView;
27use crate::dtype::DType;
28use crate::expr::BoundExpression;
29use crate::optimizer::ArrayOptimizer;
30use crate::scalar::Scalar;
31use crate::scalar_fn::Arity;
32use crate::scalar_fn::ChildName;
33use crate::scalar_fn::EmptyOptions;
34use crate::scalar_fn::ExecutionArgs;
35use crate::scalar_fn::ReduceNode;
36use crate::scalar_fn::ReduceNodeValidity;
37use crate::scalar_fn::ScalarFnId;
38use crate::scalar_fn::ScalarFnVTable;
39use crate::scalar_fn::ScalarFnVTableExt;
40use crate::scalar_fn::is_not_null_node;
41
42/// An expression that replaces null values in the input with a fill value.
43#[derive(Clone)]
44pub struct FillNull;
45
46impl FillNull {
47    /// Creates a lazy operation that replaces null input values with `fill_value`.
48    ///
49    /// # Errors
50    ///
51    /// Returns an error if the children have different lengths or incompatible dtypes.
52    pub fn try_new(input: ArrayRef, fill_value: ArrayRef) -> VortexResult<ScalarFnArray> {
53        ScalarFnArray::try_new(FillNull.bind(EmptyOptions), [input, fill_value])
54    }
55
56    /// Returns whether fill_null can execute inputs of `dtype`.
57    ///
58    /// These are the dtypes whose canonical arrays have a [`FillNullKernel`], so update this list
59    /// whenever `fill_null_canonical` gains or loses one. Rewrites into fill_null, such as the
60    /// CASE WHEN simplification, check it first because other dtypes fail at execution time.
61    pub(crate) fn supports_dtype(dtype: &DType) -> bool {
62        matches!(
63            dtype,
64            DType::Bool(_)
65                | DType::Primitive(..)
66                | DType::Decimal(..)
67                | DType::Utf8(_)
68                | DType::Binary(_)
69        )
70    }
71}
72
73impl ScalarFnVTable for FillNull {
74    type Options = EmptyOptions;
75
76    fn id(&self) -> ScalarFnId {
77        static ID: CachedId = CachedId::new("vortex.fill_null");
78        *ID
79    }
80
81    fn serialize(&self, _options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
82        Ok(Some(vec![]))
83    }
84
85    fn deserialize(
86        &self,
87        _metadata: &[u8],
88        _session: &VortexSession,
89    ) -> VortexResult<Self::Options> {
90        Ok(EmptyOptions)
91    }
92
93    fn arity(&self, _options: &Self::Options) -> Arity {
94        Arity::Exact(2)
95    }
96
97    fn child_name(&self, _options: &Self::Options, child_idx: usize) -> ChildName {
98        match child_idx {
99            0 => ChildName::from("input"),
100            1 => ChildName::from("fill_value"),
101            _ => unreachable!("Invalid child index {} for FillNull expression", child_idx),
102        }
103    }
104
105    fn return_dtype(&self, _options: &Self::Options, arg_dtypes: &[DType]) -> VortexResult<DType> {
106        vortex_ensure!(
107            arg_dtypes[0].eq_ignore_nullability(&arg_dtypes[1]),
108            "fill_null requires input and fill value to have the same base type, got {} and {}",
109            arg_dtypes[0],
110            arg_dtypes[1]
111        );
112        // The result dtype takes the nullability of the fill value.
113        Ok(arg_dtypes[0]
114            .clone()
115            .with_nullability(arg_dtypes[1].nullability()))
116    }
117
118    fn execute(
119        &self,
120        _options: &Self::Options,
121        args: &dyn ExecutionArgs,
122        ctx: &mut ExecutionCtx,
123    ) -> VortexResult<ArrayRef> {
124        let input = args.get(0)?;
125        let fill_value = args.get(1)?;
126        // Parent kernels decline a fill that is not already constant. Materialize it here so a
127        // lazy scalar, such as a literal `ScalarFnArray`, can still fill nulls.
128        let fill_was_lazy = !fill_value.is::<Constant>();
129        let fill_value = if fill_was_lazy {
130            fill_value.execute_until::<Constant>(ctx)?
131        } else {
132            fill_value
133        };
134
135        let Some(fill_scalar) = fill_value.as_constant() else {
136            // Slicing a constant fill to an empty range yields an empty canonical array rather
137            // than a constant. An empty input has no nulls to fill, so there is nothing to do.
138            vortex_ensure!(
139                input.is_empty(),
140                "fill_null fill_value must be a constant/scalar"
141            );
142            let dtype = input
143                .dtype()
144                .with_nullability(fill_value.dtype().nullability());
145            return Ok(Canonical::empty(&dtype).into_array());
146        };
147
148        vortex_ensure!(
149            !fill_scalar.is_null(),
150            "fill_null requires a non-null fill value"
151        );
152
153        let Some(columnar) = input.as_opt::<AnyColumnar>() else {
154            if fill_was_lazy {
155                // Hand the rebuilt node back with a constant fill so the input's FillNull
156                // kernels get a chance before we decode the input.
157                return FillNull::try_new(input, fill_value)?
158                    .into_array()
159                    .optimize_ctx(ctx.session());
160            }
161            let input = input.execute::<ArrayRef>(ctx)?;
162            return Ok(FillNull::try_new(input, fill_value)?.into_array());
163        };
164
165        match columnar {
166            ColumnarView::Canonical(canonical) => fill_null_canonical(canonical, &fill_scalar, ctx),
167            ColumnarView::Constant(constant) => fill_null_constant(constant, &fill_scalar),
168        }
169    }
170
171    fn simplify(
172        &self,
173        _options: &Self::Options,
174        expr: &BoundExpression,
175    ) -> VortexResult<Option<BoundExpression>> {
176        if !expr.child(0).dtype().is_nullable() {
177            return Ok(Some(expr.child(0).clone()));
178        }
179
180        Ok(None)
181    }
182
183    fn validity<T: ReduceNode>(
184        &self,
185        _options: &Self::Options,
186        node: &T,
187    ) -> VortexResult<ReduceNodeValidity<T>> {
188        // After fill_null, the result validity depends on the fill value's nullability.
189        // If fill_value is non-nullable, the result is always valid.
190        Ok(ReduceNodeValidity::Reduced(is_not_null_node(
191            &node.child(1),
192        )?))
193    }
194
195    fn is_strict(&self, _options: &Self::Options) -> bool {
196        // This function replaces null input values instead of propagating them.
197        false
198    }
199
200    fn is_infallible(&self, _options: &Self::Options) -> bool {
201        true
202    }
203}
204
205/// Fill nulls on a canonical array by directly dispatching to the appropriate kernel.
206///
207/// Returns the filled array, or bails if no kernel is registered for the canonical type.
208fn fill_null_canonical(
209    canonical: CanonicalView<'_>,
210    fill_value: &Scalar,
211    ctx: &mut ExecutionCtx,
212) -> VortexResult<ArrayRef> {
213    let arr = canonical.to_array_ref();
214    if let Some(result) = short_circuit(&arr, fill_value)? {
215        return Ok(result);
216    }
217    match canonical {
218        CanonicalView::Bool(a) => <Bool as FillNullKernel>::fill_null(a, fill_value, ctx)?
219            .ok_or_else(|| vortex_err!("FillNullKernel for BoolArray returned None")),
220        CanonicalView::Primitive(a) => {
221            <Primitive as FillNullKernel>::fill_null(a, fill_value, ctx)?
222                .ok_or_else(|| vortex_err!("FillNullKernel for PrimitiveArray returned None"))
223        }
224        CanonicalView::Decimal(a) => <Decimal as FillNullKernel>::fill_null(a, fill_value, ctx)?
225            .ok_or_else(|| vortex_err!("FillNullKernel for DecimalArray returned None")),
226        CanonicalView::VarBinView(a) => {
227            <VarBinView as FillNullKernel>::fill_null(a, fill_value, ctx)?
228                .ok_or_else(|| vortex_err!("FillNullKernel for VarBinViewArray returned None"))
229        }
230        other => vortex_bail!(
231            "No FillNullKernel for canonical array {}",
232            other.to_array_ref().encoding_id()
233        ),
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use std::sync::Arc;
240
241    use vortex_buffer::buffer;
242    use vortex_error::VortexExpect;
243
244    use super::*;
245    use crate::Canonical;
246    use crate::IntoArray;
247    use crate::VortexSessionExecute;
248    use crate::array_session;
249    use crate::arrays::Chunked;
250    use crate::arrays::ChunkedArray;
251    use crate::arrays::Constant;
252    use crate::arrays::ConstantArray;
253    use crate::arrays::Primitive;
254    use crate::arrays::PrimitiveArray;
255    use crate::arrays::ScalarFnArray;
256    use crate::arrays::StructArray;
257    use crate::arrays::scalar_fn::ExactScalarFn;
258    use crate::assert_arrays_eq;
259    use crate::builders::builder_with_capacity_in;
260    use crate::builtins::ArrayBuiltins;
261    use crate::dtype::DType;
262    use crate::dtype::DecimalDType;
263    use crate::dtype::MapDType;
264    use crate::dtype::Nullability;
265    use crate::dtype::PType;
266    use crate::dtype::StructFields;
267    use crate::expr::fill_null;
268    use crate::expr::get_item;
269    use crate::expr::lit;
270    use crate::expr::root;
271    use crate::extension::datetime::Date;
272    use crate::extension::datetime::TimeUnit;
273    use crate::kernel::ExecuteParentKernel;
274    use crate::matcher::Matcher;
275    use crate::optimizer::rules::ArrayParentReduceRule;
276    use crate::scalar::Scalar;
277    use crate::scalar_fn::fns::binary::Binary;
278    use crate::scalar_fn::fns::operators::Operator;
279
280    /// A fill of `40 + 2` that is not a [`ConstantArray`] until it executes.
281    fn lazy_fill_42(len: usize) -> VortexResult<ArrayRef> {
282        Ok(ScalarFnArray::try_new(
283            Binary.bind(Operator::Add),
284            vec![
285                ConstantArray::new(40i32, len).into_array(),
286                ConstantArray::new(2i32, len).into_array(),
287            ],
288        )?
289        .into_array())
290    }
291
292    #[test]
293    fn dtype() {
294        let dtype = DType::Primitive(PType::I32, Nullability::Nullable);
295        assert_eq!(
296            fill_null(root(), lit(0i32)).return_dtype(&dtype).unwrap(),
297            DType::Primitive(PType::I32, Nullability::NonNullable)
298        );
299    }
300
301    #[test]
302    fn replace_children() {
303        let expr = fill_null(root(), lit(0i32));
304        let replaced = expr
305            .with_children(vec![get_item("a", root()), lit(42i32)])
306            .vortex_expect("operation should succeed in test");
307        assert_eq!(replaced, fill_null(get_item("a", root()), lit(42i32)));
308    }
309
310    #[test]
311    fn evaluate() {
312        let mut ctx = array_session().create_execution_ctx();
313        let test_array =
314            PrimitiveArray::from_option_iter([Some(1i32), None, Some(3), None, Some(5)])
315                .into_array();
316
317        let expr = fill_null(root(), lit(42i32));
318        let result = test_array.apply(&expr).unwrap();
319
320        assert_eq!(
321            result.dtype(),
322            &DType::Primitive(PType::I32, Nullability::NonNullable)
323        );
324        assert_arrays_eq!(
325            result,
326            PrimitiveArray::from_iter([1i32, 42, 3, 42, 5]),
327            &mut ctx
328        );
329    }
330
331    #[test]
332    fn evaluate_lazy_constant_fill() -> VortexResult<()> {
333        let mut ctx = array_session().create_execution_ctx();
334        let input = PrimitiveArray::from_option_iter([Some(1i32), None, Some(3)]).into_array();
335        let fill = lazy_fill_42(input.len())?;
336        assert!(fill.as_constant().is_none());
337
338        let result = FillNull::try_new(input, fill)?
339            .into_array()
340            .execute::<ArrayRef>(&mut ctx)?;
341
342        assert_eq!(
343            result.dtype(),
344            &DType::Primitive(PType::I32, Nullability::NonNullable)
345        );
346        assert_arrays_eq!(result, PrimitiveArray::from_iter([1i32, 42, 3]), &mut ctx);
347        Ok(())
348    }
349
350    #[test]
351    fn evaluate_lazy_fill_preserves_chunked_input() -> VortexResult<()> {
352        let mut ctx = array_session().create_execution_ctx();
353        let input = ChunkedArray::try_new(
354            vec![
355                PrimitiveArray::from_option_iter([Some(1i32), None]).into_array(),
356                PrimitiveArray::from_option_iter([Some(3), None]).into_array(),
357            ],
358            DType::Primitive(PType::I32, Nullability::Nullable),
359        )?
360        .into_array();
361        let fill = lazy_fill_42(input.len())?;
362
363        let result = FillNull::try_new(input, fill)?
364            .into_array()
365            .execute::<ArrayRef>(&mut ctx)?;
366
367        assert!(result.is::<Chunked>());
368        assert_arrays_eq!(
369            result,
370            PrimitiveArray::from_iter([1i32, 42, 3, 42]),
371            &mut ctx
372        );
373        Ok(())
374    }
375
376    #[test]
377    fn non_constant_fill_declines_and_errors() -> VortexResult<()> {
378        let mut ctx = array_session().create_execution_ctx();
379        let varying = PrimitiveArray::from_iter([1i32, 2, 3]).into_array();
380
381        let constant_input = ConstantArray::new(Scalar::null_native::<i32>(), 3).into_array();
382        let constant_parent =
383            FillNull::try_new(constant_input.clone(), varying.clone())?.into_array();
384        let reduced = FillNullReduceAdaptor(Constant).reduce_parent(
385            constant_input
386                .as_opt::<Constant>()
387                .vortex_expect("constant input"),
388            ExactScalarFn::<FillNull>::try_match(&constant_parent)
389                .vortex_expect("fill_null parent"),
390            0,
391        )?;
392        assert!(reduced.is_none());
393
394        let primitive_input =
395            PrimitiveArray::from_option_iter([Some(1i32), None, Some(3)]).into_array();
396        let primitive_parent = FillNull::try_new(primitive_input.clone(), varying)?.into_array();
397        let executed = FillNullExecuteAdaptor(Primitive).execute_parent(
398            primitive_input
399                .as_opt::<Primitive>()
400                .vortex_expect("primitive input"),
401            ExactScalarFn::<FillNull>::try_match(&primitive_parent)
402                .vortex_expect("fill_null parent"),
403            0,
404            &mut ctx,
405        )?;
406        assert!(executed.is_none());
407
408        let err = primitive_parent.execute::<ArrayRef>(&mut ctx).unwrap_err();
409        assert!(
410            err.to_string()
411                .contains("fill_null fill_value must be a constant/scalar")
412        );
413        Ok(())
414    }
415
416    /// Filling a chunked array slices the constant fill per chunk, and an empty slice is a
417    /// canonical array rather than a constant. Stacked fills over an empty chunk must still run.
418    #[test]
419    fn repeated_fill_over_empty_chunk() -> VortexResult<()> {
420        let mut ctx = array_session().create_execution_ctx();
421        let input = ChunkedArray::try_new(
422            vec![
423                PrimitiveArray::from_option_iter(Vec::<Option<u16>>::new()).into_array(),
424                PrimitiveArray::from_option_iter([Some(7u16), None]).into_array(),
425            ],
426            DType::Primitive(PType::U16, Nullability::Nullable),
427        )?
428        .into_array();
429
430        let result = input
431            .fill_null(Scalar::primitive(1u16, Nullability::Nullable))?
432            .fill_null(Scalar::primitive(2u16, Nullability::NonNullable))?;
433
434        assert_arrays_eq!(result, PrimitiveArray::from_iter([7u16, 1]), &mut ctx);
435        Ok(())
436    }
437
438    #[test]
439    fn evaluate_struct_field() {
440        let mut ctx = array_session().create_execution_ctx();
441        let test_array = StructArray::from_fields(&[(
442            "a",
443            PrimitiveArray::from_option_iter([Some(1i32), None, Some(3)]).into_array(),
444        )])
445        .unwrap()
446        .into_array();
447
448        let expr = fill_null(get_item("a", root()), lit(0i32));
449        let result = test_array.apply(&expr).unwrap();
450
451        assert_eq!(
452            result.dtype(),
453            &DType::Primitive(PType::I32, Nullability::NonNullable)
454        );
455        assert_arrays_eq!(result, PrimitiveArray::from_iter([1i32, 0, 3]), &mut ctx);
456    }
457
458    #[test]
459    fn evaluate_non_nullable_input() {
460        let mut ctx = array_session().create_execution_ctx();
461        let test_array = buffer![1i32, 2, 3].into_array();
462        let expr = fill_null(root(), lit(0i32));
463        let result = test_array.apply(&expr).unwrap();
464        assert_arrays_eq!(result, PrimitiveArray::from_iter([1i32, 2, 3]), &mut ctx);
465    }
466
467    /// Inputs with both valid and null rows execute exactly for the dtypes that `supports_dtype`
468    /// accepts, so adding a kernel without updating `supports_dtype` fails here.
469    #[test]
470    fn supports_dtype_matches_kernels() -> VortexResult<()> {
471        let mut ctx = array_session().create_execution_ctx();
472        let i32_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
473        let dtypes = [
474            DType::Bool(Nullability::Nullable),
475            DType::Primitive(PType::I32, Nullability::Nullable),
476            DType::Decimal(DecimalDType::new(19, 2), Nullability::Nullable),
477            DType::Utf8(Nullability::Nullable),
478            DType::Binary(Nullability::Nullable),
479            DType::List(Arc::new(i32_dtype.clone()), Nullability::Nullable),
480            DType::FixedSizeList(Arc::new(i32_dtype.clone()), 2, Nullability::Nullable),
481            DType::Struct(
482                StructFields::from_iter([("value", i32_dtype.clone())]),
483                Nullability::Nullable,
484            ),
485            DType::Map(
486                MapDType::try_new(i32_dtype.clone(), i32_dtype, false)?,
487                Nullability::Nullable,
488            ),
489            DType::Extension(Date::new(TimeUnit::Days, Nullability::Nullable).erased()),
490        ];
491
492        let supported = [
493            true, true, true, true, true, false, false, false, false, false,
494        ];
495        for (dtype, supported) in dtypes.into_iter().zip(supported) {
496            assert_eq!(FillNull::supports_dtype(&dtype), supported, "{dtype}");
497            let fill = Scalar::default_value(&dtype.as_nonnullable());
498            let mut builder = builder_with_capacity_in(&dtype, 2, ctx.allocator());
499            builder.append_scalar(&fill.cast(&dtype)?)?;
500            builder.append_null();
501            let filled = builder
502                .finish()
503                .fill_null(fill.clone())?
504                .execute::<Canonical>(&mut ctx);
505
506            if supported {
507                let filled = filled?.into_array();
508                assert_eq!(filled.dtype(), fill.dtype(), "{dtype}");
509                assert_eq!(filled.execute_scalar(1, &mut ctx)?, fill, "{dtype}");
510                assert_arrays_eq!(filled, ConstantArray::new(fill, 2), &mut ctx);
511            } else {
512                assert!(filled.is_err(), "{dtype} executes but is not supported");
513            }
514        }
515        Ok(())
516    }
517
518    #[test]
519    fn test_display() {
520        let expr = fill_null(get_item("value", root()), lit(0i32));
521        assert_eq!(expr.to_string(), "vortex.fill_null($.value, 0i32)");
522    }
523}