Skip to main content

vortex_array/scalar_fn/fns/
case_when.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! SQL-style CASE WHEN: evaluates `(condition, value)` pairs in order and returns
5//! the value from the first matching condition (first-match-wins). NULL conditions
6//! are treated as false. If no ELSE clause is provided, unmatched rows produce NULL;
7//! otherwise they get the ELSE value.
8//!
9//! Unlike SQL which coerces all branches to a common supertype, all THEN/ELSE
10//! branches must share the same base dtype (ignoring nullability). The result
11//! nullability is the union of all branches (forced nullable if no ELSE).
12//!
13//! The serialized expression format is not part of a Vortex edition and is not
14//! guaranteed to remain compatible across Vortex versions.
15
16use 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/// Options for the n-ary CaseWhen expression.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub struct CaseWhenOptions {
60    /// Number of WHEN/THEN pairs.
61    pub num_when_then_pairs: u32,
62    /// Whether an ELSE clause is present.
63    /// If false, unmatched rows return NULL.
64    pub has_else: bool,
65}
66
67impl CaseWhenOptions {
68    /// Total number of child expressions: 2 per WHEN/THEN pair, plus 1 if ELSE is present.
69    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/// An n-ary CASE WHEN expression.
85///
86/// Children are in order: `[when_0, then_0, when_1, then_1, ..., else?]`.
87#[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        // Unlike SQL which coerces all branches to a common supertype, we require
187        // all THEN/ELSE branches to have the same base dtype (ignoring nullability).
188        // The result nullability is the union of all branches, at every nesting level.
189        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            // No ELSE means unmatched rows are NULL
216            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        // `remaining` now selects the rows that no branch matched.
269        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        // Rewrite the COALESCE-shaped CASE WHEN into `fill_null`, which references `x`
278        // once and lowers to a single fill kernel instead of a `zip`/merge that resolves
279        // `x` twice (once for the `is_null` predicate, once for the value branch).
280        //
281        //   CASE WHEN is_null(x)     THEN c ELSE x END  ==>  fill_null(x, c)
282        //   CASE WHEN is_not_null(x) THEN x ELSE c END  ==>  fill_null(x, c)
283        //
284        // The fill `c` must be a `Literal`: `fill_null`'s kernel reads the fill value via
285        // `as_constant()`, so a non-constant fill would produce an unexecutable expression.
286        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        // `is_null(x) ? c : x` — predicate operand and ELSE are the same `x`, fill is THEN.
295        let (x, fill) = if when.is::<IsNull>() && when.child(0) == els {
296            (els, then)
297        // `is_not_null(x) ? x : c` — predicate operand and THEN are the same `x`, fill is ELSE.
298        } 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            // Filling the nulls of `x` with NULL is a no-op
310            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        // A null in an unselected branch does not force a null output.
325        false
326    }
327
328    fn is_infallible(&self, _options: &Self::Options) -> bool {
329        true
330    }
331}
332
333/// Assemble coarse runs directly; compact fragmented branches before restoring row positions.
334///
335/// `else_mask` selects the rows that no branch matched. Every branch and the ELSE value
336/// are cast to `output_dtype`, the dtype that `return_dtype` reports.
337fn 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    // A span may add an ELSE run. Coarse runs amortize slicing without the
357    // extra compact-output buffer and gather; isolated branch selections do too.
358    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    // Cast once up front: every span below is a slice of these arrays.
430    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        // Each case lists the branch selections in branch order. Rows that no branch
554        // selects come from ELSE.
555        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    /// THEN and ELSE may differ in nested nullability. The output must keep nested nulls from
608    /// either side, on the run merge, the compact merge and when no branch matches.
609    #[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    // ==================== Serialization Tests ====================
792
793    #[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    // ==================== Display Tests ====================
867
868    #[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        // CASE WHEN x > 10 THEN 'high' WHEN x > 5 THEN 'medium' ELSE 'low' END
893        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    // ==================== DType Tests ====================
907
908    #[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    // ==================== Arity Tests ====================
975
976    #[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    // ==================== Child Name Tests ====================
995
996    #[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    // ==================== N-ary Arity Tests ====================
1008
1009    #[test]
1010    fn test_arity_nary_with_else() {
1011        let options = CaseWhenOptions {
1012            num_when_then_pairs: 3,
1013            has_else: true,
1014        };
1015        // 3 pairs * 2 children + 1 else = 7
1016        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        // 3 pairs * 2 children = 6
1026        assert_eq!(CaseWhen.arity(&options), Arity::Exact(6));
1027    }
1028
1029    // ==================== N-ary Child Name Tests ====================
1030
1031    #[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    // ==================== N-ary DType Tests ====================
1047
1048    #[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        // When some THEN branches are nullable and others are not,
1062        // the result should be nullable (union of nullabilities).
1063        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    // ==================== Expression Manipulation Tests ====================
1089
1090    #[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    // ==================== Evaluate Tests ====================
1100
1101    #[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        // Test n-ary via nested_case_when
1127        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        // Both conditions match for values > 3, but first one wins
1153        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        // CASE WHEN value > 0 THEN 100 END — condition is always true, no ELSE.
1233        // Result must be Nullable because the implicit ELSE is NULL.
1234        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        // When a later THEN branch is Nullable and branches[0] and ELSE are NonNullable,
1257        // the result dtype must still be Nullable.
1258        //
1259        // CASE WHEN value = 0 THEN 10          -- NonNullable
1260        //      WHEN value = 1 THEN nullable(20) -- Nullable
1261        //      ELSE 0                           -- NonNullable
1262        // → result must be Nullable(i32)
1263        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    // ==================== N-ary Evaluate Tests ====================
1385
1386    #[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        // Two conditions, no ELSE — unmatched rows should be NULL
1395        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        // 5 WHEN/THEN pairs: each value maps to its value * 10
1422        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        // All conditions are false, no ELSE — everything should be NULL
1449        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        // value=10: matches cond1 (>5) and cond2 (>0), first should win
1475        // value=20: matches all three, first should win
1476        // value=30: matches all three, first should win
1477        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        // First matching condition always wins
1488        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        // After branch 0 claims all rows, remaining becomes all_false.
1495        // The loop breaks before evaluating branch 1's condition.
1496        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                // Never evaluated due to early exit; 999 must never appear in output.
1504                (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        // Branch 0 claims value=1. Branch 1 targets the same rows but they are already
1517        // matched → effective_mask is all_false → branch 1 is skipped (THEN not used).
1518        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                // Same condition as branch 0 — all matching rows already claimed → skipped.
1526                // 999 must never appear in output.
1527                (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        // Exercises merge_case_branches with a non-primitive (Utf8) builder.
1540        let test_array =
1541            StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4].into_array())])?
1542                .into_array();
1543
1544        // CASE WHEN value > 2 THEN 'high' WHEN value > 0 THEN 'low' ELSE 'none' END
1545        // value=1,2 → 'low' (branch 1 after branch 0 claims 3,4)
1546        // value=3,4 → 'high' (branch 0)
1547        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        // row 0: cond1=true → 10
1590        // row 1: cond1=NULL(→false), cond2=true → 20
1591        // row 2: cond1=false, cond2=NULL(→false) → else=0
1592        assert_arrays_eq!(result, buffer![10i32, 20, 0].into_array(), &mut ctx);
1593    }
1594
1595    // ==================== Simplify: COALESCE -> fill_null ====================
1596
1597    /// Builds a non-nullable struct scope whose named fields are all `Nullable(I64)`.
1598    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        // CASE WHEN is_null(x) THEN 0 ELSE x END  ==>  fill_null(x, 0)
1611        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        // CASE WHEN is_not_null(x) THEN x ELSE 0 END  ==>  fill_null(x, 0)
1625        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        // The is_null operand (x) and the ELSE (y) are different columns: not a COALESCE.
1639        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        // COALESCE(x, c) with a *column* fill: fill_null cannot consume a non-constant
1652        // fill value, so the rewrite must not fire.
1653        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        // Filling the nulls of x with NULL is a no-op, so both forms collapse to just `x`.
1666        //   CASE WHEN is_null(x)     THEN null ELSE x    END  ==>  x
1667        //   CASE WHEN is_not_null(x) THEN x    ELSE null END  ==>  x
1668        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        // The collapse-to-input rewrite must preserve values (and `x`'s nullability).
1695        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        // The optimized expression must produce the same values as the original CASE WHEN.
1752        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        // Original keeps CASE WHEN's nullable result dtype; the rewrite tightens it to
1763        // NonNullable because a non-null fill cannot leave any nulls behind. Values match.
1764        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        // Lists have no fill_null kernel, so the COALESCE shape must stay a CASE WHEN.
1801        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        // Alternating rows exercise compact branch assembly.
1842        let n = 100usize;
1843
1844        // Branch 0: even rows → 0, Branch 1: odd rows → 1, Else: never reached.
1845        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        // Even rows → 0, odd rows → 1.
1868        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}