vortex_array/compute/
is_constant.rs

1use vortex_error::{VortexExpect as _, VortexResult, vortex_bail};
2
3use crate::arrays::{ConstantArray, NullArray};
4use crate::stats::{Precision, Stat, StatsProviderExt};
5use crate::{Array, ArrayExt, Encoding};
6
7pub trait IsConstantFn<A> {
8    /// # Preconditions
9    ///
10    /// * All values are valid
11    /// * array.len() > 1
12    ///
13    /// Returns `Ok(None)` to signal we couldn't make an exact determination.
14    fn is_constant(&self, array: A, opts: &IsConstantOpts) -> VortexResult<Option<bool>>;
15}
16
17impl<E: Encoding> IsConstantFn<&dyn Array> for E
18where
19    E: for<'a> IsConstantFn<&'a E::Array>,
20{
21    fn is_constant(&self, array: &dyn Array, opts: &IsConstantOpts) -> VortexResult<Option<bool>> {
22        let array_ref = array
23            .as_any()
24            .downcast_ref::<E::Array>()
25            .vortex_expect("Failed to downcast array");
26        IsConstantFn::is_constant(self, array_ref, opts)
27    }
28}
29
30/// Configuration for [`is_constant_opts`] operations.
31#[derive(Clone)]
32pub struct IsConstantOpts {
33    /// Should the operation make an effort to canonicalize the target array if its encoding doesn't implement [`IsConstantFn`].
34    pub canonicalize: bool,
35}
36
37impl Default for IsConstantOpts {
38    fn default() -> Self {
39        Self { canonicalize: true }
40    }
41}
42
43/// Computes whether an array has constant values. If the array's encoding doesn't implement the relevant VTable, it'll try and canonicalize in order to make a determination.
44/// An array is constant IFF at least one of the following conditions apply:
45/// 1. It has at least one element (**Note** - an empty array isn't constant).
46/// 1. Its encoded as a [`ConstantArray`] or [`NullArray`]
47/// 1. Has an exact statistic attached to it, saying its constant.
48/// 1. Is all invalid.
49/// 1. Is all valid AND has minimum and maximum statistics that are equal.
50///
51/// If the array has some null values but is not all null, it'll never be constant.
52/// **Please note:** Might return false negatives if a specific encoding couldn't make a determination.
53pub fn is_constant(array: &dyn Array) -> VortexResult<bool> {
54    let opts = IsConstantOpts::default();
55    is_constant_opts(array, &opts)
56}
57
58/// Computes whether an array has constant values. Configurable by [`IsConstantOpts`].
59///
60/// Please see [`is_constant`] for a more detailed explanation of its behavior.
61pub fn is_constant_opts(array: &dyn Array, opts: &IsConstantOpts) -> VortexResult<bool> {
62    // We try and rely on some easy to get stats
63    if let Some(Precision::Exact(value)) = array.statistics().get_as::<bool>(Stat::IsConstant) {
64        return Ok(value);
65    }
66
67    let is_constant = is_constant_impl(array, opts)?;
68
69    if let Some(is_constant) = is_constant {
70        array
71            .statistics()
72            .set(Stat::IsConstant, Precision::Exact(is_constant.into()));
73    }
74
75    Ok(is_constant.unwrap_or_default())
76}
77
78fn is_constant_impl(array: &dyn Array, opts: &IsConstantOpts) -> VortexResult<Option<bool>> {
79    match array.len() {
80        // Our current semantics are that we can always get a value out of a constant array. We might want to change that in the future.
81        0 => return Ok(Some(false)),
82        // Array of length 1 is always constant.
83        1 => return Ok(Some(true)),
84        _ => {}
85    }
86
87    // Constant and null arrays are always constant
88    if array.as_opt::<ConstantArray>().is_some() || array.as_opt::<NullArray>().is_some() {
89        return Ok(Some(true));
90    }
91
92    let all_invalid = array.all_invalid()?;
93    if all_invalid {
94        return Ok(Some(true));
95    }
96
97    let all_valid = array.all_valid()?;
98
99    // If we have some nulls, array can't be constant
100    if !all_valid && !all_invalid {
101        return Ok(Some(false));
102    }
103
104    // We already know here that the array is all valid, so we check for min/max stats.
105    let min = array
106        .statistics()
107        .get_scalar(Stat::Min, array.dtype())
108        .and_then(|p| p.as_exact());
109    let max = array
110        .statistics()
111        .get_scalar(Stat::Max, array.dtype())
112        .and_then(|p| p.as_exact());
113
114    if let Some((min, max)) = min.zip(max) {
115        if min == max {
116            return Ok(Some(true));
117        }
118    }
119
120    assert!(
121        all_valid,
122        "All values must be valid as an invariant of the VTable."
123    );
124    let is_constant = if let Some(vtable_fn) = array.vtable().is_constant_fn() {
125        vtable_fn.is_constant(array, opts)?
126    } else {
127        log::debug!(
128            "No is_constant implementation found for {}",
129            array.encoding()
130        );
131
132        if opts.canonicalize {
133            let array = array.to_canonical()?;
134
135            if let Some(is_constant_fn) = array.as_ref().vtable().is_constant_fn() {
136                is_constant_fn.is_constant(array.as_ref(), opts)?
137            } else {
138                vortex_bail!(
139                    "No is_constant function for canonical array: {}",
140                    array.as_ref().encoding(),
141                )
142            }
143        } else {
144            None
145        }
146    };
147
148    Ok(is_constant)
149}