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

vortex-data / vortex / 16531106304

25 Jul 2025 08:34PM UTC coverage: 81.725% (-0.06%) from 81.789%
16531106304

Pull #3356

github

web-flow
Merge 41015f2b5 into 1819cd50a
Pull Request #3356: Clean up stats propagation for slicing

79 of 106 new or added lines in 12 files covered. (74.53%)

26 existing lines in 12 files now uncovered.

43164 of 52816 relevant lines covered (81.73%)

170491.59 hits per line

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

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

4
use std::sync::LazyLock;
5

6
use arcref::ArcRef;
7
use vortex_dtype::{DType, Nullability, StructFields};
8
use vortex_error::{VortexExpect, VortexResult, vortex_bail};
9
use vortex_scalar::Scalar;
10

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

17
/// The minimum and maximum non-null values of an array, or None if there are no non-null values.
18
///
19
/// The return value dtype is the non-nullable version of the array dtype.
20
///
21
/// This will update the stats set of this array (as a side effect).
22
pub fn min_max(array: &dyn Array) -> VortexResult<Option<MinMaxResult>> {
63,088✔
23
    let scalar = MIN_MAX_FN
63,088✔
24
        .invoke(&InvocationArgs {
63,088✔
25
            inputs: &[array.into()],
63,088✔
26
            options: &(),
63,088✔
27
        })?
63,088✔
28
        .unwrap_scalar()?;
63,088✔
29
    MinMaxResult::from_scalar(scalar)
63,088✔
30
}
63,088✔
31

32
#[derive(Debug, Clone, PartialEq, Eq)]
33
pub struct MinMaxResult {
34
    pub min: Scalar,
35
    pub max: Scalar,
36
}
37

38
impl MinMaxResult {
39
    pub fn from_scalar(scalar: Scalar) -> VortexResult<Option<Self>> {
123,261✔
40
        if scalar.is_null() {
123,261✔
41
            Ok(None)
10,998✔
42
        } else {
43
            let min = scalar
112,263✔
44
                .as_struct()
112,263✔
45
                .field_by_idx(0)
112,263✔
46
                .vortex_expect("missing min field");
112,263✔
47
            let max = scalar
112,263✔
48
                .as_struct()
112,263✔
49
                .field_by_idx(1)
112,263✔
50
                .vortex_expect("missing max field");
112,263✔
51
            Ok(Some(MinMaxResult { min, max }))
112,263✔
52
        }
53
    }
123,261✔
54
}
55

56
pub struct MinMax;
57

58
impl ComputeFnVTable for MinMax {
59
    fn invoke(
63,088✔
60
        &self,
63,088✔
61
        args: &InvocationArgs,
63,088✔
62
        kernels: &[ArcRef<dyn Kernel>],
63,088✔
63
    ) -> VortexResult<Output> {
63,088✔
64
        let UnaryArgs { array, .. } = UnaryArgs::<()>::try_from(args)?;
63,088✔
65

66
        let return_dtype = self.return_dtype(args)?;
63,088✔
67

68
        match min_max_impl(array, kernels)? {
63,088✔
69
            None => Ok(Scalar::null(return_dtype).into()),
5,722✔
70
            Some(MinMaxResult { min, max }) => {
57,366✔
71
                assert!(
57,366✔
72
                    min <= max,
57,366✔
73
                    "min > max: min={} max={} encoding={}",
×
74
                    min,
75
                    max,
76
                    array.encoding_id()
×
77
                );
78

79
                // Update the stats set with the computed min/max
80
                array
57,366✔
81
                    .statistics()
57,366✔
82
                    .set(Stat::Min, Precision::Exact(min.value().clone()));
57,366✔
83
                array
57,366✔
84
                    .statistics()
57,366✔
85
                    .set(Stat::Max, Precision::Exact(max.value().clone()));
57,366✔
86

87
                // Return the min/max as a struct scalar
88
                Ok(Scalar::struct_(return_dtype, vec![min, max]).into())
57,366✔
89
            }
90
        }
91
    }
63,088✔
92

93
    fn return_dtype(&self, args: &InvocationArgs) -> VortexResult<DType> {
126,176✔
94
        let UnaryArgs { array, .. } = UnaryArgs::<()>::try_from(args)?;
126,176✔
95

96
        // We return a min/max struct scalar, where the overall struct is nullable in the case
97
        // that the array is all null or empty.
98
        Ok(DType::Struct(
126,176✔
99
            StructFields::new(
126,176✔
100
                ["min", "max"].into(),
126,176✔
101
                vec![array.dtype().clone(), array.dtype().clone()],
126,176✔
102
            ),
126,176✔
103
            Nullability::Nullable,
126,176✔
104
        ))
126,176✔
105
    }
126,176✔
106

107
    fn return_len(&self, _args: &InvocationArgs) -> VortexResult<usize> {
63,088✔
108
        Ok(1)
63,088✔
109
    }
63,088✔
110

111
    fn is_elementwise(&self) -> bool {
63,088✔
112
        false
63,088✔
113
    }
63,088✔
114
}
115

116
fn min_max_impl(
63,088✔
117
    array: &dyn Array,
63,088✔
118
    kernels: &[ArcRef<dyn Kernel>],
63,088✔
119
) -> VortexResult<Option<MinMaxResult>> {
63,088✔
120
    if array.is_empty() || array.valid_count()? == 0 {
63,088✔
121
        return Ok(None);
446✔
122
    }
62,642✔
123

124
    if let Some(array) = array.as_opt::<ConstantVTable>() {
62,642✔
125
        if !array.scalar().is_null() {
1,999✔
126
            return Ok(Some(MinMaxResult {
1,999✔
127
                min: array.scalar().clone(),
1,999✔
128
                max: array.scalar().clone(),
1,999✔
129
            }));
1,999✔
NEW
130
        }
×
131
    }
60,643✔
132

133
    let min = array
60,643✔
134
        .statistics()
60,643✔
135
        .get_scalar(Stat::Min, array.dtype())
60,643✔
136
        .and_then(Precision::as_exact);
60,643✔
137
    let max = array
60,643✔
138
        .statistics()
60,643✔
139
        .get_scalar(Stat::Max, array.dtype())
60,643✔
140
        .and_then(Precision::as_exact);
60,643✔
141

142
    if let Some((min, max)) = min.zip(max) {
60,643✔
143
        return Ok(Some(MinMaxResult { min, max }));
396✔
144
    }
60,247✔
145

146
    let args = InvocationArgs {
60,247✔
147
        inputs: &[array.into()],
60,247✔
148
        options: &(),
60,247✔
149
    };
60,247✔
150
    for kernel in kernels {
230,426✔
151
        if let Some(output) = kernel.invoke(&args)? {
230,352✔
152
            return MinMaxResult::from_scalar(output.unwrap_scalar()?);
60,173✔
153
        }
170,179✔
154
    }
155
    if let Some(output) = array.invoke(&MIN_MAX_FN, &args)? {
74✔
156
        return MinMaxResult::from_scalar(output.unwrap_scalar()?);
×
157
    }
74✔
158

159
    if !array.is_canonical() {
74✔
160
        let array = array.to_canonical()?;
74✔
161
        return min_max(array.as_ref());
74✔
162
    }
×
163

164
    vortex_bail!(NotImplemented: "min_max", array.encoding_id());
×
165
}
63,088✔
166

167
/// The minimum and maximum non-null values of an array, or None if there are no non-null values.
168
pub trait MinMaxKernel: VTable {
169
    fn min_max(&self, array: &Self::Array) -> VortexResult<Option<MinMaxResult>>;
170
}
171

172
pub struct MinMaxKernelRef(ArcRef<dyn Kernel>);
173
inventory::collect!(MinMaxKernelRef);
174

175
#[derive(Debug)]
176
pub struct MinMaxKernelAdapter<V: VTable>(pub V);
177

178
impl<V: VTable + MinMaxKernel> MinMaxKernelAdapter<V> {
179
    pub const fn lift(&'static self) -> MinMaxKernelRef {
×
180
        MinMaxKernelRef(ArcRef::new_ref(self))
×
181
    }
×
182
}
183

184
impl<V: VTable + MinMaxKernel> Kernel for MinMaxKernelAdapter<V> {
185
    fn invoke(&self, args: &InvocationArgs) -> VortexResult<Option<Output>> {
230,274✔
186
        let inputs = UnaryArgs::<()>::try_from(args)?;
230,274✔
187
        let Some(array) = inputs.array.as_opt::<V>() else {
230,274✔
188
            return Ok(None);
170,137✔
189
        };
190
        let dtype = DType::Struct(
60,137✔
191
            StructFields::new(
60,137✔
192
                ["min", "max"].into(),
60,137✔
193
                vec![array.dtype().clone(), array.dtype().clone()],
60,137✔
194
            ),
60,137✔
195
            Nullability::Nullable,
60,137✔
196
        );
60,137✔
197
        Ok(Some(match V::min_max(&self.0, array)? {
60,137✔
198
            None => Scalar::null(dtype).into(),
5,276✔
199
            Some(MinMaxResult { min, max }) => Scalar::struct_(dtype, vec![min, max]).into(),
54,861✔
200
        }))
201
    }
230,274✔
202
}
203

204
pub static MIN_MAX_FN: LazyLock<ComputeFn> = LazyLock::new(|| {
4,497✔
205
    let compute = ComputeFn::new("min_max".into(), ArcRef::new_ref(&MinMax));
4,497✔
206
    for kernel in inventory::iter::<MinMaxKernelRef> {
57,354✔
207
        compute.register_kernel(kernel.0.clone());
52,857✔
208
    }
52,857✔
209
    compute
4,497✔
210
});
4,497✔
211

212
#[cfg(test)]
213
mod tests {
214
    use arrow_buffer::BooleanBuffer;
215
    use vortex_buffer::buffer;
216

217
    use crate::arrays::{BoolArray, NullArray, PrimitiveArray};
218
    use crate::compute::{MinMaxResult, min_max};
219
    use crate::validity::Validity;
220

221
    #[test]
222
    fn test_prim_max() {
1✔
223
        let p = PrimitiveArray::new(buffer![1, 2, 3], Validity::NonNullable);
1✔
224
        assert_eq!(
1✔
225
            min_max(p.as_ref()).unwrap(),
1✔
226
            Some(MinMaxResult {
1✔
227
                min: 1.into(),
1✔
228
                max: 3.into()
1✔
229
            })
1✔
230
        );
231
    }
1✔
232

233
    #[test]
234
    fn test_bool_max() {
1✔
235
        let p = BoolArray::new(
1✔
236
            BooleanBuffer::from([true, true, true].as_slice()),
1✔
237
            Validity::NonNullable,
1✔
238
        );
239
        assert_eq!(
1✔
240
            min_max(p.as_ref()).unwrap(),
1✔
241
            Some(MinMaxResult {
1✔
242
                min: true.into(),
1✔
243
                max: true.into()
1✔
244
            })
1✔
245
        );
246

247
        let p = BoolArray::new(
1✔
248
            BooleanBuffer::from([false, false, false].as_slice()),
1✔
249
            Validity::NonNullable,
1✔
250
        );
251
        assert_eq!(
1✔
252
            min_max(p.as_ref()).unwrap(),
1✔
253
            Some(MinMaxResult {
1✔
254
                min: false.into(),
1✔
255
                max: false.into()
1✔
256
            })
1✔
257
        );
258

259
        let p = BoolArray::new(
1✔
260
            BooleanBuffer::from([false, true, false].as_slice()),
1✔
261
            Validity::NonNullable,
1✔
262
        );
263
        assert_eq!(
1✔
264
            min_max(p.as_ref()).unwrap(),
1✔
265
            Some(MinMaxResult {
1✔
266
                min: false.into(),
1✔
267
                max: true.into()
1✔
268
            })
1✔
269
        );
270
    }
1✔
271

272
    #[test]
273
    fn test_null() {
1✔
274
        let p = NullArray::new(1);
1✔
275
        assert_eq!(min_max(p.as_ref()).unwrap(), None);
1✔
276
    }
1✔
277
}
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