Skip to main content

vortex_array/aggregate_fn/fns/is_constant/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4mod bool;
5mod decimal;
6mod extension;
7mod fixed_size_list;
8mod list;
9mod map;
10pub mod primitive;
11mod struct_;
12mod varbin;
13
14use vortex_error::VortexExpect;
15use vortex_error::VortexResult;
16use vortex_error::vortex_bail;
17use vortex_session::registry::CachedId;
18
19use self::bool::check_bool_constant;
20use self::decimal::check_decimal_constant;
21use self::extension::check_extension_constant;
22use self::fixed_size_list::check_fixed_size_list_constant;
23use self::list::check_listview_constant;
24use self::map::check_map_constant;
25use self::primitive::check_primitive_constant;
26use self::struct_::check_struct_constant;
27use self::varbin::check_varbinview_constant;
28use crate::ArrayRef;
29use crate::Canonical;
30use crate::Columnar;
31use crate::ExecutionCtx;
32use crate::IntoArray;
33use crate::aggregate_fn::Accumulator;
34use crate::aggregate_fn::AggregateFnId;
35use crate::aggregate_fn::AggregateFnVTable;
36use crate::aggregate_fn::DynAccumulator;
37use crate::aggregate_fn::EmptyOptions;
38use crate::arrays::Constant;
39use crate::arrays::Null;
40use crate::builtins::ArrayBuiltins;
41use crate::dtype::DType;
42use crate::dtype::FieldNames;
43use crate::dtype::Nullability;
44use crate::dtype::StructFields;
45use crate::expr::stats::Precision;
46use crate::expr::stats::Stat;
47use crate::expr::stats::StatsProvider;
48use crate::expr::stats::StatsProviderExt;
49use crate::scalar::Scalar;
50use crate::scalar_fn::fns::operators::Operator;
51
52/// Check if two arrays of the same length have equal values at every position (null-safe).
53///
54/// Two positions are considered equal if they are both null, or both non-null with the same value.
55// TODO(ngates): move this function out when we have any/all aggregate functions.
56fn arrays_value_equal(a: &ArrayRef, b: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<bool> {
57    debug_assert_eq!(a.len(), b.len());
58    if a.is_empty() {
59        return Ok(true);
60    }
61
62    // Check validity masks match (null positions must be identical).
63    let a_mask = a.validity()?.execute_mask(a.len(), ctx)?;
64    let b_mask = b.validity()?.execute_mask(b.len(), ctx)?;
65    if a_mask != b_mask {
66        return Ok(false);
67    }
68
69    let valid_count = a_mask.true_count();
70    if valid_count == 0 {
71        // Both all-null → equal.
72        return Ok(true);
73    }
74
75    // Compare values element-wise. Result is null where both inputs are null,
76    // true/false where both are valid.
77    let eq_result = a.binary(b.clone(), Operator::Eq)?;
78    let eq_result = eq_result.null_as_false().execute(ctx)?;
79
80    Ok(eq_result.true_count() == valid_count)
81}
82
83/// Compute whether an array has constant values.
84///
85/// An array is constant IFF at least one of the following conditions apply:
86/// 1. It has at least one element (**Note** - an empty array isn't constant).
87/// 2. It's encoded as a [`ConstantArray`](crate::arrays::ConstantArray) or [`NullArray`](crate::arrays::NullArray)
88/// 3. Has an exact statistic attached to it, saying its constant.
89/// 4. Is all invalid.
90/// 5. Is all valid AND has minimum and maximum statistics that are equal.
91pub fn is_constant(array: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<bool> {
92    // Short-circuit using cached array statistics.
93    if let Precision::Exact(value) = array.statistics().get_as::<bool>(Stat::IsConstant) {
94        return Ok(value);
95    }
96
97    // Empty arrays are not constant.
98    if array.is_empty() {
99        return Ok(false);
100    }
101
102    // Array of length 1 is always constant.
103    if array.len() == 1 {
104        array
105            .statistics()
106            .set(Stat::IsConstant, Precision::Exact(true.into()));
107        return Ok(true);
108    }
109
110    // Constant and null arrays are always constant.
111    if array.is::<Constant>() || array.is::<Null>() {
112        array
113            .statistics()
114            .set(Stat::IsConstant, Precision::Exact(true.into()));
115        return Ok(true);
116    }
117
118    let all_invalid = array.all_invalid(ctx)?;
119    if all_invalid {
120        array
121            .statistics()
122            .set(Stat::IsConstant, Precision::Exact(true.into()));
123        return Ok(true);
124    }
125
126    let all_valid = array.all_valid(ctx)?;
127
128    // If we have some nulls but not all nulls, array can't be constant.
129    if !all_valid && !all_invalid {
130        array
131            .statistics()
132            .set(Stat::IsConstant, Precision::Exact(false.into()));
133        return Ok(false);
134    }
135
136    // We already know here that the array is all valid, so we check for min/max stats.
137    let min_stat = array.statistics().get(Stat::Min);
138    let max_stat = array.statistics().get(Stat::Max);
139
140    if let Precision::Exact(min) = min_stat.as_ref()
141        && let Precision::Exact(max) = max_stat.as_ref()
142        && min == max
143        && (Stat::NaNCount.dtype(array.dtype()).is_none()
144            || array.statistics().get_as::<u64>(Stat::NaNCount) == Precision::exact(0u64))
145    {
146        array
147            .statistics()
148            .set(Stat::IsConstant, Precision::Exact(true.into()));
149        return Ok(true);
150    }
151
152    // Short-circuit for unsupported dtypes.
153    if IsConstant
154        .return_dtype(&EmptyOptions, array.dtype())
155        .is_none()
156    {
157        // Null dtype - vacuously false for empty
158        return Ok(false);
159    }
160
161    // Compute using Accumulator<IsConstant>.
162    let mut acc = Accumulator::try_new(IsConstant, EmptyOptions, array.dtype().clone())?;
163    acc.accumulate(array, ctx)?;
164    let result_scalar = acc.finish()?;
165
166    let result = result_scalar.as_bool().value().unwrap_or(false);
167
168    // Cache the computed is_constant as a statistic.
169    array
170        .statistics()
171        .set(Stat::IsConstant, Precision::Exact(result.into()));
172
173    Ok(result)
174}
175
176/// Compute whether an array is constant.
177///
178/// Returns a `Bool(NonNullable)` scalar.
179/// The partial state is a nullable struct `{is_constant: Bool(NN), value: input_dtype?}`.
180/// A null struct means the accumulator has seen no data yet (empty).
181#[derive(Clone, Debug)]
182pub struct IsConstant;
183
184impl IsConstant {
185    /// Build a partial scalar from a kernel's `is_constant` result.
186    ///
187    /// Kernels that compute `is_constant` by delegating to child arrays can call this
188    /// to package the boolean result into the partial struct format expected by the
189    /// accumulator, avoiding duplicated boilerplate.
190    pub fn make_partial(
191        batch: &ArrayRef,
192        is_constant: bool,
193        ctx: &mut ExecutionCtx,
194    ) -> VortexResult<Scalar> {
195        let partial_dtype = make_is_constant_partial_dtype(batch.dtype());
196        if is_constant {
197            if batch.is_empty() {
198                return Ok(Scalar::null(partial_dtype));
199            }
200            let first_value = batch.execute_scalar(0, ctx)?.into_nullable();
201            Ok(Scalar::struct_(
202                partial_dtype,
203                vec![Scalar::bool(true, Nullability::NonNullable), first_value],
204            ))
205        } else {
206            Ok(Scalar::struct_(
207                partial_dtype,
208                vec![
209                    Scalar::bool(false, Nullability::NonNullable),
210                    Scalar::null(batch.dtype().as_nullable()),
211                ],
212            ))
213        }
214    }
215}
216
217/// Partial accumulator state for is_constant.
218pub struct IsConstantPartial {
219    is_constant: bool,
220    /// None = empty (no values seen), Some(null) = all nulls, Some(v) = first value seen.
221    first_value: Option<Scalar>,
222    element_dtype: DType,
223}
224
225impl IsConstantPartial {
226    fn check_value(&mut self, value: Scalar) {
227        if !self.is_constant {
228            return;
229        }
230        match &self.first_value {
231            None => {
232                self.first_value = Some(value);
233            }
234            Some(first) => {
235                if *first != value {
236                    self.is_constant = false;
237                }
238            }
239        }
240    }
241}
242
243static NAMES: std::sync::LazyLock<FieldNames> =
244    std::sync::LazyLock::new(|| FieldNames::from(["is_constant", "value"]));
245
246pub fn make_is_constant_partial_dtype(element_dtype: &DType) -> DType {
247    DType::Struct(
248        StructFields::new(
249            NAMES.clone(),
250            vec![
251                DType::Bool(Nullability::NonNullable),
252                element_dtype.as_nullable(),
253            ],
254        ),
255        Nullability::Nullable,
256    )
257}
258
259impl AggregateFnVTable for IsConstant {
260    type Options = EmptyOptions;
261    type Partial = IsConstantPartial;
262
263    fn id(&self) -> AggregateFnId {
264        static ID: CachedId = CachedId::new("vortex.is_constant");
265        *ID
266    }
267
268    fn serialize(&self, _options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
269        unimplemented!("IsConstant is not yet serializable");
270    }
271
272    fn return_dtype(&self, _options: &Self::Options, input_dtype: &DType) -> Option<DType> {
273        match input_dtype {
274            DType::Null | DType::Variant(..) => None,
275            _ => Some(DType::Bool(Nullability::NonNullable)),
276        }
277    }
278
279    fn partial_dtype(&self, _options: &Self::Options, input_dtype: &DType) -> Option<DType> {
280        match input_dtype {
281            DType::Null | DType::Variant(..) => None,
282            _ => Some(make_is_constant_partial_dtype(input_dtype)),
283        }
284    }
285
286    fn empty_partial(
287        &self,
288        _options: &Self::Options,
289        input_dtype: &DType,
290    ) -> VortexResult<Self::Partial> {
291        Ok(IsConstantPartial {
292            is_constant: true,
293            first_value: None,
294            element_dtype: input_dtype.clone(),
295        })
296    }
297
298    fn combine_partials(&self, partial: &mut Self::Partial, other: Scalar) -> VortexResult<()> {
299        if !partial.is_constant {
300            return Ok(());
301        }
302
303        // Null struct means the other accumulator was empty, skip it.
304        if other.is_null() {
305            return Ok(());
306        }
307
308        let other_is_constant = other
309            .as_struct()
310            .field_by_idx(0)
311            .map(|s| s.as_bool().value().unwrap_or(false))
312            .unwrap_or(false);
313
314        if !other_is_constant {
315            partial.is_constant = false;
316            return Ok(());
317        }
318
319        let other_value = other.as_struct().field_by_idx(1);
320
321        if let Some(other_val) = other_value {
322            partial.check_value(other_val);
323        }
324
325        Ok(())
326    }
327
328    fn to_scalar(&self, partial: &Self::Partial) -> VortexResult<Scalar> {
329        let dtype = make_is_constant_partial_dtype(&partial.element_dtype);
330        Ok(match &partial.first_value {
331            None => {
332                // Empty accumulator — return null struct.
333                Scalar::null(dtype)
334            }
335            Some(first_value) => Scalar::struct_(
336                dtype,
337                vec![
338                    Scalar::bool(partial.is_constant, Nullability::NonNullable),
339                    first_value
340                        .clone()
341                        .cast(&partial.element_dtype.as_nullable())?,
342                ],
343            ),
344        })
345    }
346
347    fn reset(&self, partial: &mut Self::Partial) {
348        partial.is_constant = true;
349        partial.first_value = None;
350    }
351
352    #[inline]
353    fn is_saturated(&self, partial: &Self::Partial) -> bool {
354        !partial.is_constant
355    }
356
357    fn accumulate(
358        &self,
359        partial: &mut Self::Partial,
360        batch: &Columnar,
361        ctx: &mut ExecutionCtx,
362    ) -> VortexResult<()> {
363        if !partial.is_constant {
364            return Ok(());
365        }
366
367        match batch {
368            Columnar::Constant(c) => {
369                partial.check_value(c.scalar().clone().into_nullable());
370                Ok(())
371            }
372            Columnar::Canonical(c) => {
373                if c.is_empty() {
374                    return Ok(());
375                }
376
377                // Convert to ArrayRef for DynArrayData methods.
378                let array_ref = c.clone().into_array();
379
380                let all_invalid = array_ref.all_invalid(ctx)?;
381                if all_invalid {
382                    partial.check_value(Scalar::null(partial.element_dtype.as_nullable()));
383                    return Ok(());
384                }
385
386                let all_valid = array_ref.all_valid(ctx)?;
387                // Mixed nulls → not constant.
388                if !all_valid && !all_invalid {
389                    partial.is_constant = false;
390                    return Ok(());
391                }
392
393                // All valid from here. Check batch-level constancy.
394                if c.len() == 1 {
395                    partial.check_value(array_ref.execute_scalar(0, ctx)?.into_nullable());
396                    return Ok(());
397                }
398
399                let batch_is_constant = match c {
400                    Canonical::Primitive(p) => check_primitive_constant(p),
401                    Canonical::Bool(b) => check_bool_constant(b),
402                    Canonical::VarBinView(v) => check_varbinview_constant(v),
403                    Canonical::Decimal(d) => check_decimal_constant(d),
404                    Canonical::Struct(s) => check_struct_constant(s, ctx)?,
405                    Canonical::Extension(e) => check_extension_constant(e, ctx)?,
406                    Canonical::List(l) => check_listview_constant(l, ctx)?,
407                    Canonical::Map(m) => check_map_constant(m, ctx)?,
408                    Canonical::FixedSizeList(f) => check_fixed_size_list_constant(f, ctx)?,
409                    Canonical::Null(_) => true,
410                    Canonical::Union(_) => {
411                        todo!("TODO(connor)[Union]: implement IsConstant for Union arrays")
412                    }
413                    Canonical::Variant(_) => {
414                        vortex_bail!("Variant arrays don't support IsConstant")
415                    }
416                };
417
418                if !batch_is_constant {
419                    partial.is_constant = false;
420                    return Ok(());
421                }
422
423                partial.check_value(array_ref.execute_scalar(0, ctx)?.into_nullable());
424                Ok(())
425            }
426        }
427    }
428
429    fn finalize(&self, partials: ArrayRef) -> VortexResult<ArrayRef> {
430        partials.get_item(NAMES.get(0).vortex_expect("out of bounds").clone())
431    }
432
433    fn finalize_scalar(&self, partial: &Self::Partial) -> VortexResult<Scalar> {
434        if partial.first_value.is_none() {
435            // Empty accumulator → return false.
436            return Ok(Scalar::bool(false, Nullability::NonNullable));
437        }
438        Ok(Scalar::bool(partial.is_constant, Nullability::NonNullable))
439    }
440}
441
442#[cfg(test)]
443mod tests {
444    use rstest::rstest;
445    use vortex_buffer::Buffer;
446    use vortex_buffer::buffer;
447    use vortex_error::VortexResult;
448
449    use crate::IntoArray as _;
450    use crate::VortexSessionExecute;
451    use crate::aggregate_fn::fns::is_constant::is_constant;
452    use crate::array_session;
453    use crate::arrays::BoolArray;
454    use crate::arrays::ChunkedArray;
455    use crate::arrays::DecimalArray;
456    use crate::arrays::ListArray;
457    use crate::arrays::PrimitiveArray;
458    use crate::arrays::StructArray;
459    use crate::builders::MapBuilder;
460    use crate::dtype::DType;
461    use crate::dtype::DecimalDType;
462    use crate::dtype::FieldNames;
463    use crate::dtype::MapDType;
464    use crate::dtype::Nullability;
465    use crate::dtype::PType;
466    use crate::expr::stats::Stat;
467    use crate::scalar::Scalar;
468    use crate::validity::Validity;
469
470    type MapEntryFixture<'a> = (i32, Option<&'a str>);
471    type MapRowFixture<'a> = Option<Vec<MapEntryFixture<'a>>>;
472
473    fn map_array_from_rows(rows: &[MapRowFixture<'_>]) -> VortexResult<crate::ArrayRef> {
474        let map_dtype = MapDType::try_new(
475            DType::Primitive(PType::I32, Nullability::NonNullable),
476            DType::Utf8(Nullability::Nullable),
477            false,
478        )?;
479        let dtype = DType::Map(map_dtype.clone(), Nullability::Nullable);
480        let mut builder = MapBuilder::<u64, u64>::with_capacity_in(
481            map_dtype,
482            Nullability::Nullable,
483            rows.len(),
484            vortex_buffer::BufferAllocatorRef::static_ref(),
485        );
486
487        for row in rows {
488            let scalar = match row {
489                Some(entries) => {
490                    let entries = entries
491                        .iter()
492                        .map(|(key, value)| {
493                            let key = Scalar::primitive(*key, Nullability::NonNullable);
494                            let value = value.map_or_else(
495                                || Scalar::null(DType::Utf8(Nullability::Nullable)),
496                                |value| Scalar::utf8(value, Nullability::Nullable),
497                            );
498                            (key, value)
499                        })
500                        .collect::<Vec<_>>();
501                    Scalar::try_map(dtype.clone(), entries)?
502                }
503                None => Scalar::null(dtype.clone()),
504            };
505            builder.append_value(scalar.as_map())?;
506        }
507
508        Ok(builder.finish_into_map().into_array())
509    }
510
511    // Tests migrated from compute/is_constant.rs
512    #[test]
513    fn is_constant_min_max_no_nan() -> VortexResult<()> {
514        let mut ctx = array_session().create_execution_ctx();
515
516        let arr = buffer![0, 1].into_array();
517        arr.statistics()
518            .compute_all(&[Stat::Min, Stat::Max], &mut ctx)?;
519        assert!(!is_constant(&arr, &mut ctx)?);
520
521        let arr = buffer![0, 0].into_array();
522        arr.statistics()
523            .compute_all(&[Stat::Min, Stat::Max], &mut ctx)?;
524        assert!(is_constant(&arr, &mut ctx)?);
525
526        let arr = PrimitiveArray::from_option_iter([Some(0), Some(0)]).into_array();
527        assert!(is_constant(&arr, &mut ctx)?);
528        Ok(())
529    }
530
531    #[test]
532    fn is_constant_min_max_with_nan() -> VortexResult<()> {
533        let mut ctx = array_session().create_execution_ctx();
534
535        let arr = PrimitiveArray::from_iter([0.0, 0.0, f32::NAN]).into_array();
536        arr.statistics()
537            .compute_all(&[Stat::Min, Stat::Max], &mut ctx)?;
538        assert!(!is_constant(&arr, &mut ctx)?);
539
540        let arr =
541            PrimitiveArray::from_option_iter([Some(f32::NEG_INFINITY), Some(f32::NEG_INFINITY)])
542                .into_array();
543        arr.statistics()
544            .compute_all(&[Stat::Min, Stat::Max], &mut ctx)?;
545        assert!(is_constant(&arr, &mut ctx)?);
546        Ok(())
547    }
548
549    // Tests migrated from arrays/bool/compute/is_constant.rs
550    #[rstest]
551    #[case(vec![true], true)]
552    #[case(vec![false; 65], true)]
553    #[case({
554        let mut v = vec![true; 64];
555        v.push(false);
556        v
557    }, false)]
558    fn test_bool_is_constant(#[case] input: Vec<bool>, #[case] expected: bool) -> VortexResult<()> {
559        let array = BoolArray::from_iter(input);
560        let mut ctx = array_session().create_execution_ctx();
561        assert_eq!(is_constant(&array.into_array(), &mut ctx)?, expected);
562        Ok(())
563    }
564
565    // Tests migrated from arrays/chunked/compute/is_constant.rs
566    #[test]
567    fn empty_chunk_is_constant() -> VortexResult<()> {
568        let chunked = ChunkedArray::try_new(
569            vec![
570                Buffer::<u8>::empty().into_array(),
571                Buffer::<u8>::empty().into_array(),
572                buffer![255u8, 255].into_array(),
573                Buffer::<u8>::empty().into_array(),
574                buffer![255u8, 255].into_array(),
575            ],
576            DType::Primitive(PType::U8, Nullability::NonNullable),
577        )?
578        .into_array();
579
580        let mut ctx = array_session().create_execution_ctx();
581        assert!(is_constant(&chunked, &mut ctx)?);
582        Ok(())
583    }
584
585    // Tests migrated from arrays/decimal/compute/is_constant.rs
586    #[test]
587    fn test_decimal_is_constant() -> VortexResult<()> {
588        let mut ctx = array_session().create_execution_ctx();
589
590        let array = DecimalArray::new(
591            buffer![0i128, 1i128, 2i128],
592            DecimalDType::new(19, 0),
593            Validity::NonNullable,
594        );
595        assert!(!is_constant(&array.into_array(), &mut ctx)?);
596
597        let array = DecimalArray::new(
598            buffer![100i128, 100i128, 100i128],
599            DecimalDType::new(19, 0),
600            Validity::NonNullable,
601        );
602        assert!(is_constant(&array.into_array(), &mut ctx)?);
603        Ok(())
604    }
605
606    // Tests migrated from arrays/list/compute/is_constant.rs
607    #[test]
608    fn test_is_constant_nested_list() -> VortexResult<()> {
609        let mut ctx = array_session().create_execution_ctx();
610
611        let xs = ListArray::try_new(
612            buffer![0i32, 1, 0, 1].into_array(),
613            buffer![0u32, 2, 4].into_array(),
614            Validity::NonNullable,
615        )?;
616
617        let struct_of_lists = StructArray::try_new(
618            FieldNames::from(["xs"]),
619            vec![xs.into_array()],
620            2,
621            Validity::NonNullable,
622        )?;
623        assert!(is_constant(
624            &struct_of_lists.clone().into_array(),
625            &mut ctx
626        )?);
627        assert!(is_constant(&struct_of_lists.into_array(), &mut ctx)?);
628        Ok(())
629    }
630
631    #[rstest]
632    #[case(
633        // [1,2], [1, 2], [1, 2]
634        vec![1i32, 2, 1, 2, 1, 2],
635        vec![0u32, 2, 4, 6],
636        true
637    )]
638    #[case(
639        // [1, 2], [3], [4, 5]
640        vec![1i32, 2, 3, 4, 5],
641        vec![0u32, 2, 3, 5],
642        false
643    )]
644    #[case(
645        // [1, 2], [3, 4]
646        vec![1i32, 2, 3, 4],
647        vec![0u32, 2, 4],
648        false
649    )]
650    #[case(
651        // [], [], []
652        vec![],
653        vec![0u32, 0, 0, 0],
654        true
655    )]
656    fn test_list_is_constant(
657        #[case] elements: Vec<i32>,
658        #[case] offsets: Vec<u32>,
659        #[case] expected: bool,
660    ) -> VortexResult<()> {
661        let list_array = ListArray::try_new(
662            PrimitiveArray::from_iter(elements).into_array(),
663            PrimitiveArray::from_iter(offsets).into_array(),
664            Validity::NonNullable,
665        )?;
666
667        let mut ctx = array_session().create_execution_ctx();
668        assert_eq!(is_constant(&list_array.into_array(), &mut ctx)?, expected);
669        Ok(())
670    }
671
672    #[test]
673    fn test_list_is_constant_nested_lists() -> VortexResult<()> {
674        let inner_elements = buffer![1i32, 2, 1, 2].into_array();
675        let inner_offsets = buffer![0u32, 1, 2, 3, 4].into_array();
676        let inner_lists = ListArray::try_new(inner_elements, inner_offsets, Validity::NonNullable)?;
677
678        let outer_offsets = buffer![0u32, 2, 4].into_array();
679        let outer_list = ListArray::try_new(
680            inner_lists.into_array(),
681            outer_offsets,
682            Validity::NonNullable,
683        )?;
684
685        let mut ctx = array_session().create_execution_ctx();
686        // Both outer lists contain [[1], [2]], so should be constant
687        assert!(is_constant(&outer_list.into_array(), &mut ctx)?);
688        Ok(())
689    }
690
691    #[rstest]
692    #[case(
693        // 100 identical [1, 2] lists
694        [1i32, 2].repeat(100),
695        (0..101).map(|i| (i * 2) as u32).collect(),
696        true
697    )]
698    #[case(
699        // Difference after threshold: 64 identical [1, 2] + one [3, 4]
700        {
701            let mut elements = [1i32, 2].repeat(64);
702            elements.extend_from_slice(&[3, 4]);
703            elements
704        },
705        (0..66).map(|i| (i * 2) as u32).collect(),
706        false
707    )]
708    #[case(
709        // Difference in first 64: first 63 identical [1, 2] + one [3, 4] + rest identical [1, 2]
710        {
711            let mut elements = [1i32, 2].repeat(63);
712            elements.extend_from_slice(&[3, 4]);
713            elements.extend([1i32, 2].repeat(37));
714            elements
715        },
716        (0..101).map(|i| (i * 2) as u32).collect(),
717        false
718    )]
719    fn test_list_is_constant_with_threshold(
720        #[case] elements: Vec<i32>,
721        #[case] offsets: Vec<u32>,
722        #[case] expected: bool,
723    ) -> VortexResult<()> {
724        let list_array = ListArray::try_new(
725            PrimitiveArray::from_iter(elements).into_array(),
726            PrimitiveArray::from_iter(offsets).into_array(),
727            Validity::NonNullable,
728        )?;
729
730        let mut ctx = array_session().create_execution_ctx();
731        assert_eq!(is_constant(&list_array.into_array(), &mut ctx)?, expected);
732        Ok(())
733    }
734
735    #[test]
736    fn test_map_is_constant() -> VortexResult<()> {
737        let mut ctx = array_session().create_execution_ctx();
738
739        let identical = map_array_from_rows(&[
740            Some(vec![(1, Some("one")), (2, None)]),
741            Some(vec![(1, Some("one")), (2, None)]),
742        ])?;
743        assert!(is_constant(&identical, &mut ctx)?);
744
745        let different = map_array_from_rows(&[
746            Some(vec![(1, Some("one")), (2, None)]),
747            Some(vec![(1, Some("one")), (3, None)]),
748        ])?;
749        assert!(!is_constant(&different, &mut ctx)?);
750
751        let all_null = map_array_from_rows(&[None, None])?;
752        assert!(is_constant(&all_null, &mut ctx)?);
753
754        Ok(())
755    }
756}