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

vortex-data / vortex / 17075133033

19 Aug 2025 04:01PM UTC coverage: 87.949% (+0.09%) from 87.856%
17075133033

push

github

web-flow
feat: ArrayOperations infallible, eager validation + new_unchecked (#4177)

ArrayOperations currently return VortexResult<>, but they really should
just be infallible. A failed array op is generally indicative of
programmer or encoding error. There's really nothing interesting we can
do to handle an out-of-bounds slice() or scalar_at.

There's a lot that falls out of this, like fixing a bunch of tests,
tweaking our scalar value casting to return Option instead of Result,
etc.

---------

Signed-off-by: Andrew Duffy <andrew@a10y.dev>

1744 of 1985 new or added lines in 195 files covered. (87.86%)

36 existing lines in 27 files now uncovered.

56745 of 64520 relevant lines covered (87.95%)

624082.56 hits per line

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

90.83
/vortex-array/src/compute/is_sorted.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, VortexExpect, 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_SORTED_FN: LazyLock<ComputeFn> = LazyLock::new(|| {
5,972✔
19
    let compute = ComputeFn::new("is_sorted".into(), ArcRef::new_ref(&IsSorted));
5,972✔
20
    for kernel in inventory::iter::<IsSortedKernelRef> {
58,672✔
21
        compute.register_kernel(kernel.0.clone());
52,700✔
22
    }
52,700✔
23
    compute
5,972✔
24
});
5,972✔
25

26
pub fn is_sorted(array: &dyn Array) -> VortexResult<bool> {
8,391✔
27
    is_sorted_opts(array, false)
8,391✔
28
}
8,391✔
29

30
pub fn is_strict_sorted(array: &dyn Array) -> VortexResult<bool> {
8,243✔
31
    is_sorted_opts(array, true)
8,243✔
32
}
8,243✔
33

34
pub fn is_sorted_opts(array: &dyn Array, strict: bool) -> VortexResult<bool> {
16,634✔
35
    Ok(IS_SORTED_FN
16,634✔
36
        .invoke(&InvocationArgs {
16,634✔
37
            inputs: &[array.into()],
16,634✔
38
            options: &IsSortedOptions { strict },
16,634✔
39
        })?
16,634✔
40
        .unwrap_scalar()?
16,634✔
41
        .as_bool()
16,634✔
42
        .value()
16,634✔
43
        .vortex_expect("non-nullable"))
16,634✔
44
}
16,634✔
45

46
struct IsSorted;
47
impl ComputeFnVTable for IsSorted {
48
    fn invoke(
16,634✔
49
        &self,
16,634✔
50
        args: &InvocationArgs,
16,634✔
51
        kernels: &[ArcRef<dyn Kernel>],
16,634✔
52
    ) -> VortexResult<Output> {
16,634✔
53
        let IsSortedArgs { array, strict } = IsSortedArgs::try_from(args)?;
16,634✔
54

55
        // We currently don't support sorting struct arrays.
56
        if array.dtype().is_struct() {
16,634✔
57
            return Ok(Scalar::from(false).into());
5,912✔
58
        }
10,722✔
59

60
        let is_sorted = if strict {
10,722✔
61
            if let Some(Precision::Exact(value)) =
×
62
                array.statistics().get_as::<bool>(Stat::IsStrictSorted)
5,287✔
63
            {
64
                return Ok(Scalar::from(value).into());
×
65
            }
5,287✔
66

67
            let is_strict_sorted = is_sorted_impl(array, kernels, true)?;
5,287✔
68
            let array_stats = array.statistics();
5,287✔
69

70
            if is_strict_sorted {
5,287✔
71
                array_stats.set(Stat::IsSorted, Precision::Exact(true.into()));
5,219✔
72
                array_stats.set(Stat::IsStrictSorted, Precision::Exact(true.into()));
5,219✔
73
            } else {
5,219✔
74
                array_stats.set(Stat::IsStrictSorted, Precision::Exact(false.into()));
68✔
75
            }
68✔
76

77
            is_strict_sorted
5,287✔
78
        } else {
79
            if let Some(Precision::Exact(value)) = array.statistics().get_as::<bool>(Stat::IsSorted)
5,435✔
80
            {
81
                return Ok(Scalar::from(value).into());
×
82
            }
5,435✔
83

84
            let is_sorted = is_sorted_impl(array, kernels, false)?;
5,435✔
85
            let array_stats = array.statistics();
5,435✔
86

87
            if is_sorted {
5,435✔
88
                array_stats.set(Stat::IsSorted, Precision::Exact(true.into()));
2,765✔
89
            } else {
3,467✔
90
                array_stats.set(Stat::IsSorted, Precision::Exact(false.into()));
2,670✔
91
                array_stats.set(Stat::IsStrictSorted, Precision::Exact(false.into()));
2,670✔
92
            }
2,670✔
93

94
            is_sorted
5,435✔
95
        };
96

97
        Ok(Scalar::from(is_sorted).into())
10,722✔
98
    }
16,634✔
99

100
    fn return_dtype(&self, _args: &InvocationArgs) -> VortexResult<DType> {
16,634✔
101
        Ok(DType::Bool(Nullability::NonNullable))
16,634✔
102
    }
16,634✔
103

104
    fn return_len(&self, _args: &InvocationArgs) -> VortexResult<usize> {
16,634✔
105
        Ok(1)
16,634✔
106
    }
16,634✔
107

108
    fn is_elementwise(&self) -> bool {
16,634✔
109
        true
16,634✔
110
    }
16,634✔
111
}
112

113
struct IsSortedArgs<'a> {
114
    array: &'a dyn Array,
115
    strict: bool,
116
}
117

118
impl<'a> TryFrom<&InvocationArgs<'a>> for IsSortedArgs<'a> {
119
    type Error = VortexError;
120

121
    fn try_from(value: &InvocationArgs<'a>) -> Result<Self, Self::Error> {
55,665✔
122
        if value.inputs.len() != 1 {
55,665✔
123
            vortex_bail!(
×
124
                "IsSorted function requires exactly one argument, got {}",
×
125
                value.inputs.len()
×
126
            );
127
        }
55,665✔
128
        let array = value.inputs[0]
55,665✔
129
            .array()
55,665✔
130
            .ok_or_else(|| vortex_err!("Invalid argument type for is sorted function"))?;
55,665✔
131
        let options = *value
55,665✔
132
            .options
55,665✔
133
            .as_any()
55,665✔
134
            .downcast_ref::<IsSortedOptions>()
55,665✔
135
            .ok_or_else(|| vortex_err!("Invalid options type for is sorted function"))?;
55,665✔
136

137
        Ok(IsSortedArgs {
55,665✔
138
            array,
55,665✔
139
            strict: options.strict,
55,665✔
140
        })
55,665✔
141
    }
55,665✔
142
}
143

144
#[derive(Clone, Copy)]
145
struct IsSortedOptions {
146
    strict: bool,
147
}
148

149
impl Options for IsSortedOptions {
150
    fn as_any(&self) -> &dyn Any {
55,665✔
151
        self
55,665✔
152
    }
55,665✔
153
}
154

155
pub struct IsSortedKernelRef(ArcRef<dyn Kernel>);
156
inventory::collect!(IsSortedKernelRef);
157

158
#[derive(Debug)]
159
pub struct IsSortedKernelAdapter<V: VTable>(pub V);
160

161
impl<V: VTable + IsSortedKernel> IsSortedKernelAdapter<V> {
162
    pub const fn lift(&'static self) -> IsSortedKernelRef {
×
163
        IsSortedKernelRef(ArcRef::new_ref(self))
×
164
    }
×
165
}
166

167
impl<V: VTable + IsSortedKernel> Kernel for IsSortedKernelAdapter<V> {
168
    fn invoke(&self, args: &InvocationArgs) -> VortexResult<Option<Output>> {
39,001✔
169
        let IsSortedArgs { array, strict } = IsSortedArgs::try_from(args)?;
39,001✔
170
        let Some(array) = array.as_opt::<V>() else {
39,001✔
171
            return Ok(None);
29,259✔
172
        };
173

174
        let is_sorted = if strict {
9,742✔
175
            V::is_strict_sorted(&self.0, array)?
4,625✔
176
        } else {
177
            V::is_sorted(&self.0, array)?
5,117✔
178
        };
179

180
        Ok(Some(Scalar::from(is_sorted).into()))
9,742✔
181
    }
39,001✔
182
}
183

184
pub trait IsSortedKernel: VTable {
185
    /// # Preconditions
186
    /// - The array's length is > 1.
187
    /// - The array is not encoded as `NullArray` or `ConstantArray`.
188
    /// - If doing a `strict` check, if the array is nullable, it'll have at most 1 null element
189
    ///   as the first item in the array.
190
    fn is_sorted(&self, array: &Self::Array) -> VortexResult<bool>;
191

192
    fn is_strict_sorted(&self, array: &Self::Array) -> VortexResult<bool>;
193
}
194

195
#[allow(clippy::wrong_self_convention)]
196
/// Helper methods to check sortedness with strictness
197
pub trait IsSortedIteratorExt: Iterator
198
where
199
    <Self as Iterator>::Item: PartialOrd,
200
{
201
    fn is_strict_sorted(self) -> bool
4,625✔
202
    where
4,625✔
203
        Self: Sized,
4,625✔
204
        Self::Item: PartialOrd,
4,625✔
205
    {
206
        self.is_sorted_by(|a, b| a < b)
1,043,603✔
207
    }
4,625✔
208
}
209

210
impl<T> IsSortedIteratorExt for T
211
where
212
    T: Iterator + ?Sized,
213
    T::Item: PartialOrd,
214
{
215
}
216

217
fn is_sorted_impl(
10,722✔
218
    array: &dyn Array,
10,722✔
219
    kernels: &[ArcRef<dyn Kernel>],
10,722✔
220
    strict: bool,
10,722✔
221
) -> VortexResult<bool> {
10,722✔
222
    // Arrays with 0 or 1 elements are strict sorted.
223
    if array.len() <= 1 {
10,722✔
224
        return Ok(true);
934✔
225
    }
9,788✔
226

227
    // Constant and null arrays are always sorted, but not strict sorted.
228
    if array.is::<ConstantVTable>() || array.is::<NullVTable>() {
9,788✔
229
        return Ok(!strict);
3✔
230
    }
9,785✔
231

232
    // Enforce strictness before we even try to check if the array is sorted.
233
    if strict {
9,785✔
234
        let invalid_count = array.invalid_count()?;
4,629✔
235
        match invalid_count {
4,629✔
236
            // We can keep going
237
            0 => {}
4,623✔
238
            // If we have a potential null value - it has to be the first one.
239
            1 => {
240
                if !array.is_invalid(0)? {
3✔
241
                    return Ok(false);
1✔
242
                }
2✔
243
            }
244
            _ => return Ok(false),
3✔
245
        }
246
    }
5,156✔
247

248
    let args = InvocationArgs {
9,781✔
249
        inputs: &[array.into()],
9,781✔
250
        options: &IsSortedOptions { strict },
9,781✔
251
    };
9,781✔
252

253
    for kernel in kernels {
39,070✔
254
        if let Some(output) = kernel.invoke(&args)? {
39,031✔
255
            return Ok(output
9,742✔
256
                .unwrap_scalar()?
9,742✔
257
                .as_bool()
9,742✔
258
                .value()
9,742✔
259
                .vortex_expect("non-nullable"));
9,742✔
260
        }
29,289✔
261
    }
262
    if let Some(output) = array.invoke(&IS_SORTED_FN, &args)? {
39✔
263
        return Ok(output
×
264
            .unwrap_scalar()?
×
265
            .as_bool()
×
266
            .value()
×
267
            .vortex_expect("non-nullable"));
×
268
    }
39✔
269

270
    if !array.is_canonical() {
39✔
271
        log::debug!(
39✔
272
            "No is_sorted implementation found for {}",
×
273
            array.encoding_id()
×
274
        );
275

276
        // Recurse to canonical implementation
277
        let array = array.to_canonical()?;
39✔
278

279
        return if strict {
39✔
UNCOV
280
            is_strict_sorted(array.as_ref())
×
281
        } else {
282
            is_sorted(array.as_ref())
39✔
283
        };
284
    }
×
285

286
    vortex_bail!(
×
287
        "No is_sorted function for canonical array: {}",
×
288
        array.encoding_id(),
×
289
    )
290
}
10,722✔
291

292
#[cfg(test)]
293
mod tests {
294
    use vortex_buffer::buffer;
295

296
    use crate::IntoArray;
297
    use crate::arrays::{BoolArray, PrimitiveArray};
298
    use crate::compute::{is_sorted, is_strict_sorted};
299
    use crate::validity::Validity;
300
    #[test]
301
    fn test_is_sorted() {
1✔
302
        assert!(
1✔
303
            is_sorted(PrimitiveArray::new(buffer!(0, 1, 2, 3), Validity::AllValid).as_ref())
1✔
304
                .unwrap()
1✔
305
        );
306
        assert!(
1✔
307
            is_sorted(
1✔
308
                PrimitiveArray::new(
1✔
309
                    buffer!(0, 1, 2, 3),
1✔
310
                    Validity::Array(BoolArray::from_iter([false, true, true, true]).into_array())
1✔
311
                )
1✔
312
                .as_ref()
1✔
313
            )
1✔
314
            .unwrap()
1✔
315
        );
316
        assert!(
1✔
317
            !is_sorted(
1✔
318
                PrimitiveArray::new(
1✔
319
                    buffer!(0, 1, 2, 3),
1✔
320
                    Validity::Array(BoolArray::from_iter([true, false, true, true]).into_array())
1✔
321
                )
1✔
322
                .as_ref()
1✔
323
            )
1✔
324
            .unwrap()
1✔
325
        );
326

327
        assert!(
1✔
328
            !is_sorted(PrimitiveArray::new(buffer!(0, 1, 3, 2), Validity::AllValid).as_ref())
1✔
329
                .unwrap()
1✔
330
        );
331
        assert!(
1✔
332
            !is_sorted(
1✔
333
                PrimitiveArray::new(
1✔
334
                    buffer!(0, 1, 3, 2),
1✔
335
                    Validity::Array(BoolArray::from_iter([false, true, true, true]).into_array()),
1✔
336
                )
1✔
337
                .as_ref()
1✔
338
            )
1✔
339
            .unwrap(),
1✔
340
        );
341
    }
1✔
342

343
    #[test]
344
    fn test_is_strict_sorted() {
1✔
345
        assert!(
1✔
346
            is_strict_sorted(PrimitiveArray::new(buffer!(0, 1, 2, 3), Validity::AllValid).as_ref())
1✔
347
                .unwrap()
1✔
348
        );
349
        assert!(
1✔
350
            is_strict_sorted(
1✔
351
                PrimitiveArray::new(
1✔
352
                    buffer!(0, 1, 2, 3),
1✔
353
                    Validity::Array(BoolArray::from_iter([false, true, true, true]).into_array())
1✔
354
                )
1✔
355
                .as_ref()
1✔
356
            )
1✔
357
            .unwrap()
1✔
358
        );
359
        assert!(
1✔
360
            !is_strict_sorted(
1✔
361
                PrimitiveArray::new(
1✔
362
                    buffer!(0, 1, 2, 3),
1✔
363
                    Validity::Array(BoolArray::from_iter([true, false, true, true]).into_array()),
1✔
364
                )
1✔
365
                .as_ref()
1✔
366
            )
1✔
367
            .unwrap(),
1✔
368
        );
369

370
        assert!(
1✔
371
            !is_strict_sorted(
1✔
372
                PrimitiveArray::new(
1✔
373
                    buffer!(0, 1, 3, 2),
1✔
374
                    Validity::Array(BoolArray::from_iter([false, true, true, true]).into_array()),
1✔
375
                )
1✔
376
                .as_ref()
1✔
377
            )
1✔
378
            .unwrap(),
1✔
379
        );
380
    }
1✔
381
}
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