• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vortex-data / vortex / 16728097825

04 Aug 2025 04:00PM UTC coverage: 48.355% (-35.1%) from 83.429%
16728097825

Pull #4108

github

web-flow
Merge 1b2d27fd8 into 649ba9576
Pull Request #4108: perf[vortex-array]: use all_valid instead of `invalid_count() == 0`

1 of 1 new or added line in 1 file covered. (100.0%)

11596 existing lines in 378 files now uncovered.

18635 of 38538 relevant lines covered (48.35%)

151786.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.73
/vortex-array/src/compute/is_constant.rs
1
// SPDX-License-Identifier: Apache-2.0
2
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3

4
use std::any::Any;
5
use std::sync::LazyLock;
6

7
use arcref::ArcRef;
8
use vortex_dtype::{DType, Nullability};
9
use vortex_error::{VortexError, VortexResult, vortex_bail, vortex_err};
10
use vortex_scalar::Scalar;
11

12
use crate::Array;
13
use crate::arrays::{ConstantVTable, NullVTable};
14
use crate::compute::{ComputeFn, ComputeFnVTable, InvocationArgs, Kernel, Options, Output};
15
use crate::stats::{Precision, Stat, StatsProviderExt};
16
use crate::vtable::VTable;
17

18
static IS_CONSTANT_FN: LazyLock<ComputeFn> = LazyLock::new(|| {
2✔
19
    let compute = ComputeFn::new("is_constant".into(), ArcRef::new_ref(&IsConstant));
2✔
20
    for kernel in inventory::iter::<IsConstantKernelRef> {
32✔
21
        compute.register_kernel(kernel.0.clone());
30✔
22
    }
30✔
23
    compute
2✔
24
});
2✔
25

26
/// Computes whether an array has constant values. If the array's encoding doesn't implement the
27
/// relevant VTable, it'll try and canonicalize in order to make a determination.
28
///
29
/// An array is constant IFF at least one of the following conditions apply:
30
/// 1. It has at least one element (**Note** - an empty array isn't constant).
31
/// 1. It's encoded as a [`crate::arrays::ConstantArray`] or [`crate::arrays::NullArray`]
32
/// 1. Has an exact statistic attached to it, saying its constant.
33
/// 1. Is all invalid.
34
/// 1. Is all valid AND has minimum and maximum statistics that are equal.
35
///
36
/// If the array has some null values but is not all null, it'll never be constant.
37
///
38
/// Returns `Ok(None)` if we could not determine whether the array is constant, e.g. if
39
/// canonicalization is disabled and the no kernel exists for the array's encoding.
40
pub fn is_constant(array: &dyn Array) -> VortexResult<Option<bool>> {
788✔
41
    let opts = IsConstantOpts::default();
788✔
42
    is_constant_opts(array, &opts)
788✔
43
}
788✔
44

45
/// Computes whether an array has constant values. Configurable by [`IsConstantOpts`].
46
///
47
/// Please see [`is_constant`] for a more detailed explanation of its behavior.
48
pub fn is_constant_opts(array: &dyn Array, options: &IsConstantOpts) -> VortexResult<Option<bool>> {
72,342✔
49
    let result = IS_CONSTANT_FN
72,342✔
50
        .invoke(&InvocationArgs {
72,342✔
51
            inputs: &[array.into()],
72,342✔
52
            options,
72,342✔
53
        })?
72,342✔
54
        .unwrap_scalar()?
72,342✔
55
        .as_bool()
72,342✔
56
        .value();
72,342✔
57

58
    Ok(result)
72,342✔
59
}
72,342✔
60

61
struct IsConstant;
62

63
impl ComputeFnVTable for IsConstant {
64
    fn invoke(
72,342✔
65
        &self,
72,342✔
66
        args: &InvocationArgs,
72,342✔
67
        kernels: &[ArcRef<dyn Kernel>],
72,342✔
68
    ) -> VortexResult<Output> {
72,342✔
69
        let IsConstantArgs { array, options } = IsConstantArgs::try_from(args)?;
72,342✔
70

71
        // We try and rely on some easy-to-get stats
72
        if let Some(Precision::Exact(value)) = array.statistics().get_as::<bool>(Stat::IsConstant) {
72,342✔
73
            return Ok(Scalar::from(Some(value)).into());
47,256✔
74
        }
25,086✔
75

76
        let value = is_constant_impl(array, options, kernels)?;
25,086✔
77

78
        if options.cost == Cost::Canonicalize {
25,086✔
79
            // When we run linear canonicalize, there we must always return an exact answer.
80
            assert!(
1,086✔
81
                value.is_some(),
1,086✔
82
                "is constant in array {array} canonicalize returned None"
×
83
            );
84
        }
24,000✔
85

86
        // Only if we made a determination do we update the stats.
87
        if let Some(value) = value {
25,086✔
88
            array
24,498✔
89
                .statistics()
24,498✔
90
                .set(Stat::IsConstant, Precision::Exact(value.into()));
24,498✔
91
        }
24,498✔
92

93
        Ok(Scalar::from(value).into())
25,086✔
94
    }
72,342✔
95

96
    fn return_dtype(&self, _args: &InvocationArgs) -> VortexResult<DType> {
72,342✔
97
        // We always return a nullable boolean where `null` indicates we couldn't determine
98
        // whether the array is constant.
99
        Ok(DType::Bool(Nullability::Nullable))
72,342✔
100
    }
72,342✔
101

102
    fn return_len(&self, _args: &InvocationArgs) -> VortexResult<usize> {
72,342✔
103
        Ok(1)
72,342✔
104
    }
72,342✔
105

106
    fn is_elementwise(&self) -> bool {
72,342✔
107
        false
72,342✔
108
    }
72,342✔
109
}
110

111
fn is_constant_impl(
25,086✔
112
    array: &dyn Array,
25,086✔
113
    options: &IsConstantOpts,
25,086✔
114
    kernels: &[ArcRef<dyn Kernel>],
25,086✔
115
) -> VortexResult<Option<bool>> {
25,086✔
116
    match array.len() {
25,086✔
117
        // 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.
118
        0 => return Ok(Some(false)),
×
119
        // Array of length 1 is always constant.
120
        1 => return Ok(Some(true)),
4,592✔
121
        _ => {}
20,494✔
122
    }
123

124
    // Constant and null arrays are always constant
125
    if array.is::<ConstantVTable>() || array.is::<NullVTable>() {
20,494✔
126
        return Ok(Some(true));
7,396✔
127
    }
13,098✔
128

129
    let all_invalid = array.all_invalid()?;
13,098✔
130
    if all_invalid {
13,098✔
UNCOV
131
        return Ok(Some(true));
×
132
    }
13,098✔
133

134
    let all_valid = array.all_valid()?;
13,098✔
135

136
    // If we have some nulls, array can't be constant
137
    if !all_valid && !all_invalid {
13,098✔
UNCOV
138
        return Ok(Some(false));
×
139
    }
13,098✔
140

141
    // We already know here that the array is all valid, so we check for min/max stats.
142
    let min = array.statistics().get_scalar(Stat::Min, array.dtype());
13,098✔
143
    let max = array.statistics().get_scalar(Stat::Max, array.dtype());
13,098✔
144

145
    if let Some((min, max)) = min.zip(max) {
13,098✔
146
        // min/max are equal and exact and there are no NaNs
147
        if min.is_exact()
320✔
148
            && min == max
320✔
149
            && (Stat::NaNCount.dtype(array.dtype()).is_none()
192✔
UNCOV
150
                || array.statistics().get_as::<u64>(Stat::NaNCount) == Some(Precision::exact(0u64)))
×
151
        {
152
            return Ok(Some(true));
192✔
153
        }
128✔
154
    }
12,778✔
155

156
    assert!(
12,906✔
157
        all_valid,
12,906✔
158
        "All values must be valid as an invariant of the VTable."
×
159
    );
160
    let args = InvocationArgs {
12,906✔
161
        inputs: &[array.into()],
12,906✔
162
        options,
12,906✔
163
    };
12,906✔
164
    for kernel in kernels {
118,784✔
165
        if let Some(output) = kernel.invoke(&args)? {
118,196✔
166
            return Ok(output.unwrap_scalar()?.as_bool().value());
12,318✔
167
        }
105,878✔
168
    }
169
    if let Some(output) = array.invoke(&IS_CONSTANT_FN, &args)? {
588✔
170
        return Ok(output.unwrap_scalar()?.as_bool().value());
×
171
    }
588✔
172

173
    log::debug!(
588✔
174
        "No is_constant implementation found for {}",
×
175
        array.encoding_id()
×
176
    );
177

178
    if options.cost == Cost::Canonicalize && !array.is_canonical() {
588✔
UNCOV
179
        let array = array.to_canonical()?;
×
UNCOV
180
        let is_constant = is_constant_opts(array.as_ref(), options)?;
×
UNCOV
181
        return Ok(is_constant);
×
182
    }
588✔
183

184
    // Otherwise, we cannot determine if the array is constant.
185
    Ok(None)
588✔
186
}
25,086✔
187

188
pub struct IsConstantKernelRef(ArcRef<dyn Kernel>);
189
inventory::collect!(IsConstantKernelRef);
190

191
pub trait IsConstantKernel: VTable {
192
    /// # Preconditions
193
    ///
194
    /// * All values are valid
195
    /// * array.len() > 1
196
    ///
197
    /// Returns `Ok(None)` to signal we couldn't make an exact determination.
198
    fn is_constant(&self, array: &Self::Array, opts: &IsConstantOpts)
199
    -> VortexResult<Option<bool>>;
200
}
201

202
#[derive(Debug)]
203
pub struct IsConstantKernelAdapter<V: VTable>(pub V);
204

205
impl<V: VTable + IsConstantKernel> IsConstantKernelAdapter<V> {
206
    pub const fn lift(&'static self) -> IsConstantKernelRef {
×
207
        IsConstantKernelRef(ArcRef::new_ref(self))
×
208
    }
×
209
}
210

211
impl<V: VTable + IsConstantKernel> Kernel for IsConstantKernelAdapter<V> {
212
    fn invoke(&self, args: &InvocationArgs) -> VortexResult<Option<Output>> {
118,196✔
213
        let args = IsConstantArgs::try_from(args)?;
118,196✔
214
        let Some(array) = args.array.as_opt::<V>() else {
118,196✔
215
            return Ok(None);
105,878✔
216
        };
217
        let is_constant = V::is_constant(&self.0, array, args.options)?;
12,318✔
218
        Ok(Some(Scalar::from(is_constant).into()))
12,318✔
219
    }
118,196✔
220
}
221

222
struct IsConstantArgs<'a> {
223
    array: &'a dyn Array,
224
    options: &'a IsConstantOpts,
225
}
226

227
impl<'a> TryFrom<&InvocationArgs<'a>> for IsConstantArgs<'a> {
228
    type Error = VortexError;
229

230
    fn try_from(value: &InvocationArgs<'a>) -> Result<Self, Self::Error> {
190,538✔
231
        if value.inputs.len() != 1 {
190,538✔
232
            vortex_bail!("Expected 1 input, found {}", value.inputs.len());
×
233
        }
190,538✔
234
        let array = value.inputs[0]
190,538✔
235
            .array()
190,538✔
236
            .ok_or_else(|| vortex_err!("Expected input 0 to be an array"))?;
190,538✔
237
        let options = value
190,538✔
238
            .options
190,538✔
239
            .as_any()
190,538✔
240
            .downcast_ref::<IsConstantOpts>()
190,538✔
241
            .ok_or_else(|| vortex_err!("Expected options to be of type IsConstantOpts"))?;
190,538✔
242
        Ok(Self { array, options })
190,538✔
243
    }
190,538✔
244
}
245

246
/// When calling `is_constant` the children are all checked for constantness.
247
/// This enum decide at each precision/cost level the constant check should run as.
248
/// The cost increase as we move down the list.
249
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
250
pub enum Cost {
251
    /// Only apply constant time computation to estimate constantness.
252
    Negligible,
253
    /// Allow the encoding to do a linear amount of work to determine is constant.
254
    /// Each encoding should implement short-circuiting make the common case runtime well below
255
    /// a linear scan.
256
    Specialized,
257
    /// Same as linear, but when necessary canonicalize the array and check is constant.
258
    /// This *must* always return a known answer.
259
    Canonicalize,
260
}
261

262
/// Configuration for [`is_constant_opts`] operations.
263
#[derive(Clone, Debug)]
264
pub struct IsConstantOpts {
265
    /// What precision cost trade off should be used
266
    pub cost: Cost,
267
}
268

269
impl Default for IsConstantOpts {
270
    fn default() -> Self {
788✔
271
        Self {
788✔
272
            cost: Cost::Canonicalize,
788✔
273
        }
788✔
274
    }
788✔
275
}
276

277
impl Options for IsConstantOpts {
278
    fn as_any(&self) -> &dyn Any {
190,538✔
279
        self
190,538✔
280
    }
190,538✔
281
}
282

283
impl IsConstantOpts {
284
    pub fn is_negligible_cost(&self) -> bool {
8,212✔
285
        self.cost == Cost::Negligible
8,212✔
286
    }
8,212✔
287
}
288

289
#[cfg(test)]
290
mod tests {
291
    use crate::arrays::PrimitiveArray;
292
    use crate::stats::Stat;
293

294
    #[test]
295
    fn is_constant_min_max_no_nan() {
296
        let arr = PrimitiveArray::from_iter([0, 1]);
297
        arr.statistics()
298
            .compute_all(&[Stat::Min, Stat::Max])
299
            .unwrap();
300
        assert!(!arr.is_constant());
301

302
        let arr = PrimitiveArray::from_iter([0, 0]);
303
        arr.statistics()
304
            .compute_all(&[Stat::Min, Stat::Max])
305
            .unwrap();
306
        assert!(arr.is_constant());
307

308
        let arr = PrimitiveArray::from_option_iter([Some(0), Some(0)]);
309
        assert!(arr.is_constant());
310
    }
311

312
    #[test]
313
    fn is_constant_min_max_with_nan() {
314
        let arr = PrimitiveArray::from_iter([0.0, 0.0, f32::NAN]);
315
        arr.statistics()
316
            .compute_all(&[Stat::Min, Stat::Max])
317
            .unwrap();
318
        assert!(!arr.is_constant());
319

320
        let arr =
321
            PrimitiveArray::from_option_iter([Some(f32::NEG_INFINITY), Some(f32::NEG_INFINITY)]);
322
        arr.statistics()
323
            .compute_all(&[Stat::Min, Stat::Max])
324
            .unwrap();
325
        assert!(arr.is_constant());
326
    }
327
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc