1use std::fmt;
17use std::fmt::Formatter;
18use std::hash::Hash;
19use std::sync::Arc;
20
21use prost::Message;
22use vortex_buffer::BufferMut;
23use vortex_error::VortexResult;
24use vortex_error::vortex_bail;
25use vortex_error::vortex_ensure;
26use vortex_mask::AllOr;
27use vortex_mask::Mask;
28use vortex_session::VortexSession;
29use vortex_session::registry::CachedId;
30
31use crate::ArrayRef;
32use crate::ExecutionCtx;
33use crate::IntoArray;
34use crate::arrays::BoolArray;
35use crate::arrays::ConstantArray;
36use crate::arrays::bool::BoolArrayExt;
37use crate::builders::builder_with_capacity_in;
38use crate::builtins::ArrayBuiltins;
39use crate::dtype::DType;
40use crate::expr::BoundExpression;
41use crate::expr::display::ExprDisplay;
42use crate::proto::expr as pb;
43use crate::scalar::Scalar;
44use crate::scalar_fn::Arity;
45use crate::scalar_fn::ChildName;
46use crate::scalar_fn::EmptyOptions;
47use crate::scalar_fn::ExecutionArgs;
48use crate::scalar_fn::ScalarFnId;
49use crate::scalar_fn::ScalarFnVTable;
50use crate::scalar_fn::ScalarFnVTableExt;
51use crate::scalar_fn::fns::fill_null::FillNull;
52use crate::scalar_fn::fns::is_not_null::IsNotNull;
53use crate::scalar_fn::fns::is_null::IsNull;
54use crate::scalar_fn::fns::literal::Literal;
55use crate::scalar_fn::fns::zip::zip_nullability_union;
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub struct CaseWhenOptions {
60 pub num_when_then_pairs: u32,
62 pub has_else: bool,
65}
66
67impl CaseWhenOptions {
68 pub fn num_children(&self) -> usize {
70 self.num_when_then_pairs as usize * 2 + usize::from(self.has_else)
71 }
72}
73
74impl fmt::Display for CaseWhenOptions {
75 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
76 write!(
77 f,
78 "case_when(pairs={}, else={})",
79 self.num_when_then_pairs, self.has_else
80 )
81 }
82}
83
84#[derive(Clone)]
88pub struct CaseWhen;
89
90impl ScalarFnVTable for CaseWhen {
91 type Options = CaseWhenOptions;
92
93 fn id(&self) -> ScalarFnId {
94 static ID: CachedId = CachedId::new("vortex.case_when");
95 *ID
96 }
97
98 fn serialize(&self, options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
99 vortex_ensure!(
100 options.num_when_then_pairs > 0,
101 "CaseWhen must have at least one WHEN/THEN pair"
102 );
103 let Some(num_children) = options
104 .num_when_then_pairs
105 .checked_mul(2)
106 .and_then(|count| count.checked_add(u32::from(options.has_else)))
107 else {
108 vortex_bail!("CaseWhen has too many children to serialize");
109 };
110 Ok(Some(pb::CaseWhenOpts { num_children }.encode_to_vec()))
111 }
112
113 fn deserialize(
114 &self,
115 metadata: &[u8],
116 _session: &VortexSession,
117 ) -> VortexResult<Self::Options> {
118 let opts = pb::CaseWhenOpts::decode(metadata)?;
119 if opts.num_children < 2 {
120 vortex_bail!(
121 "CaseWhen expects at least 2 children, got {}",
122 opts.num_children
123 );
124 }
125 Ok(CaseWhenOptions {
126 num_when_then_pairs: opts.num_children / 2,
127 has_else: opts.num_children % 2 == 1,
128 })
129 }
130
131 fn arity(&self, options: &Self::Options) -> Arity {
132 Arity::Exact(options.num_children())
133 }
134
135 fn child_name(&self, options: &Self::Options, child_idx: usize) -> ChildName {
136 let num_pair_children = options.num_when_then_pairs as usize * 2;
137 if child_idx < num_pair_children {
138 let pair_idx = child_idx / 2;
139 if child_idx.is_multiple_of(2) {
140 ChildName::from(Arc::from(format!("when_{pair_idx}")))
141 } else {
142 ChildName::from(Arc::from(format!("then_{pair_idx}")))
143 }
144 } else if options.has_else && child_idx == num_pair_children {
145 ChildName::from("else")
146 } else {
147 unreachable!("Invalid child index {} for CaseWhen", child_idx)
148 }
149 }
150
151 fn fmt_sql(
152 &self,
153 options: &Self::Options,
154 expr: &dyn ExprDisplay,
155 f: &mut Formatter<'_>,
156 ) -> fmt::Result {
157 write!(f, "CASE")?;
158 for i in 0..options.num_when_then_pairs as usize {
159 write!(
160 f,
161 " WHEN {} THEN {}",
162 expr.display_child(i * 2),
163 expr.display_child(i * 2 + 1)
164 )?;
165 }
166 if options.has_else {
167 let else_idx = options.num_when_then_pairs as usize * 2;
168 write!(f, " ELSE {}", expr.display_child(else_idx))?;
169 }
170 write!(f, " END")
171 }
172
173 fn return_dtype(&self, options: &Self::Options, arg_dtypes: &[DType]) -> VortexResult<DType> {
174 if options.num_when_then_pairs == 0 {
175 vortex_bail!("CaseWhen must have at least one WHEN/THEN pair");
176 }
177
178 let expected_len = options.num_children();
179 if arg_dtypes.len() != expected_len {
180 vortex_bail!(
181 "CaseWhen expects {expected_len} argument dtypes, got {}",
182 arg_dtypes.len()
183 );
184 }
185
186 let first_then = &arg_dtypes[1];
190 let mut result_dtype = first_then.clone();
191
192 for i in 1..options.num_when_then_pairs as usize {
193 let then_i = &arg_dtypes[i * 2 + 1];
194 let Some(union) = zip_nullability_union(&result_dtype, then_i) else {
195 vortex_bail!(
196 "CaseWhen THEN dtypes must match (ignoring nullability), got {} and {}",
197 first_then,
198 then_i
199 );
200 };
201 result_dtype = union;
202 }
203
204 if options.has_else {
205 let else_dtype = &arg_dtypes[options.num_when_then_pairs as usize * 2];
206 let Some(union) = zip_nullability_union(&result_dtype, else_dtype) else {
207 vortex_bail!(
208 "CaseWhen THEN and ELSE dtypes must match (ignoring nullability), got {} and {}",
209 first_then,
210 else_dtype
211 );
212 };
213 result_dtype = union;
214 } else {
215 result_dtype = result_dtype.as_nullable();
217 }
218
219 Ok(result_dtype)
220 }
221
222 fn execute(
223 &self,
224 options: &Self::Options,
225 args: &dyn ExecutionArgs,
226 ctx: &mut ExecutionCtx,
227 ) -> VortexResult<ArrayRef> {
228 let row_count = args.row_count();
229 let num_pairs = options.num_when_then_pairs as usize;
230 let dtypes = (0..args.num_inputs())
231 .map(|index| Ok(args.get(index)?.dtype().clone()))
232 .collect::<VortexResult<Vec<_>>>()?;
233 let output_dtype = self.return_dtype(options, &dtypes)?;
234
235 let mut remaining = Mask::new_true(row_count);
236 let mut branches: Vec<(Mask, ArrayRef)> = Vec::with_capacity(num_pairs);
237
238 for i in 0..num_pairs {
239 if remaining.all_false() {
240 break;
241 }
242
243 let condition = args.get(i * 2)?.filter(remaining.clone())?;
244 let cond_bool = condition.execute::<BoolArray>(ctx)?;
245 let cond_mask = cond_bool.to_mask_fill_null_false(ctx);
246 let effective_mask = remaining.intersect_by_rank(&cond_mask);
247
248 if effective_mask.all_false() {
249 continue;
250 }
251
252 let then_value = args.get(i * 2 + 1)?;
253 remaining = remaining.bitand_not(&effective_mask);
254 branches.push((effective_mask, then_value));
255 }
256
257 let else_value: ArrayRef = if options.has_else {
258 args.get(num_pairs * 2)?
259 } else {
260 let then_dtype = args.get(1)?.dtype().as_nullable();
261 ConstantArray::new(Scalar::null(then_dtype), row_count).into_array()
262 };
263
264 if branches.is_empty() {
265 return else_value.cast(output_dtype);
266 }
267
268 merge_case_branches(&branches, &remaining, &else_value, &output_dtype, ctx)
270 }
271
272 fn simplify(
273 &self,
274 options: &Self::Options,
275 expr: &BoundExpression,
276 ) -> VortexResult<Option<BoundExpression>> {
277 if options.num_when_then_pairs != 1 || !options.has_else {
287 return Ok(None);
288 }
289
290 let when = expr.child(0);
291 let then = expr.child(1);
292 let els = expr.child(2);
293
294 let (x, fill) = if when.is::<IsNull>() && when.child(0) == els {
296 (els, then)
297 } else if when.is::<IsNotNull>() && when.child(0) == then {
299 (then, els)
300 } else {
301 return Ok(None);
302 };
303
304 let Some(scalar) = fill.as_opt::<Literal>() else {
305 return Ok(None);
306 };
307
308 if scalar.is_null() {
309 return Ok(Some(x.clone()));
311 }
312
313 if !FillNull::supports_dtype(x.dtype()) {
314 return Ok(None);
315 }
316
317 Ok(Some(FillNull.try_new_bound_expr(
318 EmptyOptions,
319 [x.clone(), fill.clone()],
320 )?))
321 }
322
323 fn is_strict(&self, _options: &Self::Options) -> bool {
324 false
326 }
327
328 fn is_infallible(&self, _options: &Self::Options) -> bool {
329 true
330 }
331}
332
333fn merge_case_branches(
338 branches: &[(Mask, ArrayRef)],
339 else_mask: &Mask,
340 else_value: &ArrayRef,
341 output_dtype: &DType,
342 ctx: &mut ExecutionCtx,
343) -> VortexResult<ArrayRef> {
344 if let Some((_, value)) = branches.iter().find(|(mask, _)| mask.all_true()) {
345 return value.cast(output_dtype.clone());
346 }
347
348 if prefer_runs(branches, else_value.len()) {
349 return merge_runs(branches, else_value, output_dtype, ctx);
350 }
351
352 merge_compact(branches, else_mask, else_value, output_dtype, ctx)
353}
354
355fn prefer_runs(branches: &[(Mask, ArrayRef)], len: usize) -> bool {
356 const MIN_ROWS_PER_RUN: usize = 128;
359
360 let mut remaining_spans = branches.len().max(len / (2 * MIN_ROWS_PER_RUN));
361 for (mask, _) in branches {
362 let count = match mask {
363 Mask::AllTrue(_) => 1,
364 Mask::AllFalse(_) => 0,
365 Mask::Values(values) => values.cached_slices().map_or_else(
366 || {
367 values
368 .bit_buffer()
369 .set_slices()
370 .take(remaining_spans.saturating_add(1))
371 .count()
372 },
373 |slices| slices.len(),
374 ),
375 };
376
377 if count > remaining_spans {
378 return false;
379 }
380 remaining_spans -= count;
381 }
382
383 true
384}
385
386fn merge_compact(
387 branches: &[(Mask, ArrayRef)],
388 else_mask: &Mask,
389 else_value: &ArrayRef,
390 output_dtype: &DType,
391 ctx: &mut ExecutionCtx,
392) -> VortexResult<ArrayRef> {
393 let len = else_value.len();
394 let mut builder = builder_with_capacity_in(output_dtype, len, ctx.allocator());
395 let mut indices = BufferMut::<u64>::zeroed_in(len, ctx.allocator().clone());
396
397 let else_branch = (!else_mask.all_false()).then_some((else_mask, else_value));
398 let selections = branches
399 .iter()
400 .map(|(mask, value)| (mask, value))
401 .chain(else_branch);
402
403 for (mask, value) in selections {
404 let offset = builder.len();
405 value
406 .filter(mask.clone())?
407 .cast(output_dtype.clone())?
408 .append_to_builder(builder.as_mut(), ctx)?;
409 assign_positions(&mut indices, mask, offset);
410 }
411
412 builder.finish().take(indices.freeze().into_array())
413}
414
415fn merge_runs(
416 branches: &[(Mask, ArrayRef)],
417 else_value: &ArrayRef,
418 output_dtype: &DType,
419 ctx: &mut ExecutionCtx,
420) -> VortexResult<ArrayRef> {
421 let mut spans = Vec::new();
422 for (index, (mask, _)) in branches.iter().enumerate() {
423 if let AllOr::Some(slices) = mask.slices() {
424 spans.extend(slices.iter().map(|&(start, end)| (start, end, index)));
425 }
426 }
427 spans.sort_unstable_by_key(|&(start, ..)| start);
428
429 let values = branches
431 .iter()
432 .map(|(_, value)| value.cast(output_dtype.clone()))
433 .collect::<VortexResult<Vec<_>>>()?;
434 let else_value = else_value.cast(output_dtype.clone())?;
435 let len = else_value.len();
436
437 let mut builder = builder_with_capacity_in(output_dtype, len, ctx.allocator());
438 for (start, end, branch) in spans {
439 if builder.len() < start {
440 else_value
441 .slice(builder.len()..start)?
442 .append_to_builder(builder.as_mut(), ctx)?;
443 }
444
445 values[branch]
446 .slice(start..end)?
447 .append_to_builder(builder.as_mut(), ctx)?;
448 }
449
450 if builder.len() < len {
451 else_value
452 .slice(builder.len()..len)?
453 .append_to_builder(builder.as_mut(), ctx)?;
454 }
455
456 Ok(builder.finish())
457}
458
459fn assign_positions(indices: &mut [u64], mask: &Mask, offset: usize) {
460 match mask.indices() {
461 AllOr::All => {
462 for (position, index) in indices.iter_mut().enumerate() {
463 *index = (offset + position) as u64;
464 }
465 }
466 AllOr::None => {}
467 AllOr::Some(selected) => {
468 for (position, &row) in selected.iter().enumerate() {
469 indices[row] = (offset + position) as u64;
470 }
471 }
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use std::sync::LazyLock;
478
479 use rstest::rstest;
480 use vortex_buffer::Buffer;
481 use vortex_buffer::buffer;
482 use vortex_error::VortexExpect as _;
483 use vortex_session::VortexSession;
484
485 use super::*;
486 use crate::Canonical;
487 use crate::IntoArray;
488 use crate::VortexSessionExecute;
489 use crate::arrays::BoolArray;
490 use crate::arrays::DecimalArray;
491 use crate::arrays::PrimitiveArray;
492 use crate::arrays::ScalarFnArray;
493 use crate::arrays::StructArray;
494 use crate::arrays::VarBinViewArray;
495 use crate::assert_arrays_eq;
496 use crate::dtype::DType;
497 use crate::dtype::DecimalDType;
498 use crate::dtype::Nullability;
499 use crate::dtype::PType;
500 use crate::dtype::StructFields;
501 use crate::expr::Expression;
502 use crate::expr::case_when;
503 use crate::expr::case_when_no_else;
504 use crate::expr::col;
505 use crate::expr::eq;
506 use crate::expr::get_item;
507 use crate::expr::gt;
508 use crate::expr::is_not_null;
509 use crate::expr::is_null;
510 use crate::expr::lit;
511 use crate::expr::nested_case_when;
512 use crate::expr::proto::ExprSerializeProtoExt;
513 use crate::expr::root;
514 use crate::expr::test_harness;
515 use crate::scalar::Scalar;
516 use crate::scalar_fn::ScalarFnVTableExt;
517 use crate::scalar_fn::VecExecutionArgs;
518 use crate::scalar_fn::fns::operators::Operator;
519 use crate::validity::Validity;
520
521 static SESSION: LazyLock<VortexSession> = LazyLock::new(crate::array_session);
522
523 #[test]
524 fn compact_and_run_merges_agree_for_nested_and_nullable_values() -> VortexResult<()> {
525 let primitive =
526 PrimitiveArray::from_option_iter([Some(1i64), None, Some(3), Some(4)]).into_array();
527 let fixtures = vec![
528 primitive.clone(),
529 DecimalArray::new(
530 buffer![100i64, -200, 300, 400],
531 DecimalDType::new(15, 2),
532 Validity::from_iter([true, false, true, true]),
533 )
534 .into_array(),
535 DecimalArray::new(
536 buffer![1i128 << 100, -2, 3, 4],
537 DecimalDType::new(38, 0),
538 Validity::NonNullable,
539 )
540 .into_array(),
541 VarBinViewArray::from_iter(
542 [
543 Some("long out-of-line value"),
544 None,
545 Some("short"),
546 Some(""),
547 ],
548 DType::Utf8(Nullability::Nullable),
549 )
550 .into_array(),
551 StructArray::try_from_iter([("nested", primitive)])?.into_array(),
552 ];
553 let cases: [&[[bool; 4]]; 5] = [
556 &[[true, false, true, false]],
557 &[[true, true, false, false]],
558 &[[false, true, false, false]],
559 &[[true, false, false, false], [false, false, true, false]],
560 &[[false, false, false, true], [false, true, false, false]],
561 ];
562
563 let mut ctx = SESSION.create_execution_ctx();
564 for value in fixtures {
565 let reorder =
566 |indices: Buffer<u32>, ctx: &mut ExecutionCtx| -> VortexResult<ArrayRef> {
567 Ok(value
568 .take(indices.into_array())?
569 .execute::<Canonical>(ctx)?
570 .into_array())
571 };
572 let branch_values = [value.clone(), reorder(buffer![1u32, 0, 3, 2], &mut ctx)?];
573 let otherwise = reorder(buffer![3u32, 2, 1, 0], &mut ctx)?;
574
575 for selections in cases {
576 let branches: Vec<_> = selections
577 .iter()
578 .zip(&branch_values)
579 .map(|(selection, value)| (Mask::from_iter(*selection), value.clone()))
580 .collect();
581 let else_mask = Mask::from_iter(
582 (0..4).map(|row| !selections.iter().any(|selection| selection[row])),
583 );
584
585 assert_arrays_eq!(
586 merge_runs(&branches, &otherwise, value.dtype(), &mut ctx)?,
587 merge_compact(&branches, &else_mask, &otherwise, value.dtype(), &mut ctx)?,
588 &mut ctx
589 );
590 }
591 }
592
593 Ok(())
594 }
595
596 fn nested_struct(values: [Option<i32>; 4], nullable: bool) -> VortexResult<ArrayRef> {
597 let field = if nullable {
598 PrimitiveArray::from_option_iter(values).into_array()
599 } else {
600 PrimitiveArray::from_iter(values.map(|value| value.vortex_expect("non-null value")))
601 .into_array()
602 };
603
604 Ok(StructArray::try_from_iter([("a", field)])?.into_array())
605 }
606
607 #[rstest]
610 #[case::compact_nullable_then([true, false, true, false], true)]
611 #[case::compact_nullable_else([true, false, true, false], false)]
612 #[case::runs_nullable_then([true, true, false, false], true)]
613 #[case::runs_nullable_else([true, true, false, false], false)]
614 #[case::no_match_nullable_else([false, false, false, false], false)]
615 fn nested_nullability_is_merged_across_branches(
616 #[case] selection: [bool; 4],
617 #[case] nullable_then: bool,
618 ) -> VortexResult<()> {
619 let nullable = [None, Some(2), None, Some(4)];
620 let non_nullable = [Some(10), Some(20), Some(30), Some(40)];
621 let (then_values, else_values) = if nullable_then {
622 (nullable, non_nullable)
623 } else {
624 (non_nullable, nullable)
625 };
626
627 let array = ScalarFnArray::try_new(
628 CaseWhen.bind(CaseWhenOptions {
629 num_when_then_pairs: 1,
630 has_else: true,
631 }),
632 vec![
633 BoolArray::from_iter(selection).into_array(),
634 nested_struct(then_values, nullable_then)?,
635 nested_struct(else_values, !nullable_then)?,
636 ],
637 )?
638 .into_array();
639
640 let expected = (0..4).map(|row| {
641 if selection[row] {
642 then_values[row]
643 } else {
644 else_values[row]
645 }
646 });
647 let expected = StructArray::try_from_iter([(
648 "a",
649 PrimitiveArray::from_option_iter(expected).into_array(),
650 )])?
651 .into_array();
652
653 let mut ctx = SESSION.create_execution_ctx();
654 assert_arrays_eq!(array, expected, &mut ctx);
655
656 Ok(())
657 }
658
659 #[test]
660 fn test_return_dtype_merges_nested_nullability() -> VortexResult<()> {
661 let field = |nullability| {
662 DType::Struct(
663 StructFields::new(
664 ["a"].into(),
665 vec![DType::Primitive(PType::I32, nullability)],
666 ),
667 Nullability::NonNullable,
668 )
669 };
670 let options = CaseWhenOptions {
671 num_when_then_pairs: 1,
672 has_else: true,
673 };
674 let condition = DType::Bool(Nullability::NonNullable);
675
676 for (then_dtype, else_dtype) in [
677 (
678 field(Nullability::Nullable),
679 field(Nullability::NonNullable),
680 ),
681 (
682 field(Nullability::NonNullable),
683 field(Nullability::Nullable),
684 ),
685 ] {
686 assert_eq!(
687 CaseWhen.return_dtype(&options, &[condition.clone(), then_dtype, else_dtype])?,
688 field(Nullability::Nullable)
689 );
690 }
691
692 Ok(())
693 }
694
695 #[test]
696 fn selected_rows_do_not_execute_erroring_branches_or_later_conditions() -> VortexResult<()> {
697 let mut ctx = SESSION.create_execution_ctx();
698 let condition =
699 BoolArray::from_iter([Some(true), Some(false), Some(true), None]).into_array();
700 let division = buffer![20i32, 30, 40, 50]
701 .into_array()
702 .binary(buffer![2i32, 0, 4, 0].into_array(), Operator::Div)?;
703 assert!(division.clone().execute::<Canonical>(&mut ctx).is_err());
704 let options = CaseWhenOptions {
705 num_when_then_pairs: 1,
706 has_else: true,
707 };
708 let result = CaseWhen.execute(
709 &options,
710 &VecExecutionArgs::new(
711 vec![
712 condition.clone(),
713 division.clone(),
714 ConstantArray::new(7i32, 4).into_array(),
715 ],
716 4,
717 ),
718 &mut ctx,
719 )?;
720 assert_arrays_eq!(result, buffer![10i32, 7, 10, 7].into_array(), &mut ctx);
721
722 let result = CaseWhen.execute(
723 &options,
724 &VecExecutionArgs::new(
725 vec![
726 condition.clone(),
727 StructArray::try_from_iter([("value", division.clone())])?.into_array(),
728 StructArray::try_from_iter([(
729 "value",
730 ConstantArray::new(7i32, 4).into_array(),
731 )])?
732 .into_array(),
733 ],
734 4,
735 ),
736 &mut ctx,
737 )?;
738 assert_arrays_eq!(
739 result,
740 StructArray::try_from_iter([("value", buffer![10i32, 7, 10, 7].into_array())])?
741 .into_array(),
742 &mut ctx
743 );
744
745 let later_condition = buffer![20i32, 30, 40, 50]
746 .into_array()
747 .binary(buffer![0i32, 3, 0, 5].into_array(), Operator::Div)?
748 .binary(ConstantArray::new(0i32, 4).into_array(), Operator::Gt)?;
749 let result = CaseWhen.execute(
750 &CaseWhenOptions {
751 num_when_then_pairs: 2,
752 has_else: true,
753 },
754 &VecExecutionArgs::new(
755 vec![
756 condition,
757 ConstantArray::new(2i32, 4).into_array(),
758 later_condition,
759 ConstantArray::new(3i32, 4).into_array(),
760 division,
761 ],
762 4,
763 ),
764 &mut ctx,
765 )?;
766 assert_arrays_eq!(result, buffer![2i32, 3, 2, 3].into_array(), &mut ctx);
767
768 let selected_error = ScalarFnArray::try_new(
769 CaseWhen.bind(options),
770 vec![
771 ConstantArray::new(true, 4).into_array(),
772 buffer![20i32, 30, 40, 50]
773 .into_array()
774 .binary(buffer![2i32, 0, 4, 0].into_array(), Operator::Div)?,
775 ConstantArray::new(7i32, 4).into_array(),
776 ],
777 )?
778 .into_array();
779 assert!(selected_error.execute::<Canonical>(&mut ctx).is_err());
780 Ok(())
781 }
782
783 fn apply_expr(expr: &Expression, array: &ArrayRef) -> ArrayRef {
784 array.clone().apply(expr).unwrap()
785 }
786
787 fn apply_bound_expr(expr: &BoundExpression, array: &ArrayRef) -> ArrayRef {
788 array.clone().apply_bound(expr).unwrap()
789 }
790
791 #[rstest]
794 #[case(1, true, 3)]
795 #[case(1, false, 2)]
796 #[case(3, true, 7)]
797 #[case(4, false, 8)]
798 #[case(u32::MAX / 2, true, u32::MAX)]
799 #[case(u32::MAX / 2, false, u32::MAX - 1)]
800 fn test_serialization_roundtrip(
801 #[case] num_when_then_pairs: u32,
802 #[case] has_else: bool,
803 #[case] num_children: u32,
804 ) -> VortexResult<()> {
805 let options = CaseWhenOptions {
806 num_when_then_pairs,
807 has_else,
808 };
809 let serialized = CaseWhen.serialize(&options)?.expect("serializable options");
810 assert_eq!(
811 pb::CaseWhenOpts::decode(serialized.as_slice())?.num_children,
812 num_children
813 );
814 assert_eq!(
815 CaseWhen.deserialize(&serialized, &VortexSession::empty())?,
816 options
817 );
818 Ok(())
819 }
820
821 #[rstest]
822 #[case(0, "at least one WHEN/THEN pair")]
823 #[case(u32::MAX / 2 + 1, "too many children")]
824 fn test_serialization_invalid_options(
825 #[case] num_when_then_pairs: u32,
826 #[case] expected_error: &str,
827 #[values(false, true)] has_else: bool,
828 ) {
829 let options = CaseWhenOptions {
830 num_when_then_pairs,
831 has_else,
832 };
833 let error = CaseWhen.serialize(&options).unwrap_err();
834 assert!(error.to_string().contains(expected_error));
835 }
836
837 #[rstest]
838 #[case::missing(&[])]
839 #[case::zero_children(&[0x08, 0])]
840 #[case::one_child(&[0x08, 1])]
841 #[case::truncated(&[0x08, 0x80])]
842 fn test_deserialization_invalid_metadata(#[case] metadata: &[u8]) {
843 assert!(
844 CaseWhen
845 .deserialize(metadata, &VortexSession::empty())
846 .is_err()
847 );
848 }
849
850 #[rstest]
851 fn test_expression_serialization_roundtrip(
852 #[values(1, 3)] num_when_then_pairs: u32,
853 #[values(false, true)] has_else: bool,
854 ) -> VortexResult<()> {
855 let branches = (0..num_when_then_pairs)
856 .map(|value| (eq(col("value"), lit(value)), lit(value)))
857 .collect();
858 let expression = nested_case_when(branches, has_else.then(|| lit(42u32)));
859 let bytes = expression.serialize_proto()?.encode_to_vec();
860 let proto = pb::Expr::decode(bytes.as_slice())?;
861 assert_eq!(proto.id, "vortex.case_when");
862 assert_eq!(Expression::from_proto(&proto, &SESSION)?, expression);
863 Ok(())
864 }
865
866 #[test]
869 fn test_display_with_else() {
870 let expr = case_when(gt(col("value"), lit(0i32)), lit(100i32), lit(0i32));
871 let display = format!("{}", expr);
872 assert!(display.contains("CASE"));
873 assert!(display.contains("WHEN"));
874 assert!(display.contains("THEN"));
875 assert!(display.contains("ELSE"));
876 assert!(display.contains("END"));
877 }
878
879 #[test]
880 fn test_display_no_else() {
881 let expr = case_when_no_else(gt(col("value"), lit(0i32)), lit(100i32));
882 let display = format!("{}", expr);
883 assert!(display.contains("CASE"));
884 assert!(display.contains("WHEN"));
885 assert!(display.contains("THEN"));
886 assert!(!display.contains("ELSE"));
887 assert!(display.contains("END"));
888 }
889
890 #[test]
891 fn test_display_nested_nary() {
892 let expr = nested_case_when(
894 vec![
895 (gt(col("x"), lit(10i32)), lit("high")),
896 (gt(col("x"), lit(5i32)), lit("medium")),
897 ],
898 Some(lit("low")),
899 );
900 let display = format!("{}", expr);
901 assert_eq!(display.matches("CASE").count(), 1);
902 assert_eq!(display.matches("WHEN").count(), 2);
903 assert_eq!(display.matches("THEN").count(), 2);
904 }
905
906 #[test]
909 fn test_return_dtype_with_else() {
910 let expr = case_when(lit(true), lit(100i32), lit(0i32));
911 let input_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
912 let result_dtype = expr.return_dtype(&input_dtype).unwrap();
913 assert_eq!(
914 result_dtype,
915 DType::Primitive(PType::I32, Nullability::NonNullable)
916 );
917 }
918
919 #[test]
920 fn test_return_dtype_with_nullable_else() {
921 let expr = case_when(
922 lit(true),
923 lit(100i32),
924 lit(Scalar::null(DType::Primitive(
925 PType::I32,
926 Nullability::Nullable,
927 ))),
928 );
929 let input_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
930 let result_dtype = expr.return_dtype(&input_dtype).unwrap();
931 assert_eq!(
932 result_dtype,
933 DType::Primitive(PType::I32, Nullability::Nullable)
934 );
935 }
936
937 #[test]
938 fn test_return_dtype_without_else_is_nullable() {
939 let expr = case_when_no_else(lit(true), lit(100i32));
940 let input_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
941 let result_dtype = expr.return_dtype(&input_dtype).unwrap();
942 assert_eq!(
943 result_dtype,
944 DType::Primitive(PType::I32, Nullability::Nullable)
945 );
946 }
947
948 #[test]
949 fn test_return_dtype_with_struct_input() {
950 let dtype = test_harness::struct_dtype();
951 let expr = case_when(
952 gt(get_item("col1", root()), lit(10u16)),
953 lit(100i32),
954 lit(0i32),
955 );
956 let result_dtype = expr.return_dtype(&dtype).unwrap();
957 assert_eq!(
958 result_dtype,
959 DType::Primitive(PType::I32, Nullability::NonNullable)
960 );
961 }
962
963 #[test]
964 fn test_return_dtype_mismatched_then_else_errors() {
965 let expr = case_when(lit(true), lit(100i32), lit("zero"));
966 let input_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
967 let err = expr.return_dtype(&input_dtype).unwrap_err();
968 assert!(
969 err.to_string()
970 .contains("THEN and ELSE dtypes must match (ignoring nullability)")
971 );
972 }
973
974 #[test]
977 fn test_arity_with_else() {
978 let options = CaseWhenOptions {
979 num_when_then_pairs: 1,
980 has_else: true,
981 };
982 assert_eq!(CaseWhen.arity(&options), Arity::Exact(3));
983 }
984
985 #[test]
986 fn test_arity_without_else() {
987 let options = CaseWhenOptions {
988 num_when_then_pairs: 1,
989 has_else: false,
990 };
991 assert_eq!(CaseWhen.arity(&options), Arity::Exact(2));
992 }
993
994 #[test]
997 fn test_child_names() {
998 let options = CaseWhenOptions {
999 num_when_then_pairs: 1,
1000 has_else: true,
1001 };
1002 assert_eq!(CaseWhen.child_name(&options, 0).to_string(), "when_0");
1003 assert_eq!(CaseWhen.child_name(&options, 1).to_string(), "then_0");
1004 assert_eq!(CaseWhen.child_name(&options, 2).to_string(), "else");
1005 }
1006
1007 #[test]
1010 fn test_arity_nary_with_else() {
1011 let options = CaseWhenOptions {
1012 num_when_then_pairs: 3,
1013 has_else: true,
1014 };
1015 assert_eq!(CaseWhen.arity(&options), Arity::Exact(7));
1017 }
1018
1019 #[test]
1020 fn test_arity_nary_without_else() {
1021 let options = CaseWhenOptions {
1022 num_when_then_pairs: 3,
1023 has_else: false,
1024 };
1025 assert_eq!(CaseWhen.arity(&options), Arity::Exact(6));
1027 }
1028
1029 #[test]
1032 fn test_child_names_nary() {
1033 let options = CaseWhenOptions {
1034 num_when_then_pairs: 3,
1035 has_else: true,
1036 };
1037 assert_eq!(CaseWhen.child_name(&options, 0).to_string(), "when_0");
1038 assert_eq!(CaseWhen.child_name(&options, 1).to_string(), "then_0");
1039 assert_eq!(CaseWhen.child_name(&options, 2).to_string(), "when_1");
1040 assert_eq!(CaseWhen.child_name(&options, 3).to_string(), "then_1");
1041 assert_eq!(CaseWhen.child_name(&options, 4).to_string(), "when_2");
1042 assert_eq!(CaseWhen.child_name(&options, 5).to_string(), "then_2");
1043 assert_eq!(CaseWhen.child_name(&options, 6).to_string(), "else");
1044 }
1045
1046 #[test]
1049 fn test_return_dtype_nary_mismatched_then_types_errors() {
1050 let expr = nested_case_when(
1051 vec![(lit(true), lit(100i32)), (lit(false), lit("oops"))],
1052 Some(lit(0i32)),
1053 );
1054 let input_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
1055 let err = expr.return_dtype(&input_dtype).unwrap_err();
1056 assert!(err.to_string().contains("THEN dtypes must match"));
1057 }
1058
1059 #[test]
1060 fn test_return_dtype_nary_mixed_nullability() {
1061 let non_null_then = lit(100i32);
1064 let nullable_then = lit(Scalar::null(DType::Primitive(
1065 PType::I32,
1066 Nullability::Nullable,
1067 )));
1068 let expr = nested_case_when(
1069 vec![(lit(true), non_null_then), (lit(false), nullable_then)],
1070 Some(lit(0i32)),
1071 );
1072 let input_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
1073 let result = expr.return_dtype(&input_dtype).unwrap();
1074 assert_eq!(result, DType::Primitive(PType::I32, Nullability::Nullable));
1075 }
1076
1077 #[test]
1078 fn test_return_dtype_nary_no_else_is_nullable() {
1079 let expr = nested_case_when(
1080 vec![(lit(true), lit(10i32)), (lit(false), lit(20i32))],
1081 None,
1082 );
1083 let input_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
1084 let result = expr.return_dtype(&input_dtype).unwrap();
1085 assert_eq!(result, DType::Primitive(PType::I32, Nullability::Nullable));
1086 }
1087
1088 #[test]
1091 fn test_replace_children() {
1092 let expr = case_when(lit(true), lit(1i32), lit(0i32));
1093 let replaced = expr
1094 .with_children([lit(false), lit(2i32), lit(3i32)])
1095 .vortex_expect("operation should succeed in test");
1096 assert_eq!(replaced, case_when(lit(false), lit(2i32), lit(3i32)));
1097 }
1098
1099 #[test]
1102 fn test_evaluate_simple_condition() {
1103 let mut ctx = SESSION.create_execution_ctx();
1104 let test_array =
1105 StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())])
1106 .unwrap()
1107 .into_array();
1108
1109 let expr = case_when(
1110 gt(get_item("value", root()), lit(2i32)),
1111 lit(100i32),
1112 lit(0i32),
1113 );
1114
1115 let result = apply_expr(&expr, &test_array);
1116 assert_arrays_eq!(
1117 result,
1118 buffer![0i32, 0, 100, 100, 100].into_array(),
1119 &mut ctx
1120 );
1121 }
1122
1123 #[test]
1124 fn test_evaluate_nary_multiple_conditions() {
1125 let mut ctx = SESSION.create_execution_ctx();
1126 let test_array =
1128 StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())])
1129 .unwrap()
1130 .into_array();
1131
1132 let expr = nested_case_when(
1133 vec![
1134 (eq(get_item("value", root()), lit(1i32)), lit(10i32)),
1135 (eq(get_item("value", root()), lit(3i32)), lit(30i32)),
1136 ],
1137 Some(lit(0i32)),
1138 );
1139
1140 let result = apply_expr(&expr, &test_array);
1141 assert_arrays_eq!(result, buffer![10i32, 0, 30, 0, 0].into_array(), &mut ctx);
1142 }
1143
1144 #[test]
1145 fn test_evaluate_nary_first_match_wins() {
1146 let mut ctx = SESSION.create_execution_ctx();
1147 let test_array =
1148 StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())])
1149 .unwrap()
1150 .into_array();
1151
1152 let expr = nested_case_when(
1154 vec![
1155 (gt(get_item("value", root()), lit(2i32)), lit(100i32)),
1156 (gt(get_item("value", root()), lit(3i32)), lit(200i32)),
1157 ],
1158 Some(lit(0i32)),
1159 );
1160
1161 let result = apply_expr(&expr, &test_array);
1162 assert_arrays_eq!(
1163 result,
1164 buffer![0i32, 0, 100, 100, 100].into_array(),
1165 &mut ctx
1166 );
1167 }
1168
1169 #[test]
1170 fn test_evaluate_no_else_returns_null() {
1171 let mut ctx = SESSION.create_execution_ctx();
1172 let test_array =
1173 StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())])
1174 .unwrap()
1175 .into_array();
1176
1177 let expr = case_when_no_else(gt(get_item("value", root()), lit(3i32)), lit(100i32));
1178
1179 let result = apply_expr(&expr, &test_array);
1180 assert!(result.dtype().is_nullable());
1181 assert_arrays_eq!(
1182 result,
1183 PrimitiveArray::from_option_iter([None::<i32>, None, None, Some(100), Some(100)])
1184 .into_array(),
1185 &mut ctx
1186 );
1187 }
1188
1189 #[test]
1190 fn test_evaluate_all_conditions_false() {
1191 let mut ctx = SESSION.create_execution_ctx();
1192 let test_array =
1193 StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())])
1194 .unwrap()
1195 .into_array();
1196
1197 let expr = case_when(
1198 gt(get_item("value", root()), lit(100i32)),
1199 lit(1i32),
1200 lit(0i32),
1201 );
1202
1203 let result = apply_expr(&expr, &test_array);
1204 assert_arrays_eq!(result, buffer![0i32, 0, 0, 0, 0].into_array(), &mut ctx);
1205 }
1206
1207 #[test]
1208 fn test_evaluate_all_conditions_true() {
1209 let mut ctx = SESSION.create_execution_ctx();
1210 let test_array =
1211 StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())])
1212 .unwrap()
1213 .into_array();
1214
1215 let expr = case_when(
1216 gt(get_item("value", root()), lit(0i32)),
1217 lit(100i32),
1218 lit(0i32),
1219 );
1220
1221 let result = apply_expr(&expr, &test_array);
1222 assert_arrays_eq!(
1223 result,
1224 buffer![100i32, 100, 100, 100, 100].into_array(),
1225 &mut ctx
1226 );
1227 }
1228
1229 #[test]
1230 fn test_evaluate_all_true_no_else_returns_correct_dtype() {
1231 let mut ctx = SESSION.create_execution_ctx();
1232 let test_array = StructArray::from_fields(&[("value", buffer![1i32, 2, 3].into_array())])
1235 .unwrap()
1236 .into_array();
1237
1238 let expr = case_when_no_else(gt(get_item("value", root()), lit(0i32)), lit(100i32));
1239
1240 let result = apply_expr(&expr, &test_array);
1241 assert!(
1242 result.dtype().is_nullable(),
1243 "result dtype must be Nullable, got {:?}",
1244 result.dtype()
1245 );
1246 assert_arrays_eq!(
1247 result,
1248 PrimitiveArray::from_option_iter([Some(100i32), Some(100), Some(100)]).into_array(),
1249 &mut ctx
1250 );
1251 }
1252
1253 #[test]
1254 fn test_merge_case_branches_widens_nullability_of_later_branch() -> VortexResult<()> {
1255 let mut ctx = SESSION.create_execution_ctx();
1256 let test_array =
1264 StructArray::from_fields(&[("value", buffer![0i32, 1, 2].into_array())])?.into_array();
1265
1266 let nullable_20 =
1267 Scalar::from(20i32).cast(&DType::Primitive(PType::I32, Nullability::Nullable))?;
1268
1269 let expr = nested_case_when(
1270 vec![
1271 (eq(get_item("value", root()), lit(0i32)), lit(10i32)),
1272 (eq(get_item("value", root()), lit(1i32)), lit(nullable_20)),
1273 ],
1274 Some(lit(0i32)),
1275 );
1276
1277 let result = apply_expr(&expr, &test_array);
1278 assert!(
1279 result.dtype().is_nullable(),
1280 "result dtype must be Nullable, got {:?}",
1281 result.dtype()
1282 );
1283 assert_arrays_eq!(
1284 result,
1285 PrimitiveArray::from_option_iter([Some(10), Some(20), Some(0)]).into_array(),
1286 &mut ctx
1287 );
1288 Ok(())
1289 }
1290
1291 #[test]
1292 fn test_evaluate_with_literal_condition() {
1293 let mut ctx = SESSION.create_execution_ctx();
1294 let test_array = buffer![1i32, 2, 3].into_array();
1295 let expr = case_when(lit(true), lit(100i32), lit(0i32));
1296 let result = apply_expr(&expr, &test_array);
1297
1298 assert_arrays_eq!(result, buffer![100i32, 100, 100].into_array(), &mut ctx);
1299 }
1300
1301 #[test]
1302 fn test_evaluate_with_bool_column_result() {
1303 let mut ctx = SESSION.create_execution_ctx();
1304 let test_array =
1305 StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())])
1306 .unwrap()
1307 .into_array();
1308
1309 let expr = case_when(
1310 gt(get_item("value", root()), lit(2i32)),
1311 lit(true),
1312 lit(false),
1313 );
1314
1315 let result = apply_expr(&expr, &test_array);
1316 assert_arrays_eq!(
1317 result,
1318 BoolArray::from_iter([false, false, true, true, true]).into_array(),
1319 &mut ctx
1320 );
1321 }
1322
1323 #[test]
1324 fn test_evaluate_with_nullable_condition() {
1325 let mut ctx = SESSION.create_execution_ctx();
1326 let test_array = StructArray::from_fields(&[(
1327 "cond",
1328 BoolArray::from_iter([Some(true), None, Some(false), None, Some(true)]).into_array(),
1329 )])
1330 .unwrap()
1331 .into_array();
1332
1333 let expr = case_when(get_item("cond", root()), lit(100i32), lit(0i32));
1334
1335 let result = apply_expr(&expr, &test_array);
1336 assert_arrays_eq!(result, buffer![100i32, 0, 0, 0, 100].into_array(), &mut ctx);
1337 }
1338
1339 #[test]
1340 fn test_evaluate_with_nullable_result_values() {
1341 let mut ctx = SESSION.create_execution_ctx();
1342 let test_array = StructArray::from_fields(&[
1343 ("value", buffer![1i32, 2, 3, 4, 5].into_array()),
1344 (
1345 "result",
1346 PrimitiveArray::from_option_iter([Some(10), None, Some(30), None, Some(50)])
1347 .into_array(),
1348 ),
1349 ])
1350 .unwrap()
1351 .into_array();
1352
1353 let expr = case_when(
1354 gt(get_item("value", root()), lit(2i32)),
1355 get_item("result", root()),
1356 lit(0i32),
1357 );
1358
1359 let result = apply_expr(&expr, &test_array);
1360 assert_arrays_eq!(
1361 result,
1362 PrimitiveArray::from_option_iter([Some(0i32), Some(0), Some(30), None, Some(50)])
1363 .into_array(),
1364 &mut ctx
1365 );
1366 }
1367
1368 #[test]
1369 fn test_evaluate_with_all_null_condition() {
1370 let mut ctx = SESSION.create_execution_ctx();
1371 let test_array = StructArray::from_fields(&[(
1372 "cond",
1373 BoolArray::from_iter([None, None, None]).into_array(),
1374 )])
1375 .unwrap()
1376 .into_array();
1377
1378 let expr = case_when(get_item("cond", root()), lit(100i32), lit(0i32));
1379
1380 let result = apply_expr(&expr, &test_array);
1381 assert_arrays_eq!(result, buffer![0i32, 0, 0].into_array(), &mut ctx);
1382 }
1383
1384 #[test]
1387 fn test_evaluate_nary_no_else_returns_null() {
1388 let mut ctx = SESSION.create_execution_ctx();
1389 let test_array =
1390 StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())])
1391 .unwrap()
1392 .into_array();
1393
1394 let expr = nested_case_when(
1396 vec![
1397 (eq(get_item("value", root()), lit(1i32)), lit(10i32)),
1398 (eq(get_item("value", root()), lit(3i32)), lit(30i32)),
1399 ],
1400 None,
1401 );
1402
1403 let result = apply_expr(&expr, &test_array);
1404 assert!(result.dtype().is_nullable());
1405 assert_arrays_eq!(
1406 result,
1407 PrimitiveArray::from_option_iter([Some(10i32), None, Some(30), None, None])
1408 .into_array(),
1409 &mut ctx
1410 );
1411 }
1412
1413 #[test]
1414 fn test_evaluate_nary_many_conditions() {
1415 let mut ctx = SESSION.create_execution_ctx();
1416 let test_array =
1417 StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())])
1418 .unwrap()
1419 .into_array();
1420
1421 let expr = nested_case_when(
1423 vec![
1424 (eq(get_item("value", root()), lit(1i32)), lit(10i32)),
1425 (eq(get_item("value", root()), lit(2i32)), lit(20i32)),
1426 (eq(get_item("value", root()), lit(3i32)), lit(30i32)),
1427 (eq(get_item("value", root()), lit(4i32)), lit(40i32)),
1428 (eq(get_item("value", root()), lit(5i32)), lit(50i32)),
1429 ],
1430 Some(lit(0i32)),
1431 );
1432
1433 let result = apply_expr(&expr, &test_array);
1434 assert_arrays_eq!(
1435 result,
1436 buffer![10i32, 20, 30, 40, 50].into_array(),
1437 &mut ctx
1438 );
1439 }
1440
1441 #[test]
1442 fn test_evaluate_nary_all_false_no_else() {
1443 let mut ctx = SESSION.create_execution_ctx();
1444 let test_array = StructArray::from_fields(&[("value", buffer![1i32, 2, 3].into_array())])
1445 .unwrap()
1446 .into_array();
1447
1448 let expr = nested_case_when(
1450 vec![
1451 (gt(get_item("value", root()), lit(100i32)), lit(10i32)),
1452 (gt(get_item("value", root()), lit(200i32)), lit(20i32)),
1453 ],
1454 None,
1455 );
1456
1457 let result = apply_expr(&expr, &test_array);
1458 assert!(result.dtype().is_nullable());
1459 assert_arrays_eq!(
1460 result,
1461 PrimitiveArray::from_option_iter([None::<i32>, None, None]).into_array(),
1462 &mut ctx
1463 );
1464 }
1465
1466 #[test]
1467 fn test_evaluate_nary_overlapping_conditions_first_wins() {
1468 let mut ctx = SESSION.create_execution_ctx();
1469 let test_array =
1470 StructArray::from_fields(&[("value", buffer![10i32, 20, 30].into_array())])
1471 .unwrap()
1472 .into_array();
1473
1474 let expr = nested_case_when(
1478 vec![
1479 (gt(get_item("value", root()), lit(5i32)), lit(1i32)),
1480 (gt(get_item("value", root()), lit(0i32)), lit(2i32)),
1481 (gt(get_item("value", root()), lit(15i32)), lit(3i32)),
1482 ],
1483 Some(lit(0i32)),
1484 );
1485
1486 let result = apply_expr(&expr, &test_array);
1487 assert_arrays_eq!(result, buffer![1i32, 1, 1].into_array(), &mut ctx);
1489 }
1490
1491 #[test]
1492 fn test_evaluate_nary_early_exit_when_remaining_empty() {
1493 let mut ctx = SESSION.create_execution_ctx();
1494 let test_array = StructArray::from_fields(&[("value", buffer![1i32, 2, 3].into_array())])
1497 .unwrap()
1498 .into_array();
1499
1500 let expr = nested_case_when(
1501 vec![
1502 (gt(get_item("value", root()), lit(0i32)), lit(100i32)),
1503 (gt(get_item("value", root()), lit(0i32)), lit(999i32)),
1505 ],
1506 Some(lit(0i32)),
1507 );
1508
1509 let result = apply_expr(&expr, &test_array);
1510 assert_arrays_eq!(result, buffer![100i32, 100, 100].into_array(), &mut ctx);
1511 }
1512
1513 #[test]
1514 fn test_evaluate_nary_skips_branch_with_empty_effective_mask() {
1515 let mut ctx = SESSION.create_execution_ctx();
1516 let test_array = StructArray::from_fields(&[("value", buffer![1i32, 2, 3].into_array())])
1519 .unwrap()
1520 .into_array();
1521
1522 let expr = nested_case_when(
1523 vec![
1524 (eq(get_item("value", root()), lit(1i32)), lit(10i32)),
1525 (eq(get_item("value", root()), lit(1i32)), lit(999i32)),
1528 (eq(get_item("value", root()), lit(2i32)), lit(20i32)),
1529 ],
1530 Some(lit(0i32)),
1531 );
1532
1533 let result = apply_expr(&expr, &test_array);
1534 assert_arrays_eq!(result, buffer![10i32, 20, 0].into_array(), &mut ctx);
1535 }
1536
1537 #[test]
1538 fn test_evaluate_nary_string_output() -> VortexResult<()> {
1539 let test_array =
1541 StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4].into_array())])?
1542 .into_array();
1543
1544 let expr = nested_case_when(
1548 vec![
1549 (gt(get_item("value", root()), lit(2i32)), lit("high")),
1550 (gt(get_item("value", root()), lit(0i32)), lit("low")),
1551 ],
1552 Some(lit("none")),
1553 );
1554
1555 let result = apply_expr(&expr, &test_array);
1556 assert_arrays_eq!(
1557 result,
1558 VarBinViewArray::from_iter_str(["low", "low", "high", "high"]),
1559 &mut SESSION.create_execution_ctx()
1560 );
1561 Ok(())
1562 }
1563
1564 #[test]
1565 fn test_evaluate_nary_with_nullable_conditions() {
1566 let mut ctx = SESSION.create_execution_ctx();
1567 let test_array = StructArray::from_fields(&[
1568 (
1569 "cond1",
1570 BoolArray::from_iter([Some(true), None, Some(false)]).into_array(),
1571 ),
1572 (
1573 "cond2",
1574 BoolArray::from_iter([Some(false), Some(true), None]).into_array(),
1575 ),
1576 ])
1577 .unwrap()
1578 .into_array();
1579
1580 let expr = nested_case_when(
1581 vec![
1582 (get_item("cond1", root()), lit(10i32)),
1583 (get_item("cond2", root()), lit(20i32)),
1584 ],
1585 Some(lit(0i32)),
1586 );
1587
1588 let result = apply_expr(&expr, &test_array);
1589 assert_arrays_eq!(result, buffer![10i32, 20, 0].into_array(), &mut ctx);
1593 }
1594
1595 fn nullable_i64_scope(fields: &[&str]) -> DType {
1599 DType::Struct(
1600 StructFields::new(
1601 fields.to_vec().into(),
1602 vec![DType::Primitive(PType::I64, Nullability::Nullable); fields.len()],
1603 ),
1604 Nullability::NonNullable,
1605 )
1606 }
1607
1608 #[test]
1609 fn test_simplify_coalesce_is_null_rewrites_to_fill_null() -> VortexResult<()> {
1610 let expr = case_when(is_null(col("x")), lit(0i64), col("x"));
1612 let optimized = expr
1613 .bind(&nullable_i64_scope(&["x"]))?
1614 .optimize_recursive()?;
1615 assert!(
1616 optimized.to_string().starts_with("vortex.fill_null"),
1617 "expected fill_null, got {optimized}"
1618 );
1619 Ok(())
1620 }
1621
1622 #[test]
1623 fn test_simplify_coalesce_is_not_null_rewrites_to_fill_null() -> VortexResult<()> {
1624 let expr = case_when(is_not_null(col("x")), col("x"), lit(0i64));
1626 let optimized = expr
1627 .bind(&nullable_i64_scope(&["x"]))?
1628 .optimize_recursive()?;
1629 assert!(
1630 optimized.to_string().starts_with("vortex.fill_null"),
1631 "expected fill_null, got {optimized}"
1632 );
1633 Ok(())
1634 }
1635
1636 #[test]
1637 fn test_simplify_does_not_fire_when_operands_differ() -> VortexResult<()> {
1638 let expr = case_when(is_null(col("x")), lit(0i64), col("y"));
1640 let optimized = expr
1641 .bind(&nullable_i64_scope(&["x", "y"]))?
1642 .optimize_recursive()?;
1643 let s = optimized.to_string();
1644 assert!(s.contains("CASE"), "expected CASE WHEN to remain, got {s}");
1645 assert!(!s.contains("fill_null"), "must not rewrite, got {s}");
1646 Ok(())
1647 }
1648
1649 #[test]
1650 fn test_simplify_does_not_fire_for_non_constant_fill() -> VortexResult<()> {
1651 let expr = case_when(is_null(col("x")), col("c"), col("x"));
1654 let optimized = expr
1655 .bind(&nullable_i64_scope(&["x", "c"]))?
1656 .optimize_recursive()?;
1657 let s = optimized.to_string();
1658 assert!(s.contains("CASE"), "expected CASE WHEN to remain, got {s}");
1659 assert!(!s.contains("fill_null"), "must not rewrite, got {s}");
1660 Ok(())
1661 }
1662
1663 #[test]
1664 fn test_simplify_null_fill_collapses_to_input() -> VortexResult<()> {
1665 let null_fill = || {
1669 lit(Scalar::null(DType::Primitive(
1670 PType::I64,
1671 Nullability::Nullable,
1672 )))
1673 };
1674
1675 for expr in [
1676 case_when(is_null(col("x")), null_fill(), col("x")),
1677 case_when(is_not_null(col("x")), col("x"), null_fill()),
1678 ] {
1679 let optimized = expr
1680 .bind(&nullable_i64_scope(&["x"]))?
1681 .optimize_recursive()?;
1682 assert_eq!(
1683 optimized.to_string(),
1684 "$.x",
1685 "expected collapse to input column, got {optimized}"
1686 );
1687 }
1688 Ok(())
1689 }
1690
1691 #[test]
1692 fn test_simplify_null_fill_semantic_equivalence() -> VortexResult<()> {
1693 let mut ctx = SESSION.create_execution_ctx();
1694 let array = PrimitiveArray::from_option_iter([Some(1i64), None, Some(3)]).into_array();
1696 let scope = DType::Primitive(PType::I64, Nullability::Nullable);
1697 let null_fill = lit(Scalar::null(DType::Primitive(
1698 PType::I64,
1699 Nullability::Nullable,
1700 )));
1701
1702 let original = case_when(is_null(root()), null_fill, root());
1703 let optimized = original.bind(&scope)?.optimize_recursive()?;
1704 assert_eq!(
1705 optimized.to_string(),
1706 "$",
1707 "expected collapse to root, got {optimized}"
1708 );
1709
1710 let expected = PrimitiveArray::from_option_iter([Some(1i64), None, Some(3)]).into_array();
1711 assert_arrays_eq!(apply_expr(&original, &array), expected, &mut ctx);
1712 assert_arrays_eq!(apply_bound_expr(&optimized, &array), expected, &mut ctx);
1713 Ok(())
1714 }
1715
1716 #[test]
1717 fn test_simplify_does_not_fire_without_else() -> VortexResult<()> {
1718 let expr = case_when_no_else(is_null(col("x")), lit(0i64));
1719 let optimized = expr
1720 .bind(&nullable_i64_scope(&["x"]))?
1721 .optimize_recursive()?;
1722 assert!(
1723 !optimized.to_string().contains("fill_null"),
1724 "must not rewrite a no-ELSE case_when, got {optimized}"
1725 );
1726 Ok(())
1727 }
1728
1729 #[test]
1730 fn test_simplify_does_not_fire_for_multi_pair() -> VortexResult<()> {
1731 let expr = nested_case_when(
1732 vec![
1733 (is_null(col("x")), lit(0i64)),
1734 (gt(col("x"), lit(5i64)), lit(1i64)),
1735 ],
1736 Some(col("x")),
1737 );
1738 let optimized = expr
1739 .bind(&nullable_i64_scope(&["x"]))?
1740 .optimize_recursive()?;
1741 assert!(
1742 !optimized.to_string().contains("fill_null"),
1743 "must not rewrite a multi-pair case_when, got {optimized}"
1744 );
1745 Ok(())
1746 }
1747
1748 #[test]
1749 fn test_simplify_semantic_equivalence() -> VortexResult<()> {
1750 let mut ctx = SESSION.create_execution_ctx();
1751 let array = PrimitiveArray::from_option_iter([Some(1i64), None, Some(3)]).into_array();
1753 let scope = DType::Primitive(PType::I64, Nullability::Nullable);
1754
1755 let original = case_when(is_null(root()), lit(0i64), root());
1756 let optimized = original.bind(&scope)?.optimize_recursive()?;
1757 assert!(
1758 optimized.to_string().starts_with("vortex.fill_null"),
1759 "expected fill_null, got {optimized}"
1760 );
1761
1762 assert_arrays_eq!(
1765 apply_expr(&original, &array),
1766 PrimitiveArray::from_option_iter([Some(1i64), Some(0), Some(3)]).into_array(),
1767 &mut ctx
1768 );
1769 assert_arrays_eq!(
1770 apply_bound_expr(&optimized, &array),
1771 buffer![1i64, 0, 3].into_array(),
1772 &mut ctx
1773 );
1774 Ok(())
1775 }
1776
1777 #[test]
1778 fn test_simplify_utf8_coalesce_remains_executable() -> VortexResult<()> {
1779 let mut ctx = SESSION.create_execution_ctx();
1780 let input = VarBinViewArray::from_iter_nullable_str([Some("a"), None]).into_array();
1781
1782 for expr in [
1783 case_when(is_null(root()), lit("fallback"), root()),
1784 case_when(is_not_null(root()), root(), lit("fallback")),
1785 ] {
1786 let optimized = expr.bind(input.dtype())?.optimize_recursive()?;
1787 assert!(optimized.is::<FillNull>(), "{optimized}");
1788 let result = input.clone().apply_bound(&optimized)?;
1789 assert_arrays_eq!(
1790 result,
1791 VarBinViewArray::from_iter_str(["a", "fallback"]),
1792 &mut ctx
1793 );
1794 }
1795 Ok(())
1796 }
1797
1798 #[test]
1799 fn test_simplify_list_coalesce_keeps_case_when() -> VortexResult<()> {
1800 let mut ctx = SESSION.create_execution_ctx();
1802 let element_dtype = Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable));
1803 let input_dtype = DType::List(Arc::clone(&element_dtype), Nullability::Nullable);
1804 let mut builder = builder_with_capacity_in(&input_dtype, 2, ctx.allocator());
1805 builder.append_scalar(&Scalar::list(
1806 Arc::clone(&element_dtype),
1807 vec![Scalar::from(1i32)],
1808 Nullability::Nullable,
1809 ))?;
1810 builder.append_scalar(&Scalar::null(input_dtype))?;
1811 let input = builder.finish();
1812 let fill = Scalar::list(
1813 element_dtype,
1814 vec![Scalar::from(2i32)],
1815 Nullability::NonNullable,
1816 );
1817
1818 let optimized = case_when(is_null(root()), lit(fill.clone()), root())
1819 .bind(input.dtype())?
1820 .optimize_recursive()?;
1821 assert!(optimized.is::<CaseWhen>(), "{optimized}");
1822 let result = input
1823 .clone()
1824 .apply_bound(&optimized)?
1825 .execute::<Canonical>(&mut ctx)?
1826 .into_array();
1827 assert_eq!(
1828 result.execute_scalar(0, &mut ctx)?,
1829 input.execute_scalar(0, &mut ctx)?
1830 );
1831 assert_eq!(
1832 result.execute_scalar(1, &mut ctx)?,
1833 fill.cast(result.dtype())?
1834 );
1835 Ok(())
1836 }
1837
1838 #[test]
1839 fn test_merge_case_branches_alternating_mask() -> VortexResult<()> {
1840 let mut ctx = SESSION.create_execution_ctx();
1841 let n = 100usize;
1843
1844 let branch0_mask = Mask::from_indices(n, (0..n).step_by(2));
1846 let branch1_mask = Mask::from_indices(n, (1..n).step_by(2));
1847
1848 let else_value = PrimitiveArray::from_option_iter(vec![Some(99i32); n]).into_array();
1849 let output_dtype = else_value.dtype().clone();
1850 let result = merge_case_branches(
1851 &[
1852 (
1853 branch0_mask,
1854 PrimitiveArray::from_option_iter(vec![Some(0i32); n]).into_array(),
1855 ),
1856 (
1857 branch1_mask,
1858 PrimitiveArray::from_option_iter(vec![Some(1i32); n]).into_array(),
1859 ),
1860 ],
1861 &Mask::new_false(n),
1862 &else_value,
1863 &output_dtype,
1864 &mut SESSION.create_execution_ctx(),
1865 )?;
1866
1867 let expected: Vec<Option<i32>> = (0..n)
1869 .map(|v| if v % 2 == 0 { Some(0) } else { Some(1) })
1870 .collect();
1871 assert_arrays_eq!(
1872 result,
1873 PrimitiveArray::from_option_iter(expected).into_array(),
1874 &mut ctx
1875 );
1876 Ok(())
1877 }
1878}