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 =
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 #[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 #[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 #[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 #[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 #[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 vec![1i32, 2, 1, 2, 1, 2],
631 vec![0u32, 2, 4, 6],
632 true
633 )]
634 #[case(
635 vec![1i32, 2, 3, 4, 5],
637 vec![0u32, 2, 3, 5],
638 false
639 )]
640 #[case(
641 vec![1i32, 2, 3, 4],
643 vec![0u32, 2, 4],
644 false
645 )]
646 #[case(
647 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 assert!(is_constant(&outer_list.into_array(), &mut ctx)?);
684 Ok(())
685 }
686
687 #[rstest]
688 #[case(
689 [1i32, 2].repeat(100),
691 (0..101).map(|i| (i * 2) as u32).collect(),
692 true
693 )]
694 #[case(
695 {
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 {
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}