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 one elements.
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    match array.len() {
63        // 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.
64        0 => return Ok(false),
65        // Array of length 1 is always constant.
66        1 => return Ok(true),
67        _ => {}
68    }
69
70    // Constant and null arrays are always constant
71    if array.as_opt::<ConstantArray>().is_some() || array.as_opt::<NullArray>().is_some() {
72        return Ok(true);
73    }
74
75    // We try and rely on some easy to get stats
76    if let Some(Precision::Exact(value)) = array.statistics().get_as::<bool>(Stat::IsConstant) {
77        return Ok(value);
78    }
79
80    let all_invalid = array.all_invalid()?;
81    if all_invalid {
82        return Ok(true);
83    }
84
85    let all_valid = array.all_valid()?;
86
87    // If we have some nulls, array can't be constant
88    if !all_valid && !all_invalid {
89        return Ok(false);
90    }
91
92    // We already know here that the array is all valid, so we check for min/max stats.
93    let min = array
94        .statistics()
95        .get_scalar(Stat::Min, array.dtype())
96        .and_then(|p| p.as_exact());
97    let max = array
98        .statistics()
99        .get_scalar(Stat::Max, array.dtype())
100        .and_then(|p| p.as_exact());
101
102    if let Some((min, max)) = min.zip(max) {
103        if min == max {
104            return Ok(true);
105        }
106    }
107
108    debug_assert!(
109        all_valid,
110        "All values must be valid as an invariant of the VTable."
111    );
112    let is_constant = if let Some(vtable_fn) = array.vtable().is_constant_fn() {
113        vtable_fn.is_constant(array, opts)?
114    } else {
115        log::debug!(
116            "No is_constant implementation found for {}",
117            array.encoding()
118        );
119
120        if opts.canonicalize {
121            let array = array.to_canonical()?;
122
123            if let Some(is_constant_fn) = array.as_ref().vtable().is_constant_fn() {
124                is_constant_fn.is_constant(array.as_ref(), opts)?
125            } else {
126                vortex_bail!(
127                    "No is_constant function for canonical array: {}",
128                    array.as_ref().encoding(),
129                )
130            }
131        } else {
132            None
133        }
134    };
135
136    if let Some(is_constant) = is_constant {
137        array
138            .statistics()
139            .set(Stat::IsConstant, Precision::Exact(is_constant.into()));
140    }
141
142    Ok(is_constant.unwrap_or_default())
143}