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 =
481            MapBuilder::<u64, u64>::with_capacity(map_dtype, Nullability::Nullable, rows.len());
482
483        for row in rows {
484            let scalar = match row {
485                Some(entries) => {
486                    let entries = entries
487                        .iter()
488                        .map(|(key, value)| {
489                            let key = Scalar::primitive(*key, Nullability::NonNullable);
490                            let value = value.map_or_else(
491                                || Scalar::null(DType::Utf8(Nullability::Nullable)),
492                                |value| Scalar::utf8(value, Nullability::Nullable),
493                            );
494                            (key, value)
495                        })
496                        .collect::<Vec<_>>();
497                    Scalar::try_map(dtype.clone(), entries)?
498                }
499                None => Scalar::null(dtype.clone()),
500            };
501            builder.append_value(scalar.as_map())?;
502        }
503
504        Ok(builder.finish_into_map().into_array())
505    }
506
507    // Tests migrated from compute/is_constant.rs
508    #[test]
509    fn is_constant_min_max_no_nan() -> VortexResult<()> {
510        let mut ctx = array_session().create_execution_ctx();
511
512        let arr = buffer![0, 1].into_array();
513        arr.statistics()
514            .compute_all(&[Stat::Min, Stat::Max], &mut ctx)?;
515        assert!(!is_constant(&arr, &mut ctx)?);
516
517        let arr = buffer![0, 0].into_array();
518        arr.statistics()
519            .compute_all(&[Stat::Min, Stat::Max], &mut ctx)?;
520        assert!(is_constant(&arr, &mut ctx)?);
521
522        let arr = PrimitiveArray::from_option_iter([Some(0), Some(0)]).into_array();
523        assert!(is_constant(&arr, &mut ctx)?);
524        Ok(())
525    }
526
527    #[test]
528    fn is_constant_min_max_with_nan() -> VortexResult<()> {
529        let mut ctx = array_session().create_execution_ctx();
530
531        let arr = PrimitiveArray::from_iter([0.0, 0.0, f32::NAN]).into_array();
532        arr.statistics()
533            .compute_all(&[Stat::Min, Stat::Max], &mut ctx)?;
534        assert!(!is_constant(&arr, &mut ctx)?);
535
536        let arr =
537            PrimitiveArray::from_option_iter([Some(f32::NEG_INFINITY), Some(f32::NEG_INFINITY)])
538                .into_array();
539        arr.statistics()
540            .compute_all(&[Stat::Min, Stat::Max], &mut ctx)?;
541        assert!(is_constant(&arr, &mut ctx)?);
542        Ok(())
543    }
544
545    // Tests migrated from arrays/bool/compute/is_constant.rs
546    #[rstest]
547    #[case(vec![true], true)]
548    #[case(vec![false; 65], true)]
549    #[case({
550        let mut v = vec![true; 64];
551        v.push(false);
552        v
553    }, false)]
554    fn test_bool_is_constant(#[case] input: Vec<bool>, #[case] expected: bool) -> VortexResult<()> {
555        let array = BoolArray::from_iter(input);
556        let mut ctx = array_session().create_execution_ctx();
557        assert_eq!(is_constant(&array.into_array(), &mut ctx)?, expected);
558        Ok(())
559    }
560
561    // Tests migrated from arrays/chunked/compute/is_constant.rs
562    #[test]
563    fn empty_chunk_is_constant() -> VortexResult<()> {
564        let chunked = ChunkedArray::try_new(
565            vec![
566                Buffer::<u8>::empty().into_array(),
567                Buffer::<u8>::empty().into_array(),
568                buffer![255u8, 255].into_array(),
569                Buffer::<u8>::empty().into_array(),
570                buffer![255u8, 255].into_array(),
571            ],
572            DType::Primitive(PType::U8, Nullability::NonNullable),
573        )?
574        .into_array();
575
576        let mut ctx = array_session().create_execution_ctx();
577        assert!(is_constant(&chunked, &mut ctx)?);
578        Ok(())
579    }
580
581    // Tests migrated from arrays/decimal/compute/is_constant.rs
582    #[test]
583    fn test_decimal_is_constant() -> VortexResult<()> {
584        let mut ctx = array_session().create_execution_ctx();
585
586        let array = DecimalArray::new(
587            buffer![0i128, 1i128, 2i128],
588            DecimalDType::new(19, 0),
589            Validity::NonNullable,
590        );
591        assert!(!is_constant(&array.into_array(), &mut ctx)?);
592
593        let array = DecimalArray::new(
594            buffer![100i128, 100i128, 100i128],
595            DecimalDType::new(19, 0),
596            Validity::NonNullable,
597        );
598        assert!(is_constant(&array.into_array(), &mut ctx)?);
599        Ok(())
600    }
601
602    // Tests migrated from arrays/list/compute/is_constant.rs
603    #[test]
604    fn test_is_constant_nested_list() -> VortexResult<()> {
605        let mut ctx = array_session().create_execution_ctx();
606
607        let xs = ListArray::try_new(
608            buffer![0i32, 1, 0, 1].into_array(),
609            buffer![0u32, 2, 4].into_array(),
610            Validity::NonNullable,
611        )?;
612
613        let struct_of_lists = StructArray::try_new(
614            FieldNames::from(["xs"]),
615            vec![xs.into_array()],
616            2,
617            Validity::NonNullable,
618        )?;
619        assert!(is_constant(
620            &struct_of_lists.clone().into_array(),
621            &mut ctx
622        )?);
623        assert!(is_constant(&struct_of_lists.into_array(), &mut ctx)?);
624        Ok(())
625    }
626
627    #[rstest]
628    #[case(
629        // [1,2], [1, 2], [1, 2]
630        vec![1i32, 2, 1, 2, 1, 2],
631        vec![0u32, 2, 4, 6],
632        true
633    )]
634    #[case(
635        // [1, 2], [3], [4, 5]
636        vec![1i32, 2, 3, 4, 5],
637        vec![0u32, 2, 3, 5],
638        false
639    )]
640    #[case(
641        // [1, 2], [3, 4]
642        vec![1i32, 2, 3, 4],
643        vec![0u32, 2, 4],
644        false
645    )]
646    #[case(
647        // [], [], []
648        vec![],
649        vec![0u32, 0, 0, 0],
650        true
651    )]
652    fn test_list_is_constant(
653        #[case] elements: Vec<i32>,
654        #[case] offsets: Vec<u32>,
655        #[case] expected: bool,
656    ) -> VortexResult<()> {
657        let list_array = ListArray::try_new(
658            PrimitiveArray::from_iter(elements).into_array(),
659            PrimitiveArray::from_iter(offsets).into_array(),
660            Validity::NonNullable,
661        )?;
662
663        let mut ctx = array_session().create_execution_ctx();
664        assert_eq!(is_constant(&list_array.into_array(), &mut ctx)?, expected);
665        Ok(())
666    }
667
668    #[test]
669    fn test_list_is_constant_nested_lists() -> VortexResult<()> {
670        let inner_elements = buffer![1i32, 2, 1, 2].into_array();
671        let inner_offsets = buffer![0u32, 1, 2, 3, 4].into_array();
672        let inner_lists = ListArray::try_new(inner_elements, inner_offsets, Validity::NonNullable)?;
673
674        let outer_offsets = buffer![0u32, 2, 4].into_array();
675        let outer_list = ListArray::try_new(
676            inner_lists.into_array(),
677            outer_offsets,
678            Validity::NonNullable,
679        )?;
680
681        let mut ctx = array_session().create_execution_ctx();
682        // Both outer lists contain [[1], [2]], so should be constant
683        assert!(is_constant(&outer_list.into_array(), &mut ctx)?);
684        Ok(())
685    }
686
687    #[rstest]
688    #[case(
689        // 100 identical [1, 2] lists
690        [1i32, 2].repeat(100),
691        (0..101).map(|i| (i * 2) as u32).collect(),
692        true
693    )]
694    #[case(
695        // Difference after threshold: 64 identical [1, 2] + one [3, 4]
696        {
697            let mut elements = [1i32, 2].repeat(64);
698            elements.extend_from_slice(&[3, 4]);
699            elements
700        },
701        (0..66).map(|i| (i * 2) as u32).collect(),
702        false
703    )]
704    #[case(
705        // Difference in first 64: first 63 identical [1, 2] + one [3, 4] + rest identical [1, 2]
706        {
707            let mut elements = [1i32, 2].repeat(63);
708            elements.extend_from_slice(&[3, 4]);
709            elements.extend([1i32, 2].repeat(37));
710            elements
711        },
712        (0..101).map(|i| (i * 2) as u32).collect(),
713        false
714    )]
715    fn test_list_is_constant_with_threshold(
716        #[case] elements: Vec<i32>,
717        #[case] offsets: Vec<u32>,
718        #[case] expected: bool,
719    ) -> VortexResult<()> {
720        let list_array = ListArray::try_new(
721            PrimitiveArray::from_iter(elements).into_array(),
722            PrimitiveArray::from_iter(offsets).into_array(),
723            Validity::NonNullable,
724        )?;
725
726        let mut ctx = array_session().create_execution_ctx();
727        assert_eq!(is_constant(&list_array.into_array(), &mut ctx)?, expected);
728        Ok(())
729    }
730
731    #[test]
732    fn test_map_is_constant() -> VortexResult<()> {
733        let mut ctx = array_session().create_execution_ctx();
734
735        let identical = map_array_from_rows(&[
736            Some(vec![(1, Some("one")), (2, None)]),
737            Some(vec![(1, Some("one")), (2, None)]),
738        ])?;
739        assert!(is_constant(&identical, &mut ctx)?);
740
741        let different = map_array_from_rows(&[
742            Some(vec![(1, Some("one")), (2, None)]),
743            Some(vec![(1, Some("one")), (3, None)]),
744        ])?;
745        assert!(!is_constant(&different, &mut ctx)?);
746
747        let all_null = map_array_from_rows(&[None, None])?;
748        assert!(is_constant(&all_null, &mut ctx)?);
749
750        Ok(())
751    }
752}