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

vortex-data / vortex / 16199940901

10 Jul 2025 03:54PM UTC coverage: 81.084% (+2.9%) from 78.188%
16199940901

Pull #3822

github

web-flow
Merge ec099962a into 325722ee9
Pull Request #3822: chore: TPC-H CI/coverage improvements

45627 of 56271 relevant lines covered (81.08%)

145002.85 hits per line

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

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

37
/// Computes whether an array has constant values. Configurable by [`IsConstantOpts`].
38
///
39
/// Please see [`is_constant`] for a more detailed explanation of its behavior.
40
pub fn is_constant_opts(array: &dyn Array, options: &IsConstantOpts) -> VortexResult<Option<bool>> {
200,141✔
41
    let result = IS_CONSTANT_FN
200,141✔
42
        .invoke(&InvocationArgs {
200,141✔
43
            inputs: &[array.into()],
200,141✔
44
            options,
200,141✔
45
        })?
200,141✔
46
        .unwrap_scalar()?
200,141✔
47
        .as_bool()
200,141✔
48
        .value();
200,141✔
49

200,141✔
50
    Ok(result)
200,141✔
51
}
200,141✔
52

53
pub static IS_CONSTANT_FN: LazyLock<ComputeFn> = LazyLock::new(|| {
8,647✔
54
    let compute = ComputeFn::new("is_constant".into(), ArcRef::new_ref(&IsConstant));
8,647✔
55
    for kernel in inventory::iter::<IsConstantKernelRef> {
104,984✔
56
        compute.register_kernel(kernel.0.clone());
96,337✔
57
    }
96,337✔
58
    compute
8,647✔
59
});
8,647✔
60

61
struct IsConstant;
62

63
impl ComputeFnVTable for IsConstant {
64
    fn invoke(
200,141✔
65
        &self,
200,141✔
66
        args: &InvocationArgs,
200,141✔
67
        kernels: &[ArcRef<dyn Kernel>],
200,141✔
68
    ) -> VortexResult<Output> {
200,141✔
69
        let IsConstantArgs { array, options } = IsConstantArgs::try_from(args)?;
200,141✔
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) {
200,141✔
73
            return Ok(Scalar::from(Some(value)).into());
110,535✔
74
        }
89,606✔
75

76
        let value = is_constant_impl(array, options, kernels)?;
89,606✔
77

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

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

93
        Ok(Scalar::from(value).into())
89,606✔
94
    }
200,141✔
95

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

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

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

111
fn is_constant_impl(
89,606✔
112
    array: &dyn Array,
89,606✔
113
    options: &IsConstantOpts,
89,606✔
114
    kernels: &[ArcRef<dyn Kernel>],
89,606✔
115
) -> VortexResult<Option<bool>> {
89,606✔
116
    match array.len() {
89,606✔
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)),
9,739✔
121
        _ => {}
79,867✔
122
    }
79,867✔
123

79,867✔
124
    // Constant and null arrays are always constant
79,867✔
125
    if array.as_opt::<ConstantVTable>().is_some() || array.as_opt::<NullVTable>().is_some() {
79,867✔
126
        return Ok(Some(true));
2✔
127
    }
79,865✔
128

129
    let all_invalid = array.all_invalid()?;
79,865✔
130
    if all_invalid {
79,865✔
131
        return Ok(Some(true));
3✔
132
    }
79,862✔
133

134
    let all_valid = array.all_valid()?;
79,862✔
135

136
    // If we have some nulls, array can't be constant
137
    if !all_valid && !all_invalid {
79,862✔
138
        return Ok(Some(false));
2,397✔
139
    }
77,465✔
140

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

145
    if let Some((min, max)) = min.zip(max) {
77,465✔
146
        // min/max are equal and exact and there are no NaNs
147
        if min.is_exact()
25,594✔
148
            && min == max
25,358✔
149
            && (Stat::NaNCount.dtype(array.dtype()).is_none()
269✔
150
                || array.statistics().get_as::<u64>(Stat::NaNCount) == Some(Precision::exact(0u64)))
2✔
151
        {
152
            return Ok(Some(true));
267✔
153
        }
25,327✔
154
    }
51,871✔
155

156
    assert!(
77,198✔
157
        all_valid,
77,198✔
158
        "All values must be valid as an invariant of the VTable."
×
159
    );
160
    let args = InvocationArgs {
77,198✔
161
        inputs: &[array.into()],
77,198✔
162
        options,
77,198✔
163
    };
77,198✔
164
    for kernel in kernels {
479,024✔
165
        if let Some(output) = kernel.invoke(&args)? {
474,108✔
166
            return Ok(output.unwrap_scalar()?.as_bool().value());
72,282✔
167
        }
401,826✔
168
    }
169
    if let Some(output) = array.invoke(&IS_CONSTANT_FN, &args)? {
4,916✔
170
        return Ok(output.unwrap_scalar()?.as_bool().value());
×
171
    }
4,916✔
172

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

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

4,844✔
184
    // Otherwise, we cannot determine if the array is constant.
4,844✔
185
    Ok(None)
4,844✔
186
}
89,606✔
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>> {
467,915✔
213
        let args = IsConstantArgs::try_from(args)?;
467,915✔
214
        let Some(array) = args.array.as_opt::<V>() else {
467,915✔
215
            return Ok(None);
397,062✔
216
        };
217
        let is_constant = V::is_constant(&self.0, array, args.options)?;
70,853✔
218
        Ok(Some(Scalar::from(is_constant).into()))
70,853✔
219
    }
467,915✔
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> {
674,249✔
231
        if value.inputs.len() != 1 {
674,249✔
232
            vortex_bail!("Expected 1 input, found {}", value.inputs.len());
×
233
        }
674,249✔
234
        let array = value.inputs[0]
674,249✔
235
            .array()
674,249✔
236
            .ok_or_else(|| vortex_err!("Expected input 0 to be an array"))?;
674,249✔
237
        let options = value
674,249✔
238
            .options
674,249✔
239
            .as_any()
674,249✔
240
            .downcast_ref::<IsConstantOpts>()
674,249✔
241
            .ok_or_else(|| vortex_err!("Expected options to be of type IsConstantOpts"))?;
674,249✔
242
        Ok(Self { array, options })
674,249✔
243
    }
674,249✔
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 {
4,375✔
271
        Self {
4,375✔
272
            cost: Cost::Canonicalize,
4,375✔
273
        }
4,375✔
274
    }
4,375✔
275
}
276

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

283
impl IsConstantOpts {
284
    pub fn is_negligible_cost(&self) -> bool {
59,783✔
285
        self.cost == Cost::Negligible
59,783✔
286
    }
59,783✔
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() {
1✔
296
        let arr = PrimitiveArray::from_iter([0, 1]);
1✔
297
        arr.statistics()
1✔
298
            .compute_all(&[Stat::Min, Stat::Max])
1✔
299
            .unwrap();
1✔
300
        assert!(!arr.is_constant());
1✔
301

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

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

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

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

© 2026 Coveralls, Inc