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