1mod 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
52fn 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 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 return Ok(true);
73 }
74
75 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
83pub fn is_constant(array: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<bool> {
92 if let Precision::Exact(value) = array.statistics().get_as::<bool>(Stat::IsConstant) {
94 return Ok(value);
95 }
96
97 if array.is_empty() {
99 return Ok(false);
100 }
101
102 if array.len() == 1 {
104 array
105 .statistics()
106 .set(Stat::IsConstant, Precision::Exact(true.into()));
107 return Ok(true);
108 }
109
110 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 !all_valid && !all_invalid {
130 array
131 .statistics()
132 .set(Stat::IsConstant, Precision::Exact(false.into()));
133 return Ok(false);
134 }
135
136 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 if IsConstant
154 .return_dtype(&EmptyOptions, array.dtype())
155 .is_none()
156 {
157 return Ok(false);
159 }
160
161 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 array
170 .statistics()
171 .set(Stat::IsConstant, Precision::Exact(result.into()));
172
173 Ok(result)
174}
175
176#[derive(Clone, Debug)]
182pub struct IsConstant;
183
184impl IsConstant {
185 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
217pub struct IsConstantPartial {
219 is_constant: bool,
220 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 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 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 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 if !all_valid && !all_invalid {
389 partial.is_constant = false;
390 return Ok(());
391 }
392
393 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 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 #[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 #[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 #[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 #[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 #[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 vec![1i32, 2, 1, 2, 1, 2],
635 vec![0u32, 2, 4, 6],
636 true
637 )]
638 #[case(
639 vec![1i32, 2, 3, 4, 5],
641 vec![0u32, 2, 3, 5],
642 false
643 )]
644 #[case(
645 vec![1i32, 2, 3, 4],
647 vec![0u32, 2, 4],
648 false
649 )]
650 #[case(
651 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 assert!(is_constant(&outer_list.into_array(), &mut ctx)?);
688 Ok(())
689 }
690
691 #[rstest]
692 #[case(
693 [1i32, 2].repeat(100),
695 (0..101).map(|i| (i * 2) as u32).collect(),
696 true
697 )]
698 #[case(
699 {
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 {
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}