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

vortex-data / vortex / 16598973893

29 Jul 2025 02:25PM UTC coverage: 82.692% (-0.01%) from 82.703%
16598973893

push

github

web-flow
Clean up stats propagation for slicing (#3356)

Reduces the amount we copy some stats (by removing into_iter that forces
a full stats copy)

---------

Signed-off-by: Nicholas Gates <nick@nickgates.com>
Signed-off-by: Robert Kruszewski <github@robertk.io>
Signed-off-by: Will Manning <will@willmanning.io>
Co-authored-by: Robert Kruszewski <github@robertk.io>
Co-authored-by: Will Manning <will@willmanning.io>

130 of 157 new or added lines in 15 files covered. (82.8%)

30 existing lines in 13 files now uncovered.

45215 of 54679 relevant lines covered (82.69%)

184610.34 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
static MIN_MAX_FN: LazyLock<ComputeFn> = LazyLock::new(|| {
5,036✔
18
    let compute = ComputeFn::new("min_max".into(), ArcRef::new_ref(&MinMax));
5,036✔
19
    for kernel in inventory::iter::<MinMaxKernelRef> {
63,876✔
20
        compute.register_kernel(kernel.0.clone());
58,840✔
21
    }
58,840✔
22
    compute
5,036✔
23
});
5,036✔
24

25
/// The minimum and maximum non-null values of an array, or None if there are no non-null values.
26
///
27
/// The return value dtype is the non-nullable version of the array dtype.
28
///
29
/// This will update the stats set of this array (as a side effect).
30
pub fn min_max(array: &dyn Array) -> VortexResult<Option<MinMaxResult>> {
64,762✔
31
    let scalar = MIN_MAX_FN
64,762✔
32
        .invoke(&InvocationArgs {
64,762✔
33
            inputs: &[array.into()],
64,762✔
34
            options: &(),
64,762✔
35
        })?
64,762✔
36
        .unwrap_scalar()?;
64,762✔
37
    MinMaxResult::from_scalar(scalar)
64,762✔
38
}
64,762✔
39

40
#[derive(Debug, Clone, PartialEq, Eq)]
41
pub struct MinMaxResult {
42
    pub min: Scalar,
43
    pub max: Scalar,
44
}
45

46
impl MinMaxResult {
47
    pub fn from_scalar(scalar: Scalar) -> VortexResult<Option<Self>> {
126,537✔
48
        if scalar.is_null() {
126,537✔
49
            Ok(None)
11,282✔
50
        } else {
51
            let min = scalar
115,255✔
52
                .as_struct()
115,255✔
53
                .field_by_idx(0)
115,255✔
54
                .vortex_expect("missing min field");
115,255✔
55
            let max = scalar
115,255✔
56
                .as_struct()
115,255✔
57
                .field_by_idx(1)
115,255✔
58
                .vortex_expect("missing max field");
115,255✔
59
            Ok(Some(MinMaxResult { min, max }))
115,255✔
60
        }
61
    }
126,537✔
62
}
63

64
pub struct MinMax;
65

66
impl ComputeFnVTable for MinMax {
67
    fn invoke(
64,762✔
68
        &self,
64,762✔
69
        args: &InvocationArgs,
64,762✔
70
        kernels: &[ArcRef<dyn Kernel>],
64,762✔
71
    ) -> VortexResult<Output> {
64,762✔
72
        let UnaryArgs { array, .. } = UnaryArgs::<()>::try_from(args)?;
64,762✔
73

74
        let return_dtype = self.return_dtype(args)?;
64,762✔
75

76
        match min_max_impl(array, kernels)? {
64,762✔
77
            None => Ok(Scalar::null(return_dtype).into()),
5,870✔
78
            Some(MinMaxResult { min, max }) => {
58,892✔
79
                assert!(
58,892✔
80
                    min <= max,
58,892✔
81
                    "min > max: min={} max={} encoding={}",
×
82
                    min,
83
                    max,
84
                    array.encoding_id()
×
85
                );
86

87
                // Update the stats set with the computed min/max
88
                array
58,892✔
89
                    .statistics()
58,892✔
90
                    .set(Stat::Min, Precision::Exact(min.value().clone()));
58,892✔
91
                array
58,892✔
92
                    .statistics()
58,892✔
93
                    .set(Stat::Max, Precision::Exact(max.value().clone()));
58,892✔
94

95
                // Return the min/max as a struct scalar
96
                Ok(Scalar::struct_(return_dtype, vec![min, max]).into())
58,892✔
97
            }
98
        }
99
    }
64,762✔
100

101
    fn return_dtype(&self, args: &InvocationArgs) -> VortexResult<DType> {
129,524✔
102
        let UnaryArgs { array, .. } = UnaryArgs::<()>::try_from(args)?;
129,524✔
103

104
        // We return a min/max struct scalar, where the overall struct is nullable in the case
105
        // that the array is all null or empty.
106
        Ok(DType::Struct(
129,524✔
107
            StructFields::new(
129,524✔
108
                ["min", "max"].into(),
129,524✔
109
                vec![array.dtype().clone(), array.dtype().clone()],
129,524✔
110
            ),
129,524✔
111
            Nullability::Nullable,
129,524✔
112
        ))
129,524✔
113
    }
129,524✔
114

115
    fn return_len(&self, _args: &InvocationArgs) -> VortexResult<usize> {
64,762✔
116
        Ok(1)
64,762✔
117
    }
64,762✔
118

119
    fn is_elementwise(&self) -> bool {
64,762✔
120
        false
64,762✔
121
    }
64,762✔
122
}
123

124
fn min_max_impl(
64,762✔
125
    array: &dyn Array,
64,762✔
126
    kernels: &[ArcRef<dyn Kernel>],
64,762✔
127
) -> VortexResult<Option<MinMaxResult>> {
64,762✔
128
    if array.is_empty() || array.valid_count()? == 0 {
64,762✔
129
        return Ok(None);
458✔
130
    }
64,304✔
131

132
    if let Some(array) = array.as_opt::<ConstantVTable>() {
64,304✔
133
        if !array.scalar().is_null() {
2,053✔
134
            return Ok(Some(MinMaxResult {
2,053✔
135
                min: array.scalar().clone(),
2,053✔
136
                max: array.scalar().clone(),
2,053✔
137
            }));
2,053✔
NEW
138
        }
×
139
    }
62,251✔
140

141
    let min = array
62,251✔
142
        .statistics()
62,251✔
143
        .get_scalar(Stat::Min, array.dtype())
62,251✔
144
        .and_then(Precision::as_exact);
62,251✔
145
    let max = array
62,251✔
146
        .statistics()
62,251✔
147
        .get_scalar(Stat::Max, array.dtype())
62,251✔
148
        .and_then(Precision::as_exact);
62,251✔
149

150
    if let Some((min, max)) = min.zip(max) {
62,251✔
151
        return Ok(Some(MinMaxResult { min, max }));
400✔
152
    }
61,851✔
153

154
    let args = InvocationArgs {
61,851✔
155
        inputs: &[array.into()],
61,851✔
156
        options: &(),
61,851✔
157
    };
61,851✔
158
    for kernel in kernels {
170,987✔
159
        if let Some(output) = kernel.invoke(&args)? {
170,911✔
160
            return MinMaxResult::from_scalar(output.unwrap_scalar()?);
61,775✔
161
        }
109,136✔
162
    }
163
    if let Some(output) = array.invoke(&MIN_MAX_FN, &args)? {
76✔
164
        return MinMaxResult::from_scalar(output.unwrap_scalar()?);
×
165
    }
76✔
166

167
    if !array.is_canonical() {
76✔
168
        let array = array.to_canonical()?;
76✔
169
        return min_max(array.as_ref());
76✔
170
    }
×
171

172
    vortex_bail!(NotImplemented: "min_max", array.encoding_id());
×
173
}
64,762✔
174

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

180
pub struct MinMaxKernelRef(ArcRef<dyn Kernel>);
181
inventory::collect!(MinMaxKernelRef);
182

183
#[derive(Debug)]
184
pub struct MinMaxKernelAdapter<V: VTable>(pub V);
185

186
impl<V: VTable + MinMaxKernel> MinMaxKernelAdapter<V> {
187
    pub const fn lift(&'static self) -> MinMaxKernelRef {
×
188
        MinMaxKernelRef(ArcRef::new_ref(self))
×
189
    }
×
190
}
191

192
impl<V: VTable + MinMaxKernel> Kernel for MinMaxKernelAdapter<V> {
193
    fn invoke(&self, args: &InvocationArgs) -> VortexResult<Option<Output>> {
170,829✔
194
        let inputs = UnaryArgs::<()>::try_from(args)?;
170,829✔
195
        let Some(array) = inputs.array.as_opt::<V>() else {
170,829✔
196
            return Ok(None);
109,091✔
197
        };
198
        let dtype = DType::Struct(
61,738✔
199
            StructFields::new(
61,738✔
200
                ["min", "max"].into(),
61,738✔
201
                vec![array.dtype().clone(), array.dtype().clone()],
61,738✔
202
            ),
61,738✔
203
            Nullability::Nullable,
61,738✔
204
        );
61,738✔
205
        Ok(Some(match V::min_max(&self.0, array)? {
61,738✔
206
            None => Scalar::null(dtype).into(),
5,412✔
207
            Some(MinMaxResult { min, max }) => Scalar::struct_(dtype, vec![min, max]).into(),
56,326✔
208
        }))
209
    }
170,829✔
210
}
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