Skip to main content

vortex_array/scalar_fn/fns/
is_null.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use vortex_error::VortexExpect as _;
5use vortex_error::VortexResult;
6use vortex_session::VortexSession;
7use vortex_session::registry::CachedId;
8
9use crate::ArrayRef;
10use crate::ExecutionCtx;
11use crate::IntoArray;
12use crate::arrays::ConstantArray;
13use crate::arrays::ScalarFnArray;
14use crate::builtins::ArrayBuiltins;
15use crate::dtype::DType;
16use crate::dtype::Nullability;
17use crate::scalar_fn::Arity;
18use crate::scalar_fn::ChildName;
19use crate::scalar_fn::EmptyOptions;
20use crate::scalar_fn::ExecutionArgs;
21use crate::scalar_fn::ScalarFnId;
22use crate::scalar_fn::ScalarFnVTable;
23use crate::scalar_fn::ScalarFnVTableExt;
24use crate::validity::Validity;
25
26/// Expression that checks for null values.
27#[derive(Clone)]
28pub struct IsNull;
29
30impl IsNull {
31    /// Creates a lazy null check over `input`.
32    #[expect(clippy::new_ret_no_self, reason = "constructs the lazy result array")]
33    pub fn new(input: ArrayRef) -> ScalarFnArray {
34        ScalarFnArray::try_new(IsNull.bind(EmptyOptions), vec![input])
35            .vortex_expect("IsNull has one child and an infallible return dtype")
36    }
37}
38
39impl ScalarFnVTable for IsNull {
40    type Options = EmptyOptions;
41
42    fn id(&self) -> ScalarFnId {
43        static ID: CachedId = CachedId::new("vortex.is_null");
44        *ID
45    }
46
47    fn serialize(&self, _instance: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
48        Ok(Some(vec![]))
49    }
50
51    fn deserialize(
52        &self,
53        _metadata: &[u8],
54        _session: &VortexSession,
55    ) -> VortexResult<Self::Options> {
56        Ok(EmptyOptions)
57    }
58
59    fn arity(&self, _options: &Self::Options) -> Arity {
60        Arity::Exact(1)
61    }
62
63    fn child_name(&self, _instance: &Self::Options, child_idx: usize) -> ChildName {
64        match child_idx {
65            0 => ChildName::from("input"),
66            _ => unreachable!("Invalid child index {} for IsNull expression", child_idx),
67        }
68    }
69
70    fn return_dtype(&self, _options: &Self::Options, _arg_dtypes: &[DType]) -> VortexResult<DType> {
71        Ok(DType::Bool(Nullability::NonNullable))
72    }
73
74    fn execute(
75        &self,
76        _data: &Self::Options,
77        args: &dyn ExecutionArgs,
78        _ctx: &mut ExecutionCtx,
79    ) -> VortexResult<ArrayRef> {
80        let child = args.get(0)?;
81        if let Some(scalar) = child.as_constant() {
82            return Ok(ConstantArray::new(scalar.is_null(), args.row_count()).into_array());
83        }
84
85        match child.validity()? {
86            Validity::NonNullable | Validity::AllValid => {
87                Ok(ConstantArray::new(false, args.row_count()).into_array())
88            }
89            Validity::AllInvalid => Ok(ConstantArray::new(true, args.row_count()).into_array()),
90            Validity::Array(a) => a.not(),
91        }
92    }
93
94    fn is_strict(&self, _instance: &Self::Options) -> bool {
95        // Null input produces the non-null boolean value `true`.
96        false
97    }
98
99    fn is_infallible(&self, _instance: &Self::Options) -> bool {
100        true
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use std::sync::LazyLock;
107
108    use vortex_buffer::buffer;
109    use vortex_error::VortexExpect as _;
110    use vortex_error::VortexResult;
111    use vortex_session::VortexSession;
112
113    use crate::IntoArray;
114    use crate::VortexSessionExecute;
115    use crate::array_session;
116    use crate::arrays::PrimitiveArray;
117    use crate::arrays::StructArray;
118    use crate::dtype::DType;
119    use crate::dtype::Nullability;
120    use crate::expr::col;
121    use crate::expr::eq;
122    use crate::expr::get_item;
123    use crate::expr::is_null;
124    use crate::expr::lit;
125    use crate::expr::or;
126    use crate::expr::root;
127    use crate::expr::test_harness;
128    use crate::scalar::Scalar;
129    use crate::stats::StatsSession;
130    use crate::stats::all_non_null;
131    use crate::stats::null_count;
132
133    static STATS_SESSION: LazyLock<VortexSession> =
134        LazyLock::new(|| VortexSession::empty().with::<StatsSession>());
135
136    #[test]
137    fn dtype() {
138        let dtype = test_harness::struct_dtype();
139        assert_eq!(
140            is_null(root()).return_dtype(&dtype).unwrap(),
141            DType::Bool(Nullability::NonNullable)
142        );
143    }
144
145    #[test]
146    fn replace_children() {
147        let expr = is_null(root());
148        expr.with_children([root()])
149            .vortex_expect("operation should succeed in test");
150    }
151
152    #[test]
153    fn evaluate_mask() {
154        let test_array =
155            PrimitiveArray::from_option_iter(vec![Some(1), None, Some(2), None, Some(3)])
156                .into_array();
157        let expected = [false, true, false, true, false];
158
159        let result = test_array.clone().apply(&is_null(root())).unwrap();
160
161        assert_eq!(result.len(), test_array.len());
162        assert_eq!(result.dtype(), &DType::Bool(Nullability::NonNullable));
163
164        for (i, expected_value) in expected.iter().enumerate() {
165            assert_eq!(
166                result
167                    .execute_scalar(i, &mut array_session().create_execution_ctx())
168                    .unwrap(),
169                Scalar::bool(*expected_value, Nullability::NonNullable)
170            );
171        }
172    }
173
174    #[test]
175    fn evaluate_all_false() {
176        let test_array = buffer![1, 2, 3, 4, 5].into_array();
177
178        let result = test_array.clone().apply(&is_null(root())).unwrap();
179
180        assert_eq!(result.len(), test_array.len());
181        // All values should be false (non-nullable input)
182        for i in 0..result.len() {
183            assert_eq!(
184                result
185                    .execute_scalar(i, &mut array_session().create_execution_ctx())
186                    .unwrap(),
187                Scalar::bool(false, Nullability::NonNullable)
188            );
189        }
190    }
191
192    #[test]
193    fn evaluate_all_true() {
194        let test_array =
195            PrimitiveArray::from_option_iter(vec![None::<i32>, None, None, None, None])
196                .into_array();
197
198        let result = test_array.clone().apply(&is_null(root())).unwrap();
199
200        assert_eq!(result.len(), test_array.len());
201        // All values should be true (all nulls)
202        for i in 0..result.len() {
203            assert_eq!(
204                result
205                    .execute_scalar(i, &mut array_session().create_execution_ctx())
206                    .unwrap(),
207                Scalar::bool(true, Nullability::NonNullable)
208            );
209        }
210    }
211
212    #[test]
213    fn evaluate_struct() {
214        let test_array = StructArray::from_fields(&[(
215            "a",
216            PrimitiveArray::from_option_iter(vec![Some(1), None, Some(2), None, Some(3)])
217                .into_array(),
218        )])
219        .unwrap()
220        .into_array();
221        let expected = [false, true, false, true, false];
222
223        let result = test_array
224            .clone()
225            .apply(&is_null(get_item("a", root())))
226            .unwrap();
227
228        assert_eq!(result.len(), test_array.len());
229        assert_eq!(result.dtype(), &DType::Bool(Nullability::NonNullable));
230
231        for (i, expected_value) in expected.iter().enumerate() {
232            assert_eq!(
233                result
234                    .execute_scalar(i, &mut array_session().create_execution_ctx())
235                    .unwrap(),
236                Scalar::bool(*expected_value, Nullability::NonNullable)
237            );
238        }
239    }
240
241    #[test]
242    fn test_display() {
243        let expr = is_null(get_item("name", root()));
244        assert_eq!(expr.to_string(), "vortex.is_null($.name)");
245
246        let expr2 = is_null(root());
247        assert_eq!(expr2.to_string(), "vortex.is_null($)");
248    }
249
250    #[test]
251    fn test_is_null_falsification() -> VortexResult<()> {
252        let expr = is_null(col("a"));
253        let dtype = test_harness::struct_dtype();
254
255        assert_eq!(
256            expr.bind(&dtype)?.falsify(&STATS_SESSION)?,
257            Some(or(eq(null_count(col("a")), lit(0u64)), all_non_null(col("a")),).bind(&dtype)?)
258        );
259        Ok(())
260    }
261
262    #[test]
263    fn test_is_null_is_not_strict() {
264        assert!(
265            !is_null(col("a"))
266                .as_scalar()
267                .is_some_and(|f| f.signature().is_strict())
268        );
269    }
270}