1mod 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#[derive(Clone)]
44pub struct FillNull;
45
46impl FillNull {
47 pub fn try_new(input: ArrayRef, fill_value: ArrayRef) -> VortexResult<ScalarFnArray> {
53 ScalarFnArray::try_new(FillNull.bind(EmptyOptions), [input, fill_value])
54 }
55
56 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 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 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 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 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 Ok(ReduceNodeValidity::Reduced(is_not_null_node(
191 &node.child(1),
192 )?))
193 }
194
195 fn is_strict(&self, _options: &Self::Options) -> bool {
196 false
198 }
199
200 fn is_infallible(&self, _options: &Self::Options) -> bool {
201 true
202 }
203}
204
205fn 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 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 #[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 #[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}