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

vortex-data / vortex / 16198676642

10 Jul 2025 03:01PM UTC coverage: 78.263% (+0.006%) from 78.257%
16198676642

push

github

web-flow
Fix empty take for `ChunkedArray` (#3823)

Got a panic when running take 

<img width="1375" height="300" alt="image"
src="https://github.com/user-attachments/assets/96c31a56-461f-4f58-ad1d-029194b34523"
/>

---------

Signed-off-by: blaginin <dima@spiraldb.com>

25 of 25 new or added lines in 2 files covered. (100.0%)

2 existing lines in 2 files now uncovered.

44206 of 56484 relevant lines covered (78.26%)

53521.1 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>> {
3,613✔
33
    let opts = IsConstantOpts::default();
3,613✔
34
    is_constant_opts(array, &opts)
3,613✔
35
}
3,613✔
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>> {
124,937✔
41
    let result = IS_CONSTANT_FN
124,937✔
42
        .invoke(&InvocationArgs {
124,937✔
43
            inputs: &[array.into()],
124,937✔
44
            options,
124,937✔
45
        })?
124,937✔
46
        .unwrap_scalar()?
124,937✔
47
        .as_bool()
124,937✔
48
        .value();
124,937✔
49

124,937✔
50
    Ok(result)
124,937✔
51
}
124,937✔
52

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

61
struct IsConstant;
62

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

76
        let value = is_constant_impl(array, options, kernels)?;
53,776✔
77

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

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

93
        Ok(Scalar::from(value).into())
53,776✔
94
    }
124,937✔
95

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

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

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

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

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

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

134
    let all_valid = array.all_valid()?;
44,440✔
135

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

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

145
    if let Some((min, max)) = min.zip(max) {
42,043✔
146
        // min/max are equal and exact and there are no NaNs
147
        if min.is_exact()
16,024✔
148
            && min == max
16,024✔
149
            && (Stat::NaNCount.dtype(array.dtype()).is_none()
39✔
150
                || array.statistics().get_as::<u64>(Stat::NaNCount) == Some(Precision::exact(0u64)))
2✔
151
        {
152
            return Ok(Some(true));
37✔
153
        }
15,987✔
154
    }
26,019✔
155

156
    assert!(
42,006✔
157
        all_valid,
42,006✔
158
        "All values must be valid as an invariant of the VTable."
×
159
    );
160
    let args = InvocationArgs {
42,006✔
161
        inputs: &[array.into()],
42,006✔
162
        options,
42,006✔
163
    };
42,006✔
164
    for kernel in kernels {
203,792✔
165
        if let Some(output) = kernel.invoke(&args)? {
200,192✔
166
            return Ok(output.unwrap_scalar()?.as_bool().value());
38,406✔
167
        }
161,786✔
168
    }
169
    if let Some(output) = array.invoke(&IS_CONSTANT_FN, &args)? {
3,600✔
170
        return Ok(output.unwrap_scalar()?.as_bool().value());
×
171
    }
3,600✔
172

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

178
    if options.cost == Cost::Canonicalize && !array.is_canonical() {
3,600✔
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
    }
3,528✔
183

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

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

283
impl IsConstantOpts {
284
    pub fn is_negligible_cost(&self) -> bool {
36,597✔
285
        self.cost == Cost::Negligible
36,597✔
286
    }
36,597✔
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