vortex_array/scalar_fn/fns/
is_not_null.rs1use std::fmt::Display;
5use std::fmt::Formatter;
6
7use vortex_error::VortexExpect as _;
8use vortex_error::VortexResult;
9use vortex_session::VortexSession;
10use vortex_session::registry::CachedId;
11
12use crate::ArrayRef;
13use crate::ExecutionCtx;
14use crate::arrays::ScalarFnArray;
15use crate::dtype::DType;
16use crate::dtype::Nullability;
17use crate::expr::display::ExprDisplay;
18use crate::scalar_fn::Arity;
19use crate::scalar_fn::ChildName;
20use crate::scalar_fn::EmptyOptions;
21use crate::scalar_fn::ExecutionArgs;
22use crate::scalar_fn::ReduceNode;
23use crate::scalar_fn::ReduceNodeValidity;
24use crate::scalar_fn::ScalarFnId;
25use crate::scalar_fn::ScalarFnVTable;
26use crate::scalar_fn::ScalarFnVTableExt;
27use crate::scalar_fn::fns::binary::Binary;
28use crate::scalar_fn::fns::is_null::IsNull;
29use crate::scalar_fn::fns::not::Not;
30use crate::scalar_fn::fns::operators::Operator;
31use crate::scalar_fn::is_not_null_node;
32
33pub(crate) fn reduce_null<T: ReduceNode>(is_null: bool, node: &T) -> VortexResult<Option<T>> {
51 let child = node.child(0);
52 if !child.node_dtype()?.is_nullable() {
53 return Ok(Some(node.new_constant((!is_null).into())));
54 }
55
56 if let ReduceNodeValidity::Reduced(validity) = child.validity()? {
57 return Ok(Some(if is_null {
58 validity.new_node(Not.bind(EmptyOptions), std::slice::from_ref(&validity))?
59 } else {
60 validity
61 }));
62 }
63
64 let Some(child_fn) = child.scalar_fn() else {
65 return Ok(None);
66 };
67 let Some(operator) = child_fn.as_opt::<Binary>() else {
68 return Ok(None);
69 };
70 let is_and = match operator {
71 Operator::And => true,
72 Operator::Or => false,
73 _ => return Ok(None),
74 };
75
76 let left = child.child(0);
77 let right = child.child(1);
78 let (nullable, non_nullable) = match (
79 left.node_dtype()?.is_nullable(),
80 right.node_dtype()?.is_nullable(),
81 ) {
82 (true, false) => (left, right),
83 (false, true) => (right, left),
84 _ => return Ok(None),
86 };
87
88 let left = if is_null {
91 nullable.new_node(IsNull.bind(EmptyOptions), std::slice::from_ref(&nullable))?
92 } else {
93 is_not_null_node(&nullable)?
94 };
95 let right = if is_null == is_and {
98 non_nullable
99 } else {
100 non_nullable.new_node(Not.bind(EmptyOptions), std::slice::from_ref(&non_nullable))?
101 };
102 let combine = if is_null { Operator::And } else { Operator::Or };
103 Ok(Some(node.new_node(Binary.bind(combine), &[left, right])?))
104}
105
106#[derive(Clone)]
108pub struct IsNotNull;
109
110impl IsNotNull {
111 #[expect(clippy::new_ret_no_self, reason = "constructs the lazy result array")]
113 pub fn new(input: ArrayRef) -> ScalarFnArray {
114 ScalarFnArray::try_new(IsNotNull.bind(EmptyOptions), [input])
115 .vortex_expect("IsNotNull has one child and an infallible return dtype")
116 }
117}
118
119impl ScalarFnVTable for IsNotNull {
120 type Options = EmptyOptions;
121
122 fn id(&self) -> ScalarFnId {
123 static ID: CachedId = CachedId::new("vortex.is_not_null");
124 *ID
125 }
126
127 fn serialize(&self, _instance: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
128 Ok(Some(vec![]))
129 }
130
131 fn deserialize(
132 &self,
133 _metadata: &[u8],
134 _session: &VortexSession,
135 ) -> VortexResult<Self::Options> {
136 Ok(EmptyOptions)
137 }
138
139 fn arity(&self, _options: &Self::Options) -> Arity {
140 Arity::Exact(1)
141 }
142
143 fn child_name(&self, _instance: &Self::Options, child_idx: usize) -> ChildName {
144 match child_idx {
145 0 => ChildName::from("input"),
146 _ => unreachable!("Invalid child index {} for IsNotNull expression", child_idx),
147 }
148 }
149
150 fn fmt_sql(
151 &self,
152 _options: &Self::Options,
153 expr: &dyn ExprDisplay,
154 f: &mut Formatter<'_>,
155 ) -> std::fmt::Result {
156 write!(f, "is_not_null(")?;
157 Display::fmt(expr.display_child(0), f)?;
158 write!(f, ")")
159 }
160
161 fn return_dtype(&self, _options: &Self::Options, _arg_dtypes: &[DType]) -> VortexResult<DType> {
162 Ok(DType::Bool(Nullability::NonNullable))
163 }
164
165 fn execute(
166 &self,
167 _data: &Self::Options,
168 args: &dyn ExecutionArgs,
169 _ctx: &mut ExecutionCtx,
170 ) -> VortexResult<ArrayRef> {
171 Ok(args.get(0)?.validity()?.to_array(args.row_count()))
172 }
173
174 fn reduce<T: ReduceNode>(&self, _options: &Self::Options, node: &T) -> VortexResult<Option<T>> {
175 reduce_null(false, node)
176 }
177
178 fn is_strict(&self, _instance: &Self::Options) -> bool {
179 false
181 }
182
183 fn is_infallible(&self, _instance: &Self::Options) -> bool {
184 true
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use std::sync::LazyLock;
191
192 use vortex_buffer::buffer;
193 use vortex_error::VortexExpect as _;
194 use vortex_error::VortexResult;
195 use vortex_session::VortexSession;
196
197 use crate::IntoArray;
198 use crate::VortexSessionExecute;
199 use crate::array_session;
200 use crate::arrays::PrimitiveArray;
201 use crate::arrays::StructArray;
202 use crate::dtype::DType;
203 use crate::dtype::Nullability;
204 use crate::dtype::StructFields;
205 use crate::expr::BoundExpression;
206 use crate::expr::Expression;
207 use crate::expr::and;
208 use crate::expr::col;
209 use crate::expr::eq;
210 use crate::expr::get_item;
211 use crate::expr::is_not_null;
212 use crate::expr::is_null;
213 use crate::expr::lit;
214 use crate::expr::not;
215 use crate::expr::or;
216 use crate::expr::root;
217 use crate::expr::test_harness;
218 use crate::scalar::Scalar;
219 use crate::scalar_fn::EmptyOptions;
220 use crate::scalar_fn::ScalarFnVTableExt;
221 use crate::scalar_fn::internal::row_count::RowCount;
222 use crate::stats::StatsSession;
223 use crate::stats::all_null;
224 use crate::stats::null_count;
225
226 static STATS_SESSION: LazyLock<VortexSession> =
227 LazyLock::new(|| VortexSession::empty().with::<StatsSession>());
228
229 #[test]
230 fn dtype() {
231 let dtype = test_harness::struct_dtype();
232 assert_eq!(
233 is_not_null(root()).return_dtype(&dtype).unwrap(),
234 DType::Bool(Nullability::NonNullable)
235 );
236 }
237
238 fn bool_dtype() -> DType {
239 let names = ["a", "b"].into();
240 let fields = vec![
241 DType::Bool(Nullability::Nullable),
242 DType::Bool(Nullability::NonNullable),
243 ];
244 DType::Struct(StructFields::new(names, fields), Nullability::NonNullable)
245 }
246
247 fn optimized(expr: Expression, scope: &DType) -> VortexResult<BoundExpression> {
248 expr.bind(scope)?.optimize()
249 }
250
251 #[test]
252 fn reduce_to_constant() -> VortexResult<()> {
253 let dtype = bool_dtype();
254 assert_eq!(
255 optimized(is_not_null(col("b")), &dtype)?,
256 lit(true).bind(&dtype)?
257 );
258 assert_eq!(
259 optimized(is_null(col("b")), &dtype)?,
260 lit(false).bind(&dtype)?
261 );
262 Ok(())
263 }
264
265 #[test]
266 fn reduce_kleene() -> VortexResult<()> {
267 let dtype = bool_dtype();
268 assert_eq!(
269 optimized(is_not_null(and(col("a"), col("b"))), &dtype)?,
270 or(is_not_null(col("a")), not(col("b"))).bind(&dtype)?
271 );
272 assert_eq!(
273 optimized(is_not_null(or(col("a"), col("b"))), &dtype)?,
274 or(is_not_null(col("a")), col("b")).bind(&dtype)?
275 );
276 assert_eq!(
277 optimized(is_null(and(col("a"), col("b"))), &dtype)?,
278 and(is_null(col("a")), col("b")).bind(&dtype)?
279 );
280 assert_eq!(
281 optimized(is_null(or(col("a"), col("b"))), &dtype)?,
282 and(is_null(col("a")), not(col("b"))).bind(&dtype)?
283 );
284 Ok(())
285 }
286
287 #[test]
288 fn replace_children() {
289 let expr = is_not_null(root());
290 expr.with_children([root()])
291 .vortex_expect("operation should succeed in test");
292 }
293
294 #[test]
295 fn evaluate_mask() {
296 let test_array =
297 PrimitiveArray::from_option_iter(vec![Some(1), None, Some(2), None, Some(3)])
298 .into_array();
299 let expected = [true, false, true, false, true];
300
301 let result = test_array.clone().apply(&is_not_null(root())).unwrap();
302
303 assert_eq!(result.len(), test_array.len());
304 assert_eq!(result.dtype(), &DType::Bool(Nullability::NonNullable));
305
306 for (i, expected_value) in expected.iter().enumerate() {
307 assert_eq!(
308 result
309 .execute_scalar(i, &mut array_session().create_execution_ctx())
310 .unwrap(),
311 Scalar::bool(*expected_value, Nullability::NonNullable)
312 );
313 }
314 }
315
316 #[test]
317 fn evaluate_all_true() {
318 let test_array = buffer![1, 2, 3, 4, 5].into_array();
319
320 let result = test_array.clone().apply(&is_not_null(root())).unwrap();
321
322 assert_eq!(result.len(), test_array.len());
323 for i in 0..result.len() {
324 assert_eq!(
325 result
326 .execute_scalar(i, &mut array_session().create_execution_ctx())
327 .unwrap(),
328 Scalar::bool(true, Nullability::NonNullable)
329 );
330 }
331 }
332
333 #[test]
334 fn evaluate_all_false() {
335 let test_array =
336 PrimitiveArray::from_option_iter(vec![None::<i32>, None, None, None, None])
337 .into_array();
338
339 let result = test_array.clone().apply(&is_not_null(root())).unwrap();
340
341 assert_eq!(result.len(), test_array.len());
342 for i in 0..result.len() {
343 assert_eq!(
344 result
345 .execute_scalar(i, &mut array_session().create_execution_ctx())
346 .unwrap(),
347 Scalar::bool(false, Nullability::NonNullable)
348 );
349 }
350 }
351
352 #[test]
353 fn evaluate_struct() {
354 let test_array = StructArray::from_fields(&[(
355 "a",
356 PrimitiveArray::from_option_iter(vec![Some(1), None, Some(2), None, Some(3)])
357 .into_array(),
358 )])
359 .unwrap()
360 .into_array();
361 let expected = [true, false, true, false, true];
362
363 let result = test_array
364 .clone()
365 .apply(&is_not_null(get_item("a", root())))
366 .unwrap();
367
368 assert_eq!(result.len(), test_array.len());
369 assert_eq!(result.dtype(), &DType::Bool(Nullability::NonNullable));
370
371 for (i, expected_value) in expected.iter().enumerate() {
372 assert_eq!(
373 result
374 .execute_scalar(i, &mut array_session().create_execution_ctx())
375 .unwrap(),
376 Scalar::bool(*expected_value, Nullability::NonNullable)
377 );
378 }
379 }
380
381 #[test]
382 fn test_display() {
383 let expr = is_not_null(get_item("name", root()));
384 assert_eq!(expr.to_string(), "is_not_null($.name)");
385
386 let expr2 = is_not_null(root());
387 assert_eq!(expr2.to_string(), "is_not_null($)");
388 }
389
390 #[test]
391 fn test_is_not_null_is_not_strict() {
392 assert!(
393 !is_not_null(col("a"))
394 .as_scalar()
395 .is_some_and(|f| f.signature().is_strict())
396 );
397 }
398
399 #[test]
400 fn test_is_not_null_falsification() -> VortexResult<()> {
401 let expr = is_not_null(col("a"));
402 let dtype = test_harness::struct_dtype();
403
404 assert_eq!(
405 expr.bind(&dtype)?.falsify(&STATS_SESSION)?,
406 Some(
407 or(
408 eq(null_count(col("a")), RowCount.new_expr(EmptyOptions, []),),
409 all_null(col("a")),
410 )
411 .bind(&dtype)?
412 )
413 );
414 Ok(())
415 }
416}