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
13use std::fmt;
14use std::fmt::Formatter;
15use std::hash::Hash;
16use std::sync::Arc;
17
18use prost::Message;
19use vortex_error::VortexResult;
20use vortex_error::vortex_bail;
21use vortex_mask::AllOr;
22use vortex_mask::Mask;
23use vortex_session::VortexSession;
24use vortex_session::registry::CachedId;
25
26use crate::ArrayRef;
27use crate::ExecutionCtx;
28use crate::IntoArray;
29use crate::arrays::BoolArray;
30use crate::arrays::ConstantArray;
31use crate::arrays::bool::BoolArrayExt;
32use crate::builders::ArrayBuilder;
33use crate::builders::builder_with_capacity_in;
34use crate::builtins::ArrayBuiltins;
35use crate::dtype::DType;
36use crate::expr::BoundExpression;
37use crate::expr::display::ExprDisplay;
38use crate::proto::expr as pb;
39use crate::scalar::Scalar;
40use crate::scalar_fn::Arity;
41use crate::scalar_fn::ChildName;
42use crate::scalar_fn::EmptyOptions;
43use crate::scalar_fn::ExecutionArgs;
44use crate::scalar_fn::ScalarFnId;
45use crate::scalar_fn::ScalarFnVTable;
46use crate::scalar_fn::ScalarFnVTableExt;
47use crate::scalar_fn::fns::fill_null::FillNull;
48use crate::scalar_fn::fns::is_not_null::IsNotNull;
49use crate::scalar_fn::fns::is_null::IsNull;
50use crate::scalar_fn::fns::literal::Literal;
51use crate::scalar_fn::fns::zip::zip_impl;
52
53/// Options for the n-ary CaseWhen expression.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub struct CaseWhenOptions {
56    /// Number of WHEN/THEN pairs.
57    pub num_when_then_pairs: u32,
58    /// Whether an ELSE clause is present.
59    /// If false, unmatched rows return NULL.
60    pub has_else: bool,
61}
62
63impl CaseWhenOptions {
64    /// Total number of child expressions: 2 per WHEN/THEN pair, plus 1 if ELSE is present.
65    pub fn num_children(&self) -> usize {
66        self.num_when_then_pairs as usize * 2 + usize::from(self.has_else)
67    }
68}
69
70impl fmt::Display for CaseWhenOptions {
71    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
72        write!(
73            f,
74            "case_when(pairs={}, else={})",
75            self.num_when_then_pairs, self.has_else
76        )
77    }
78}
79
80/// An n-ary CASE WHEN expression.
81///
82/// Children are in order: `[when_0, then_0, when_1, then_1, ..., else?]`.
83#[derive(Clone)]
84pub struct CaseWhen;
85
86impl ScalarFnVTable for CaseWhen {
87    type Options = CaseWhenOptions;
88
89    fn id(&self) -> ScalarFnId {
90        static ID: CachedId = CachedId::new("vortex.case_when");
91        *ID
92    }
93
94    fn serialize(&self, _options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
95        // let num_children = options.num_when_then_pairs * 2 + u32::from(options.has_else);
96        // Ok(Some(pb::CaseWhenOpts { num_children }.encode_to_vec()))
97        // stabilize the expr
98        vortex_bail!("cannot serialize")
99    }
100
101    fn deserialize(
102        &self,
103        metadata: &[u8],
104        _session: &VortexSession,
105    ) -> VortexResult<Self::Options> {
106        let opts = pb::CaseWhenOpts::decode(metadata)?;
107        if opts.num_children < 2 {
108            vortex_bail!(
109                "CaseWhen expects at least 2 children, got {}",
110                opts.num_children
111            );
112        }
113        Ok(CaseWhenOptions {
114            num_when_then_pairs: opts.num_children / 2,
115            has_else: opts.num_children % 2 == 1,
116        })
117    }
118
119    fn arity(&self, options: &Self::Options) -> Arity {
120        Arity::Exact(options.num_children())
121    }
122
123    fn child_name(&self, options: &Self::Options, child_idx: usize) -> ChildName {
124        let num_pair_children = options.num_when_then_pairs as usize * 2;
125        if child_idx < num_pair_children {
126            let pair_idx = child_idx / 2;
127            if child_idx.is_multiple_of(2) {
128                ChildName::from(Arc::from(format!("when_{pair_idx}")))
129            } else {
130                ChildName::from(Arc::from(format!("then_{pair_idx}")))
131            }
132        } else if options.has_else && child_idx == num_pair_children {
133            ChildName::from("else")
134        } else {
135            unreachable!("Invalid child index {} for CaseWhen", child_idx)
136        }
137    }
138
139    fn fmt_sql(
140        &self,
141        options: &Self::Options,
142        expr: &dyn ExprDisplay,
143        f: &mut Formatter<'_>,
144    ) -> fmt::Result {
145        write!(f, "CASE")?;
146        for i in 0..options.num_when_then_pairs as usize {
147            write!(
148                f,
149                " WHEN {} THEN {}",
150                expr.display_child(i * 2),
151                expr.display_child(i * 2 + 1)
152            )?;
153        }
154        if options.has_else {
155            let else_idx = options.num_when_then_pairs as usize * 2;
156            write!(f, " ELSE {}", expr.display_child(else_idx))?;
157        }
158        write!(f, " END")
159    }
160
161    fn return_dtype(&self, options: &Self::Options, arg_dtypes: &[DType]) -> VortexResult<DType> {
162        if options.num_when_then_pairs == 0 {
163            vortex_bail!("CaseWhen must have at least one WHEN/THEN pair");
164        }
165
166        let expected_len = options.num_children();
167        if arg_dtypes.len() != expected_len {
168            vortex_bail!(
169                "CaseWhen expects {expected_len} argument dtypes, got {}",
170                arg_dtypes.len()
171            );
172        }
173
174        // Unlike SQL which coerces all branches to a common supertype, we require
175        // all THEN/ELSE branches to have the same base dtype (ignoring nullability).
176        // The result nullability is the union of all branches.
177        let first_then = &arg_dtypes[1];
178        let mut result_dtype = first_then.clone();
179
180        for i in 1..options.num_when_then_pairs as usize {
181            let then_i = &arg_dtypes[i * 2 + 1];
182            if !first_then.eq_ignore_nullability(then_i) {
183                vortex_bail!(
184                    "CaseWhen THEN dtypes must match (ignoring nullability), got {} and {}",
185                    first_then,
186                    then_i
187                );
188            }
189            result_dtype = result_dtype.union_nullability(then_i.nullability());
190        }
191
192        if options.has_else {
193            let else_dtype = &arg_dtypes[options.num_when_then_pairs as usize * 2];
194            if !result_dtype.eq_ignore_nullability(else_dtype) {
195                vortex_bail!(
196                    "CaseWhen THEN and ELSE dtypes must match (ignoring nullability), got {} and {}",
197                    first_then,
198                    else_dtype
199                );
200            }
201            result_dtype = result_dtype.union_nullability(else_dtype.nullability());
202        } else {
203            // No ELSE means unmatched rows are NULL
204            result_dtype = result_dtype.as_nullable();
205        }
206
207        Ok(result_dtype)
208    }
209
210    fn execute(
211        &self,
212        options: &Self::Options,
213        args: &dyn ExecutionArgs,
214        ctx: &mut ExecutionCtx,
215    ) -> VortexResult<ArrayRef> {
216        // Inspired by https://datafusion.apache.org/blog/2026/02/02/datafusion_case/
217        //
218        // TODO: shrink input to `remaining` rows between WHEN iterations (batch reduction).
219        // TODO: project to only referenced columns before batch reduction (column projection).
220        // TODO: evaluate THEN/ELSE on compact matching/non-matching rows and scatter-merge the results.
221        // TODO: for constant WHEN/THEN values, compile to a hash table for a single-pass lookup.
222        let row_count = args.row_count();
223        let num_pairs = options.num_when_then_pairs as usize;
224
225        let mut remaining = Mask::new_true(row_count);
226        let mut branches: Vec<(Mask, ArrayRef)> = Vec::with_capacity(num_pairs);
227
228        for i in 0..num_pairs {
229            if remaining.all_false() {
230                break;
231            }
232
233            let condition = args.get(i * 2)?;
234            let cond_bool = condition.execute::<BoolArray>(ctx)?;
235            let cond_mask = cond_bool.to_mask_fill_null_false(ctx);
236            let effective_mask = &remaining & &cond_mask;
237
238            if effective_mask.all_false() {
239                continue;
240            }
241
242            let then_value = args.get(i * 2 + 1)?;
243            remaining = remaining.bitand_not(&cond_mask);
244            branches.push((effective_mask, then_value));
245        }
246
247        let else_value: ArrayRef = if options.has_else {
248            args.get(num_pairs * 2)?
249        } else {
250            let then_dtype = args.get(1)?.dtype().as_nullable();
251            ConstantArray::new(Scalar::null(then_dtype), row_count).into_array()
252        };
253
254        if branches.is_empty() {
255            return Ok(else_value);
256        }
257
258        merge_case_branches(branches, else_value, ctx)
259    }
260
261    fn simplify(
262        &self,
263        options: &Self::Options,
264        expr: &BoundExpression,
265    ) -> VortexResult<Option<BoundExpression>> {
266        // Rewrite the COALESCE-shaped CASE WHEN into `fill_null`, which references `x`
267        // once and lowers to a single fill kernel instead of a `zip`/merge that resolves
268        // `x` twice (once for the `is_null` predicate, once for the value branch).
269        //
270        //   CASE WHEN is_null(x)     THEN c ELSE x END  ==>  fill_null(x, c)
271        //   CASE WHEN is_not_null(x) THEN x ELSE c END  ==>  fill_null(x, c)
272        //
273        // The fill `c` must be a `Literal`: `fill_null`'s kernel reads the fill value via
274        // `as_constant()`, so a non-constant fill would produce an unexecutable expression.
275        if options.num_when_then_pairs != 1 || !options.has_else {
276            return Ok(None);
277        }
278
279        let when = expr.child(0);
280        let then = expr.child(1);
281        let els = expr.child(2);
282
283        // `is_null(x) ? c : x` — predicate operand and ELSE are the same `x`, fill is THEN.
284        let (x, fill) = if when.is::<IsNull>() && when.child(0) == els {
285            (els, then)
286        // `is_not_null(x) ? x : c` — predicate operand and THEN are the same `x`, fill is ELSE.
287        } else if when.is::<IsNotNull>() && when.child(0) == then {
288            (then, els)
289        } else {
290            return Ok(None);
291        };
292
293        let Some(scalar) = fill.as_opt::<Literal>() else {
294            return Ok(None);
295        };
296
297        if scalar.is_null() {
298            // Filling the nulls of `x` with NULL is a no-op
299            return Ok(Some(x.clone()));
300        }
301
302        Ok(Some(FillNull.try_new_bound_expr(
303            EmptyOptions,
304            [x.clone(), fill.clone()],
305        )?))
306    }
307
308    fn is_strict(&self, _options: &Self::Options) -> bool {
309        // A null in an unselected branch does not force a null output.
310        false
311    }
312
313    fn is_infallible(&self, _options: &Self::Options) -> bool {
314        true
315    }
316}
317
318/// Average run length at which slicing + context-aware builder appends become cheaper than `scalar_at`.
319/// Measured empirically via benchmarks.
320const SLICE_CROSSOVER_RUN_LEN: usize = 4;
321
322/// Merges disjoint `(mask, then_value)` branch pairs with an `else_value` into a single array.
323///
324/// Branch masks are guaranteed disjoint by the remaining-row tracking in [`CaseWhen::execute`].
325fn merge_case_branches(
326    branches: Vec<(Mask, ArrayRef)>,
327    else_value: ArrayRef,
328    ctx: &mut ExecutionCtx,
329) -> VortexResult<ArrayRef> {
330    if branches.len() == 1 {
331        let (mask, then_value) = &branches[0];
332        return zip_impl(then_value, &else_value, mask, ctx);
333    }
334
335    let output_nullability = branches
336        .iter()
337        .fold(else_value.dtype().nullability(), |acc, (_, arr)| {
338            acc | arr.dtype().nullability()
339        });
340    let output_dtype = else_value.dtype().with_nullability(output_nullability);
341    let branch_arrays: Vec<&ArrayRef> = branches.iter().map(|(_, arr)| arr).collect();
342
343    let mut spans: Vec<(usize, usize, usize)> = Vec::new();
344    for (branch_idx, (mask, _)) in branches.iter().enumerate() {
345        match mask.slices() {
346            AllOr::All => return branch_arrays[branch_idx].cast(output_dtype),
347            AllOr::None => {}
348            AllOr::Some(slices) => {
349                for &(start, end) in slices {
350                    spans.push((start, end, branch_idx));
351                }
352            }
353        }
354    }
355    spans.sort_unstable_by_key(|&(start, ..)| start);
356
357    if spans.is_empty() {
358        return else_value.cast(output_dtype);
359    }
360
361    let builder = builder_with_capacity_in(&output_dtype, else_value.len(), ctx.allocator());
362
363    let fragmented = spans.len() > else_value.len() / SLICE_CROSSOVER_RUN_LEN;
364    if fragmented {
365        merge_row_by_row(
366            &branch_arrays,
367            &else_value,
368            &spans,
369            &output_dtype,
370            builder,
371            ctx,
372        )
373    } else {
374        merge_run_by_run(
375            &branch_arrays,
376            &else_value,
377            &spans,
378            &output_dtype,
379            builder,
380            ctx,
381        )
382    }
383}
384
385/// Iterates spans directly, emitting one `scalar_at` per row.
386/// Zero per-run allocations; preferred for fragmented masks (avg run < [`SLICE_CROSSOVER_RUN_LEN`]).
387fn merge_row_by_row(
388    branch_arrays: &[&ArrayRef],
389    else_value: &ArrayRef,
390    spans: &[(usize, usize, usize)],
391    output_dtype: &DType,
392    mut builder: Box<dyn ArrayBuilder>,
393    ctx: &mut ExecutionCtx,
394) -> VortexResult<ArrayRef> {
395    let mut pos = 0;
396    for &(start, end, branch_idx) in spans {
397        for row in pos..start {
398            let scalar = else_value.execute_scalar(row, ctx)?;
399            builder.append_scalar(&scalar.cast(output_dtype)?)?;
400        }
401        for row in start..end {
402            let scalar = branch_arrays[branch_idx].execute_scalar(row, ctx)?;
403            builder.append_scalar(&scalar.cast(output_dtype)?)?;
404        }
405        pos = end;
406    }
407    for row in pos..else_value.len() {
408        let scalar = else_value.execute_scalar(row, ctx)?;
409        builder.append_scalar(&scalar.cast(output_dtype)?)?;
410    }
411
412    Ok(builder.finish())
413}
414
415/// Bulk-copies each span via `slice()` and context-aware builder appends.
416/// Preferred when runs are long enough that memcpy dominates over per-slice allocation cost.
417/// Lazy cast via `arr.cast(output_dtype)` is executed once per span as a block.
418fn merge_run_by_run(
419    branch_arrays: &[&ArrayRef],
420    else_value: &ArrayRef,
421    spans: &[(usize, usize, usize)],
422    output_dtype: &DType,
423    mut builder: Box<dyn ArrayBuilder>,
424    ctx: &mut ExecutionCtx,
425) -> VortexResult<ArrayRef> {
426    let else_value = else_value.cast(output_dtype.clone())?;
427    let len = else_value.len();
428    for (start, end, branch_idx) in spans {
429        if builder.len() < *start {
430            else_value
431                .slice(builder.len()..*start)?
432                .append_to_builder(builder.as_mut(), ctx)?;
433        }
434        branch_arrays[*branch_idx]
435            .cast(output_dtype.clone())?
436            .slice(*start..*end)?
437            .append_to_builder(builder.as_mut(), ctx)?;
438    }
439    if builder.len() < len {
440        else_value
441            .slice(builder.len()..len)?
442            .append_to_builder(builder.as_mut(), ctx)?;
443    }
444
445    Ok(builder.finish())
446}
447
448#[cfg(test)]
449mod tests {
450    use std::sync::LazyLock;
451
452    use vortex_buffer::buffer;
453    use vortex_error::VortexExpect as _;
454    use vortex_session::VortexSession;
455
456    use super::*;
457    use crate::Canonical;
458    use crate::IntoArray;
459    use crate::VortexSessionExecute;
460    use crate::arrays::BoolArray;
461    use crate::arrays::PrimitiveArray;
462    use crate::arrays::StructArray;
463    use crate::assert_arrays_eq;
464    use crate::dtype::DType;
465    use crate::dtype::Nullability;
466    use crate::dtype::PType;
467    use crate::dtype::StructFields;
468    use crate::expr::Expression;
469    use crate::expr::case_when;
470    use crate::expr::case_when_no_else;
471    use crate::expr::col;
472    use crate::expr::eq;
473    use crate::expr::get_item;
474    use crate::expr::gt;
475    use crate::expr::is_not_null;
476    use crate::expr::is_null;
477    use crate::expr::lit;
478    use crate::expr::nested_case_when;
479    use crate::expr::root;
480    use crate::expr::test_harness;
481    use crate::scalar::Scalar;
482
483    static SESSION: LazyLock<VortexSession> = LazyLock::new(crate::array_session);
484
485    /// Helper to evaluate an expression using the apply+execute pattern
486    fn evaluate_expr(expr: &Expression, array: &ArrayRef) -> ArrayRef {
487        let mut ctx = SESSION.create_execution_ctx();
488        array
489            .clone()
490            .apply(expr)
491            .unwrap()
492            .execute::<Canonical>(&mut ctx)
493            .unwrap()
494            .into_array()
495    }
496
497    fn evaluate_bound_expr(expr: &BoundExpression, array: &ArrayRef) -> ArrayRef {
498        let mut ctx = SESSION.create_execution_ctx();
499        array
500            .clone()
501            .apply_bound(expr)
502            .unwrap()
503            .execute::<Canonical>(&mut ctx)
504            .unwrap()
505            .into_array()
506    }
507
508    // ==================== Serialization Tests ====================
509
510    #[test]
511    #[should_panic(expected = "cannot serialize")]
512    fn test_serialization_roundtrip() {
513        let options = CaseWhenOptions {
514            num_when_then_pairs: 1,
515            has_else: true,
516        };
517        let serialized = CaseWhen.serialize(&options).unwrap().unwrap();
518        let deserialized = CaseWhen
519            .deserialize(&serialized, &VortexSession::empty())
520            .unwrap();
521        assert_eq!(options, deserialized);
522    }
523
524    #[test]
525    #[should_panic(expected = "cannot serialize")]
526    fn test_serialization_no_else() {
527        let options = CaseWhenOptions {
528            num_when_then_pairs: 1,
529            has_else: false,
530        };
531        let serialized = CaseWhen.serialize(&options).unwrap().unwrap();
532        let deserialized = CaseWhen
533            .deserialize(&serialized, &VortexSession::empty())
534            .unwrap();
535        assert_eq!(options, deserialized);
536    }
537
538    // ==================== Display Tests ====================
539
540    #[test]
541    fn test_display_with_else() {
542        let expr = case_when(gt(col("value"), lit(0i32)), lit(100i32), lit(0i32));
543        let display = format!("{}", expr);
544        assert!(display.contains("CASE"));
545        assert!(display.contains("WHEN"));
546        assert!(display.contains("THEN"));
547        assert!(display.contains("ELSE"));
548        assert!(display.contains("END"));
549    }
550
551    #[test]
552    fn test_display_no_else() {
553        let expr = case_when_no_else(gt(col("value"), lit(0i32)), lit(100i32));
554        let display = format!("{}", expr);
555        assert!(display.contains("CASE"));
556        assert!(display.contains("WHEN"));
557        assert!(display.contains("THEN"));
558        assert!(!display.contains("ELSE"));
559        assert!(display.contains("END"));
560    }
561
562    #[test]
563    fn test_display_nested_nary() {
564        // CASE WHEN x > 10 THEN 'high' WHEN x > 5 THEN 'medium' ELSE 'low' END
565        let expr = nested_case_when(
566            vec![
567                (gt(col("x"), lit(10i32)), lit("high")),
568                (gt(col("x"), lit(5i32)), lit("medium")),
569            ],
570            Some(lit("low")),
571        );
572        let display = format!("{}", expr);
573        assert_eq!(display.matches("CASE").count(), 1);
574        assert_eq!(display.matches("WHEN").count(), 2);
575        assert_eq!(display.matches("THEN").count(), 2);
576    }
577
578    // ==================== DType Tests ====================
579
580    #[test]
581    fn test_return_dtype_with_else() {
582        let expr = case_when(lit(true), lit(100i32), lit(0i32));
583        let input_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
584        let result_dtype = expr.return_dtype(&input_dtype).unwrap();
585        assert_eq!(
586            result_dtype,
587            DType::Primitive(PType::I32, Nullability::NonNullable)
588        );
589    }
590
591    #[test]
592    fn test_return_dtype_with_nullable_else() {
593        let expr = case_when(
594            lit(true),
595            lit(100i32),
596            lit(Scalar::null(DType::Primitive(
597                PType::I32,
598                Nullability::Nullable,
599            ))),
600        );
601        let input_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
602        let result_dtype = expr.return_dtype(&input_dtype).unwrap();
603        assert_eq!(
604            result_dtype,
605            DType::Primitive(PType::I32, Nullability::Nullable)
606        );
607    }
608
609    #[test]
610    fn test_return_dtype_without_else_is_nullable() {
611        let expr = case_when_no_else(lit(true), lit(100i32));
612        let input_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
613        let result_dtype = expr.return_dtype(&input_dtype).unwrap();
614        assert_eq!(
615            result_dtype,
616            DType::Primitive(PType::I32, Nullability::Nullable)
617        );
618    }
619
620    #[test]
621    fn test_return_dtype_with_struct_input() {
622        let dtype = test_harness::struct_dtype();
623        let expr = case_when(
624            gt(get_item("col1", root()), lit(10u16)),
625            lit(100i32),
626            lit(0i32),
627        );
628        let result_dtype = expr.return_dtype(&dtype).unwrap();
629        assert_eq!(
630            result_dtype,
631            DType::Primitive(PType::I32, Nullability::NonNullable)
632        );
633    }
634
635    #[test]
636    fn test_return_dtype_mismatched_then_else_errors() {
637        let expr = case_when(lit(true), lit(100i32), lit("zero"));
638        let input_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
639        let err = expr.return_dtype(&input_dtype).unwrap_err();
640        assert!(
641            err.to_string()
642                .contains("THEN and ELSE dtypes must match (ignoring nullability)")
643        );
644    }
645
646    // ==================== Arity Tests ====================
647
648    #[test]
649    fn test_arity_with_else() {
650        let options = CaseWhenOptions {
651            num_when_then_pairs: 1,
652            has_else: true,
653        };
654        assert_eq!(CaseWhen.arity(&options), Arity::Exact(3));
655    }
656
657    #[test]
658    fn test_arity_without_else() {
659        let options = CaseWhenOptions {
660            num_when_then_pairs: 1,
661            has_else: false,
662        };
663        assert_eq!(CaseWhen.arity(&options), Arity::Exact(2));
664    }
665
666    // ==================== Child Name Tests ====================
667
668    #[test]
669    fn test_child_names() {
670        let options = CaseWhenOptions {
671            num_when_then_pairs: 1,
672            has_else: true,
673        };
674        assert_eq!(CaseWhen.child_name(&options, 0).to_string(), "when_0");
675        assert_eq!(CaseWhen.child_name(&options, 1).to_string(), "then_0");
676        assert_eq!(CaseWhen.child_name(&options, 2).to_string(), "else");
677    }
678
679    // ==================== N-ary Serialization Tests ====================
680
681    #[test]
682    #[should_panic(expected = "cannot serialize")]
683    fn test_serialization_roundtrip_nary() {
684        let options = CaseWhenOptions {
685            num_when_then_pairs: 3,
686            has_else: true,
687        };
688        let serialized = CaseWhen.serialize(&options).unwrap().unwrap();
689        let deserialized = CaseWhen
690            .deserialize(&serialized, &VortexSession::empty())
691            .unwrap();
692        assert_eq!(options, deserialized);
693    }
694
695    #[test]
696    #[should_panic(expected = "cannot serialize")]
697    fn test_serialization_roundtrip_nary_no_else() {
698        let options = CaseWhenOptions {
699            num_when_then_pairs: 4,
700            has_else: false,
701        };
702        let serialized = CaseWhen.serialize(&options).unwrap().unwrap();
703        let deserialized = CaseWhen
704            .deserialize(&serialized, &VortexSession::empty())
705            .unwrap();
706        assert_eq!(options, deserialized);
707    }
708
709    // ==================== N-ary Arity Tests ====================
710
711    #[test]
712    fn test_arity_nary_with_else() {
713        let options = CaseWhenOptions {
714            num_when_then_pairs: 3,
715            has_else: true,
716        };
717        // 3 pairs * 2 children + 1 else = 7
718        assert_eq!(CaseWhen.arity(&options), Arity::Exact(7));
719    }
720
721    #[test]
722    fn test_arity_nary_without_else() {
723        let options = CaseWhenOptions {
724            num_when_then_pairs: 3,
725            has_else: false,
726        };
727        // 3 pairs * 2 children = 6
728        assert_eq!(CaseWhen.arity(&options), Arity::Exact(6));
729    }
730
731    // ==================== N-ary Child Name Tests ====================
732
733    #[test]
734    fn test_child_names_nary() {
735        let options = CaseWhenOptions {
736            num_when_then_pairs: 3,
737            has_else: true,
738        };
739        assert_eq!(CaseWhen.child_name(&options, 0).to_string(), "when_0");
740        assert_eq!(CaseWhen.child_name(&options, 1).to_string(), "then_0");
741        assert_eq!(CaseWhen.child_name(&options, 2).to_string(), "when_1");
742        assert_eq!(CaseWhen.child_name(&options, 3).to_string(), "then_1");
743        assert_eq!(CaseWhen.child_name(&options, 4).to_string(), "when_2");
744        assert_eq!(CaseWhen.child_name(&options, 5).to_string(), "then_2");
745        assert_eq!(CaseWhen.child_name(&options, 6).to_string(), "else");
746    }
747
748    // ==================== N-ary DType Tests ====================
749
750    #[test]
751    fn test_return_dtype_nary_mismatched_then_types_errors() {
752        let expr = nested_case_when(
753            vec![(lit(true), lit(100i32)), (lit(false), lit("oops"))],
754            Some(lit(0i32)),
755        );
756        let input_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
757        let err = expr.return_dtype(&input_dtype).unwrap_err();
758        assert!(err.to_string().contains("THEN dtypes must match"));
759    }
760
761    #[test]
762    fn test_return_dtype_nary_mixed_nullability() {
763        // When some THEN branches are nullable and others are not,
764        // the result should be nullable (union of nullabilities).
765        let non_null_then = lit(100i32);
766        let nullable_then = lit(Scalar::null(DType::Primitive(
767            PType::I32,
768            Nullability::Nullable,
769        )));
770        let expr = nested_case_when(
771            vec![(lit(true), non_null_then), (lit(false), nullable_then)],
772            Some(lit(0i32)),
773        );
774        let input_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
775        let result = expr.return_dtype(&input_dtype).unwrap();
776        assert_eq!(result, DType::Primitive(PType::I32, Nullability::Nullable));
777    }
778
779    #[test]
780    fn test_return_dtype_nary_no_else_is_nullable() {
781        let expr = nested_case_when(
782            vec![(lit(true), lit(10i32)), (lit(false), lit(20i32))],
783            None,
784        );
785        let input_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
786        let result = expr.return_dtype(&input_dtype).unwrap();
787        assert_eq!(result, DType::Primitive(PType::I32, Nullability::Nullable));
788    }
789
790    // ==================== Expression Manipulation Tests ====================
791
792    #[test]
793    fn test_replace_children() {
794        let expr = case_when(lit(true), lit(1i32), lit(0i32));
795        expr.with_children([lit(false), lit(2i32), lit(3i32)])
796            .vortex_expect("operation should succeed in test");
797    }
798
799    // ==================== Evaluate Tests ====================
800
801    #[test]
802    fn test_evaluate_simple_condition() {
803        let mut ctx = SESSION.create_execution_ctx();
804        let test_array =
805            StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())])
806                .unwrap()
807                .into_array();
808
809        let expr = case_when(
810            gt(get_item("value", root()), lit(2i32)),
811            lit(100i32),
812            lit(0i32),
813        );
814
815        let result = evaluate_expr(&expr, &test_array);
816        assert_arrays_eq!(
817            result,
818            buffer![0i32, 0, 100, 100, 100].into_array(),
819            &mut ctx
820        );
821    }
822
823    #[test]
824    fn test_evaluate_nary_multiple_conditions() {
825        let mut ctx = SESSION.create_execution_ctx();
826        // Test n-ary via nested_case_when
827        let test_array =
828            StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())])
829                .unwrap()
830                .into_array();
831
832        let expr = nested_case_when(
833            vec![
834                (eq(get_item("value", root()), lit(1i32)), lit(10i32)),
835                (eq(get_item("value", root()), lit(3i32)), lit(30i32)),
836            ],
837            Some(lit(0i32)),
838        );
839
840        let result = evaluate_expr(&expr, &test_array);
841        assert_arrays_eq!(result, buffer![10i32, 0, 30, 0, 0].into_array(), &mut ctx);
842    }
843
844    #[test]
845    fn test_evaluate_nary_first_match_wins() {
846        let mut ctx = SESSION.create_execution_ctx();
847        let test_array =
848            StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())])
849                .unwrap()
850                .into_array();
851
852        // Both conditions match for values > 3, but first one wins
853        let expr = nested_case_when(
854            vec![
855                (gt(get_item("value", root()), lit(2i32)), lit(100i32)),
856                (gt(get_item("value", root()), lit(3i32)), lit(200i32)),
857            ],
858            Some(lit(0i32)),
859        );
860
861        let result = evaluate_expr(&expr, &test_array);
862        assert_arrays_eq!(
863            result,
864            buffer![0i32, 0, 100, 100, 100].into_array(),
865            &mut ctx
866        );
867    }
868
869    #[test]
870    fn test_evaluate_no_else_returns_null() {
871        let mut ctx = SESSION.create_execution_ctx();
872        let test_array =
873            StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())])
874                .unwrap()
875                .into_array();
876
877        let expr = case_when_no_else(gt(get_item("value", root()), lit(3i32)), lit(100i32));
878
879        let result = evaluate_expr(&expr, &test_array);
880        assert!(result.dtype().is_nullable());
881        assert_arrays_eq!(
882            result,
883            PrimitiveArray::from_option_iter([None::<i32>, None, None, Some(100), Some(100)])
884                .into_array(),
885            &mut ctx
886        );
887    }
888
889    #[test]
890    fn test_evaluate_all_conditions_false() {
891        let mut ctx = SESSION.create_execution_ctx();
892        let test_array =
893            StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())])
894                .unwrap()
895                .into_array();
896
897        let expr = case_when(
898            gt(get_item("value", root()), lit(100i32)),
899            lit(1i32),
900            lit(0i32),
901        );
902
903        let result = evaluate_expr(&expr, &test_array);
904        assert_arrays_eq!(result, buffer![0i32, 0, 0, 0, 0].into_array(), &mut ctx);
905    }
906
907    #[test]
908    fn test_evaluate_all_conditions_true() {
909        let mut ctx = SESSION.create_execution_ctx();
910        let test_array =
911            StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())])
912                .unwrap()
913                .into_array();
914
915        let expr = case_when(
916            gt(get_item("value", root()), lit(0i32)),
917            lit(100i32),
918            lit(0i32),
919        );
920
921        let result = evaluate_expr(&expr, &test_array);
922        assert_arrays_eq!(
923            result,
924            buffer![100i32, 100, 100, 100, 100].into_array(),
925            &mut ctx
926        );
927    }
928
929    #[test]
930    fn test_evaluate_all_true_no_else_returns_correct_dtype() {
931        let mut ctx = SESSION.create_execution_ctx();
932        // CASE WHEN value > 0 THEN 100 END — condition is always true, no ELSE.
933        // Result must be Nullable because the implicit ELSE is NULL.
934        let test_array = StructArray::from_fields(&[("value", buffer![1i32, 2, 3].into_array())])
935            .unwrap()
936            .into_array();
937
938        let expr = case_when_no_else(gt(get_item("value", root()), lit(0i32)), lit(100i32));
939
940        let result = evaluate_expr(&expr, &test_array);
941        assert!(
942            result.dtype().is_nullable(),
943            "result dtype must be Nullable, got {:?}",
944            result.dtype()
945        );
946        assert_arrays_eq!(
947            result,
948            PrimitiveArray::from_option_iter([Some(100i32), Some(100), Some(100)]).into_array(),
949            &mut ctx
950        );
951    }
952
953    #[test]
954    fn test_merge_case_branches_widens_nullability_of_later_branch() -> VortexResult<()> {
955        let mut ctx = SESSION.create_execution_ctx();
956        // When a later THEN branch is Nullable and branches[0] and ELSE are NonNullable,
957        // the result dtype must still be Nullable.
958        //
959        // CASE WHEN value = 0 THEN 10          -- NonNullable
960        //      WHEN value = 1 THEN nullable(20) -- Nullable
961        //      ELSE 0                           -- NonNullable
962        // → result must be Nullable(i32)
963        let test_array =
964            StructArray::from_fields(&[("value", buffer![0i32, 1, 2].into_array())])?.into_array();
965
966        let nullable_20 =
967            Scalar::from(20i32).cast(&DType::Primitive(PType::I32, Nullability::Nullable))?;
968
969        let expr = nested_case_when(
970            vec![
971                (eq(get_item("value", root()), lit(0i32)), lit(10i32)),
972                (eq(get_item("value", root()), lit(1i32)), lit(nullable_20)),
973            ],
974            Some(lit(0i32)),
975        );
976
977        let result = evaluate_expr(&expr, &test_array);
978        assert!(
979            result.dtype().is_nullable(),
980            "result dtype must be Nullable, got {:?}",
981            result.dtype()
982        );
983        assert_arrays_eq!(
984            result,
985            PrimitiveArray::from_option_iter([Some(10), Some(20), Some(0)]).into_array(),
986            &mut ctx
987        );
988        Ok(())
989    }
990
991    #[test]
992    fn test_evaluate_with_literal_condition() {
993        let mut ctx = SESSION.create_execution_ctx();
994        let test_array = buffer![1i32, 2, 3].into_array();
995        let expr = case_when(lit(true), lit(100i32), lit(0i32));
996        let result = evaluate_expr(&expr, &test_array);
997
998        assert_arrays_eq!(result, buffer![100i32, 100, 100].into_array(), &mut ctx);
999    }
1000
1001    #[test]
1002    fn test_evaluate_with_bool_column_result() {
1003        let mut ctx = SESSION.create_execution_ctx();
1004        let test_array =
1005            StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())])
1006                .unwrap()
1007                .into_array();
1008
1009        let expr = case_when(
1010            gt(get_item("value", root()), lit(2i32)),
1011            lit(true),
1012            lit(false),
1013        );
1014
1015        let result = evaluate_expr(&expr, &test_array);
1016        assert_arrays_eq!(
1017            result,
1018            BoolArray::from_iter([false, false, true, true, true]).into_array(),
1019            &mut ctx
1020        );
1021    }
1022
1023    #[test]
1024    fn test_evaluate_with_nullable_condition() {
1025        let mut ctx = SESSION.create_execution_ctx();
1026        let test_array = StructArray::from_fields(&[(
1027            "cond",
1028            BoolArray::from_iter([Some(true), None, Some(false), None, Some(true)]).into_array(),
1029        )])
1030        .unwrap()
1031        .into_array();
1032
1033        let expr = case_when(get_item("cond", root()), lit(100i32), lit(0i32));
1034
1035        let result = evaluate_expr(&expr, &test_array);
1036        assert_arrays_eq!(result, buffer![100i32, 0, 0, 0, 100].into_array(), &mut ctx);
1037    }
1038
1039    #[test]
1040    fn test_evaluate_with_nullable_result_values() {
1041        let mut ctx = SESSION.create_execution_ctx();
1042        let test_array = StructArray::from_fields(&[
1043            ("value", buffer![1i32, 2, 3, 4, 5].into_array()),
1044            (
1045                "result",
1046                PrimitiveArray::from_option_iter([Some(10), None, Some(30), Some(40), Some(50)])
1047                    .into_array(),
1048            ),
1049        ])
1050        .unwrap()
1051        .into_array();
1052
1053        let expr = case_when(
1054            gt(get_item("value", root()), lit(2i32)),
1055            get_item("result", root()),
1056            lit(0i32),
1057        );
1058
1059        let result = evaluate_expr(&expr, &test_array);
1060        assert_arrays_eq!(
1061            result,
1062            PrimitiveArray::from_option_iter([Some(0i32), Some(0), Some(30), Some(40), Some(50)])
1063                .into_array(),
1064            &mut ctx
1065        );
1066    }
1067
1068    #[test]
1069    fn test_evaluate_with_all_null_condition() {
1070        let mut ctx = SESSION.create_execution_ctx();
1071        let test_array = StructArray::from_fields(&[(
1072            "cond",
1073            BoolArray::from_iter([None, None, None]).into_array(),
1074        )])
1075        .unwrap()
1076        .into_array();
1077
1078        let expr = case_when(get_item("cond", root()), lit(100i32), lit(0i32));
1079
1080        let result = evaluate_expr(&expr, &test_array);
1081        assert_arrays_eq!(result, buffer![0i32, 0, 0].into_array(), &mut ctx);
1082    }
1083
1084    // ==================== N-ary Evaluate Tests ====================
1085
1086    #[test]
1087    fn test_evaluate_nary_no_else_returns_null() {
1088        let mut ctx = SESSION.create_execution_ctx();
1089        let test_array =
1090            StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())])
1091                .unwrap()
1092                .into_array();
1093
1094        // Two conditions, no ELSE — unmatched rows should be NULL
1095        let expr = nested_case_when(
1096            vec![
1097                (eq(get_item("value", root()), lit(1i32)), lit(10i32)),
1098                (eq(get_item("value", root()), lit(3i32)), lit(30i32)),
1099            ],
1100            None,
1101        );
1102
1103        let result = evaluate_expr(&expr, &test_array);
1104        assert!(result.dtype().is_nullable());
1105        assert_arrays_eq!(
1106            result,
1107            PrimitiveArray::from_option_iter([Some(10i32), None, Some(30), None, None])
1108                .into_array(),
1109            &mut ctx
1110        );
1111    }
1112
1113    #[test]
1114    fn test_evaluate_nary_many_conditions() {
1115        let mut ctx = SESSION.create_execution_ctx();
1116        let test_array =
1117            StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4, 5].into_array())])
1118                .unwrap()
1119                .into_array();
1120
1121        // 5 WHEN/THEN pairs: each value maps to its value * 10
1122        let expr = nested_case_when(
1123            vec![
1124                (eq(get_item("value", root()), lit(1i32)), lit(10i32)),
1125                (eq(get_item("value", root()), lit(2i32)), lit(20i32)),
1126                (eq(get_item("value", root()), lit(3i32)), lit(30i32)),
1127                (eq(get_item("value", root()), lit(4i32)), lit(40i32)),
1128                (eq(get_item("value", root()), lit(5i32)), lit(50i32)),
1129            ],
1130            Some(lit(0i32)),
1131        );
1132
1133        let result = evaluate_expr(&expr, &test_array);
1134        assert_arrays_eq!(
1135            result,
1136            buffer![10i32, 20, 30, 40, 50].into_array(),
1137            &mut ctx
1138        );
1139    }
1140
1141    #[test]
1142    fn test_evaluate_nary_all_false_no_else() {
1143        let mut ctx = SESSION.create_execution_ctx();
1144        let test_array = StructArray::from_fields(&[("value", buffer![1i32, 2, 3].into_array())])
1145            .unwrap()
1146            .into_array();
1147
1148        // All conditions are false, no ELSE — everything should be NULL
1149        let expr = nested_case_when(
1150            vec![
1151                (gt(get_item("value", root()), lit(100i32)), lit(10i32)),
1152                (gt(get_item("value", root()), lit(200i32)), lit(20i32)),
1153            ],
1154            None,
1155        );
1156
1157        let result = evaluate_expr(&expr, &test_array);
1158        assert!(result.dtype().is_nullable());
1159        assert_arrays_eq!(
1160            result,
1161            PrimitiveArray::from_option_iter([None::<i32>, None, None]).into_array(),
1162            &mut ctx
1163        );
1164    }
1165
1166    #[test]
1167    fn test_evaluate_nary_overlapping_conditions_first_wins() {
1168        let mut ctx = SESSION.create_execution_ctx();
1169        let test_array =
1170            StructArray::from_fields(&[("value", buffer![10i32, 20, 30].into_array())])
1171                .unwrap()
1172                .into_array();
1173
1174        // value=10: matches cond1 (>5) and cond2 (>0), first should win
1175        // value=20: matches all three, first should win
1176        // value=30: matches all three, first should win
1177        let expr = nested_case_when(
1178            vec![
1179                (gt(get_item("value", root()), lit(5i32)), lit(1i32)),
1180                (gt(get_item("value", root()), lit(0i32)), lit(2i32)),
1181                (gt(get_item("value", root()), lit(15i32)), lit(3i32)),
1182            ],
1183            Some(lit(0i32)),
1184        );
1185
1186        let result = evaluate_expr(&expr, &test_array);
1187        // First matching condition always wins
1188        assert_arrays_eq!(result, buffer![1i32, 1, 1].into_array(), &mut ctx);
1189    }
1190
1191    #[test]
1192    fn test_evaluate_nary_early_exit_when_remaining_empty() {
1193        let mut ctx = SESSION.create_execution_ctx();
1194        // After branch 0 claims all rows, remaining becomes all_false.
1195        // The loop breaks before evaluating branch 1's condition.
1196        let test_array = StructArray::from_fields(&[("value", buffer![1i32, 2, 3].into_array())])
1197            .unwrap()
1198            .into_array();
1199
1200        let expr = nested_case_when(
1201            vec![
1202                (gt(get_item("value", root()), lit(0i32)), lit(100i32)),
1203                // Never evaluated due to early exit; 999 must never appear in output.
1204                (gt(get_item("value", root()), lit(0i32)), lit(999i32)),
1205            ],
1206            Some(lit(0i32)),
1207        );
1208
1209        let result = evaluate_expr(&expr, &test_array);
1210        assert_arrays_eq!(result, buffer![100i32, 100, 100].into_array(), &mut ctx);
1211    }
1212
1213    #[test]
1214    fn test_evaluate_nary_skips_branch_with_empty_effective_mask() {
1215        let mut ctx = SESSION.create_execution_ctx();
1216        // Branch 0 claims value=1. Branch 1 targets the same rows but they are already
1217        // matched → effective_mask is all_false → branch 1 is skipped (THEN not used).
1218        let test_array = StructArray::from_fields(&[("value", buffer![1i32, 2, 3].into_array())])
1219            .unwrap()
1220            .into_array();
1221
1222        let expr = nested_case_when(
1223            vec![
1224                (eq(get_item("value", root()), lit(1i32)), lit(10i32)),
1225                // Same condition as branch 0 — all matching rows already claimed → skipped.
1226                // 999 must never appear in output.
1227                (eq(get_item("value", root()), lit(1i32)), lit(999i32)),
1228                (eq(get_item("value", root()), lit(2i32)), lit(20i32)),
1229            ],
1230            Some(lit(0i32)),
1231        );
1232
1233        let result = evaluate_expr(&expr, &test_array);
1234        assert_arrays_eq!(result, buffer![10i32, 20, 0].into_array(), &mut ctx);
1235    }
1236
1237    #[test]
1238    fn test_evaluate_nary_string_output() -> VortexResult<()> {
1239        // Exercises merge_case_branches with a non-primitive (Utf8) builder.
1240        let test_array =
1241            StructArray::from_fields(&[("value", buffer![1i32, 2, 3, 4].into_array())])?
1242                .into_array();
1243
1244        // CASE WHEN value > 2 THEN 'high' WHEN value > 0 THEN 'low' ELSE 'none' END
1245        // value=1,2 → 'low' (branch 1 after branch 0 claims 3,4)
1246        // value=3,4 → 'high' (branch 0)
1247        let expr = nested_case_when(
1248            vec![
1249                (gt(get_item("value", root()), lit(2i32)), lit("high")),
1250                (gt(get_item("value", root()), lit(0i32)), lit("low")),
1251            ],
1252            Some(lit("none")),
1253        );
1254
1255        let result = evaluate_expr(&expr, &test_array);
1256        assert_eq!(
1257            result.execute_scalar(0, &mut SESSION.create_execution_ctx())?,
1258            Scalar::utf8("low", Nullability::NonNullable)
1259        );
1260        assert_eq!(
1261            result.execute_scalar(1, &mut SESSION.create_execution_ctx())?,
1262            Scalar::utf8("low", Nullability::NonNullable)
1263        );
1264        assert_eq!(
1265            result.execute_scalar(2, &mut SESSION.create_execution_ctx())?,
1266            Scalar::utf8("high", Nullability::NonNullable)
1267        );
1268        assert_eq!(
1269            result.execute_scalar(3, &mut SESSION.create_execution_ctx())?,
1270            Scalar::utf8("high", Nullability::NonNullable)
1271        );
1272        Ok(())
1273    }
1274
1275    #[test]
1276    fn test_evaluate_nary_with_nullable_conditions() {
1277        let mut ctx = SESSION.create_execution_ctx();
1278        let test_array = StructArray::from_fields(&[
1279            (
1280                "cond1",
1281                BoolArray::from_iter([Some(true), None, Some(false)]).into_array(),
1282            ),
1283            (
1284                "cond2",
1285                BoolArray::from_iter([Some(false), Some(true), None]).into_array(),
1286            ),
1287        ])
1288        .unwrap()
1289        .into_array();
1290
1291        let expr = nested_case_when(
1292            vec![
1293                (get_item("cond1", root()), lit(10i32)),
1294                (get_item("cond2", root()), lit(20i32)),
1295            ],
1296            Some(lit(0i32)),
1297        );
1298
1299        let result = evaluate_expr(&expr, &test_array);
1300        // row 0: cond1=true → 10
1301        // row 1: cond1=NULL(→false), cond2=true → 20
1302        // row 2: cond1=false, cond2=NULL(→false) → else=0
1303        assert_arrays_eq!(result, buffer![10i32, 20, 0].into_array(), &mut ctx);
1304    }
1305
1306    // ==================== Simplify: COALESCE -> fill_null ====================
1307
1308    /// Builds a non-nullable struct scope whose named fields are all `Nullable(I64)`.
1309    fn nullable_i64_scope(fields: &[&str]) -> DType {
1310        DType::Struct(
1311            StructFields::new(
1312                fields.to_vec().into(),
1313                vec![DType::Primitive(PType::I64, Nullability::Nullable); fields.len()],
1314            ),
1315            Nullability::NonNullable,
1316        )
1317    }
1318
1319    #[test]
1320    fn test_simplify_coalesce_is_null_rewrites_to_fill_null() -> VortexResult<()> {
1321        // CASE WHEN is_null(x) THEN 0 ELSE x END  ==>  fill_null(x, 0)
1322        let expr = case_when(is_null(col("x")), lit(0i64), col("x"));
1323        let optimized = expr
1324            .bind(&nullable_i64_scope(&["x"]))?
1325            .optimize_recursive()?;
1326        assert!(
1327            optimized.to_string().starts_with("vortex.fill_null"),
1328            "expected fill_null, got {optimized}"
1329        );
1330        Ok(())
1331    }
1332
1333    #[test]
1334    fn test_simplify_coalesce_is_not_null_rewrites_to_fill_null() -> VortexResult<()> {
1335        // CASE WHEN is_not_null(x) THEN x ELSE 0 END  ==>  fill_null(x, 0)
1336        let expr = case_when(is_not_null(col("x")), col("x"), lit(0i64));
1337        let optimized = expr
1338            .bind(&nullable_i64_scope(&["x"]))?
1339            .optimize_recursive()?;
1340        assert!(
1341            optimized.to_string().starts_with("vortex.fill_null"),
1342            "expected fill_null, got {optimized}"
1343        );
1344        Ok(())
1345    }
1346
1347    #[test]
1348    fn test_simplify_does_not_fire_when_operands_differ() -> VortexResult<()> {
1349        // The is_null operand (x) and the ELSE (y) are different columns: not a COALESCE.
1350        let expr = case_when(is_null(col("x")), lit(0i64), col("y"));
1351        let optimized = expr
1352            .bind(&nullable_i64_scope(&["x", "y"]))?
1353            .optimize_recursive()?;
1354        let s = optimized.to_string();
1355        assert!(s.contains("CASE"), "expected CASE WHEN to remain, got {s}");
1356        assert!(!s.contains("fill_null"), "must not rewrite, got {s}");
1357        Ok(())
1358    }
1359
1360    #[test]
1361    fn test_simplify_does_not_fire_for_non_constant_fill() -> VortexResult<()> {
1362        // COALESCE(x, c) with a *column* fill: fill_null cannot consume a non-constant
1363        // fill value, so the rewrite must not fire.
1364        let expr = case_when(is_null(col("x")), col("c"), col("x"));
1365        let optimized = expr
1366            .bind(&nullable_i64_scope(&["x", "c"]))?
1367            .optimize_recursive()?;
1368        let s = optimized.to_string();
1369        assert!(s.contains("CASE"), "expected CASE WHEN to remain, got {s}");
1370        assert!(!s.contains("fill_null"), "must not rewrite, got {s}");
1371        Ok(())
1372    }
1373
1374    #[test]
1375    fn test_simplify_null_fill_collapses_to_input() -> VortexResult<()> {
1376        // Filling the nulls of x with NULL is a no-op, so both forms collapse to just `x`.
1377        //   CASE WHEN is_null(x)     THEN null ELSE x    END  ==>  x
1378        //   CASE WHEN is_not_null(x) THEN x    ELSE null END  ==>  x
1379        let null_fill = || {
1380            lit(Scalar::null(DType::Primitive(
1381                PType::I64,
1382                Nullability::Nullable,
1383            )))
1384        };
1385
1386        for expr in [
1387            case_when(is_null(col("x")), null_fill(), col("x")),
1388            case_when(is_not_null(col("x")), col("x"), null_fill()),
1389        ] {
1390            let optimized = expr
1391                .bind(&nullable_i64_scope(&["x"]))?
1392                .optimize_recursive()?;
1393            assert_eq!(
1394                optimized.to_string(),
1395                "$.x",
1396                "expected collapse to input column, got {optimized}"
1397            );
1398        }
1399        Ok(())
1400    }
1401
1402    #[test]
1403    fn test_simplify_null_fill_semantic_equivalence() -> VortexResult<()> {
1404        let mut ctx = SESSION.create_execution_ctx();
1405        // The collapse-to-input rewrite must preserve values (and `x`'s nullability).
1406        let array = PrimitiveArray::from_option_iter([Some(1i64), None, Some(3)]).into_array();
1407        let scope = DType::Primitive(PType::I64, Nullability::Nullable);
1408        let null_fill = lit(Scalar::null(DType::Primitive(
1409            PType::I64,
1410            Nullability::Nullable,
1411        )));
1412
1413        let original = case_when(is_null(root()), null_fill, root());
1414        let optimized = original.bind(&scope)?.optimize_recursive()?;
1415        assert_eq!(
1416            optimized.to_string(),
1417            "$",
1418            "expected collapse to root, got {optimized}"
1419        );
1420
1421        let expected = PrimitiveArray::from_option_iter([Some(1i64), None, Some(3)]).into_array();
1422        assert_arrays_eq!(evaluate_expr(&original, &array), expected, &mut ctx);
1423        assert_arrays_eq!(evaluate_bound_expr(&optimized, &array), expected, &mut ctx);
1424        Ok(())
1425    }
1426
1427    #[test]
1428    fn test_simplify_does_not_fire_without_else() -> VortexResult<()> {
1429        let expr = case_when_no_else(is_null(col("x")), lit(0i64));
1430        let optimized = expr
1431            .bind(&nullable_i64_scope(&["x"]))?
1432            .optimize_recursive()?;
1433        assert!(
1434            !optimized.to_string().contains("fill_null"),
1435            "must not rewrite a no-ELSE case_when, got {optimized}"
1436        );
1437        Ok(())
1438    }
1439
1440    #[test]
1441    fn test_simplify_does_not_fire_for_multi_pair() -> VortexResult<()> {
1442        let expr = nested_case_when(
1443            vec![
1444                (is_null(col("x")), lit(0i64)),
1445                (gt(col("x"), lit(5i64)), lit(1i64)),
1446            ],
1447            Some(col("x")),
1448        );
1449        let optimized = expr
1450            .bind(&nullable_i64_scope(&["x"]))?
1451            .optimize_recursive()?;
1452        assert!(
1453            !optimized.to_string().contains("fill_null"),
1454            "must not rewrite a multi-pair case_when, got {optimized}"
1455        );
1456        Ok(())
1457    }
1458
1459    #[test]
1460    fn test_simplify_semantic_equivalence() -> VortexResult<()> {
1461        let mut ctx = SESSION.create_execution_ctx();
1462        // The optimized expression must produce the same values as the original CASE WHEN.
1463        let array = PrimitiveArray::from_option_iter([Some(1i64), None, Some(3)]).into_array();
1464        let scope = DType::Primitive(PType::I64, Nullability::Nullable);
1465
1466        let original = case_when(is_null(root()), lit(0i64), root());
1467        let optimized = original.bind(&scope)?.optimize_recursive()?;
1468        assert!(
1469            optimized.to_string().starts_with("vortex.fill_null"),
1470            "expected fill_null, got {optimized}"
1471        );
1472
1473        // Original keeps CASE WHEN's nullable result dtype; the rewrite tightens it to
1474        // NonNullable because a non-null fill cannot leave any nulls behind. Values match.
1475        assert_arrays_eq!(
1476            evaluate_expr(&original, &array),
1477            PrimitiveArray::from_option_iter([Some(1i64), Some(0), Some(3)]).into_array(),
1478            &mut ctx
1479        );
1480        assert_arrays_eq!(
1481            evaluate_bound_expr(&optimized, &array),
1482            buffer![1i64, 0, 3].into_array(),
1483            &mut ctx
1484        );
1485        Ok(())
1486    }
1487
1488    #[test]
1489    fn test_merge_case_branches_alternating_mask() -> VortexResult<()> {
1490        let mut ctx = SESSION.create_execution_ctx();
1491        // Exercises the scalar path: alternating rows produce one slice per row (no runs),
1492        // triggering the per-row cursor path in merge_case_branches.
1493        let n = 100usize;
1494
1495        // Branch 0: even rows → 0, Branch 1: odd rows → 1, Else: never reached.
1496        let branch0_mask = Mask::from_indices(n, (0..n).step_by(2));
1497        let branch1_mask = Mask::from_indices(n, (1..n).step_by(2));
1498
1499        let result = merge_case_branches(
1500            vec![
1501                (
1502                    branch0_mask,
1503                    PrimitiveArray::from_option_iter(vec![Some(0i32); n]).into_array(),
1504                ),
1505                (
1506                    branch1_mask,
1507                    PrimitiveArray::from_option_iter(vec![Some(1i32); n]).into_array(),
1508                ),
1509            ],
1510            PrimitiveArray::from_option_iter(vec![Some(99i32); n]).into_array(),
1511            &mut SESSION.create_execution_ctx(),
1512        )?;
1513
1514        // Even rows → 0, odd rows → 1.
1515        let expected: Vec<Option<i32>> = (0..n)
1516            .map(|v| if v % 2 == 0 { Some(0) } else { Some(1) })
1517            .collect();
1518        assert_arrays_eq!(
1519            result,
1520            PrimitiveArray::from_option_iter(expected).into_array(),
1521            &mut ctx
1522        );
1523        Ok(())
1524    }
1525}