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