Skip to main content

vortex_array/array/probe/
array.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::mem::needs_drop;
5
6use vortex_error::VortexError;
7use vortex_error::VortexResult;
8use vortex_error::vortex_err;
9
10use crate::ArrayRef;
11use crate::ExecutionCtx;
12use crate::array::ArrayView;
13use crate::array::VTable;
14use crate::array::probe::RepeatedArrayProbe;
15use crate::array::probe::RepeatedState;
16use crate::array::probe::repeated::child_probe;
17use crate::array::probe::repeated::is_valid_scalar;
18use crate::arrays::Primitive;
19use crate::scalar::Scalar;
20use crate::validity::Validity;
21use crate::vtable::OperationsVTable;
22
23/// A borrowed row accessor.
24///
25/// Either a one-off reader over a borrowed array, which retains nothing, or a borrow of a
26/// [`RepeatedArrayProbe`] that keeps state between reads. Both variants hold only references,
27/// so an `ArrayProbe` has no destructor and building one per read, including for every child
28/// slot of a nested array, costs nothing.
29pub enum ArrayProbe<'a> {
30    /// A single read of a borrowed array; nothing outlives the call.
31    Once(&'a ArrayRef),
32    /// A read through a retained probe.
33    Repeated(&'a mut RepeatedArrayProbe),
34}
35
36// Everything built per read must be free to drop, or the unwind path pins it in memory and
37// blocks the tail call into the encoding. Keep these at compile time.
38const _: () = assert!(!needs_drop::<ArrayProbe<'_>>());
39const _: () = assert!(!needs_drop::<ProbeState<'_, Primitive>>());
40
41impl ArrayProbe<'_> {
42    /// The array this probe reads from.
43    #[inline]
44    pub fn array(&self) -> &ArrayRef {
45        match self {
46            Self::Once(array) => array,
47            Self::Repeated(probe) => probe.array(),
48        }
49    }
50
51    /// Read the scalar at `index`, including its nullness.
52    #[inline]
53    pub fn execute_scalar(&mut self, index: usize, ctx: &mut ExecutionCtx) -> VortexResult<Scalar> {
54        match self {
55            Self::Once(array) => execute_scalar_once(array, index, ctx),
56            Self::Repeated(probe) => probe.execute_scalar(index, ctx),
57        }
58    }
59
60    /// Whether the row at `index` is valid.
61    #[inline]
62    pub fn execute_is_valid(&mut self, index: usize, ctx: &mut ExecutionCtx) -> VortexResult<bool> {
63        match self {
64            Self::Once(array) => execute_is_valid_once(array, index, ctx),
65            Self::Repeated(probe) => probe.execute_is_valid(index, ctx),
66        }
67    }
68
69    /// Whether the row at `index` is null.
70    #[inline]
71    pub fn execute_is_invalid(
72        &mut self,
73        index: usize,
74        ctx: &mut ExecutionCtx,
75    ) -> VortexResult<bool> {
76        Ok(!self.execute_is_valid(index, ctx)?)
77    }
78}
79
80/// One-off scalar read: nothing outlives the call.
81#[inline]
82fn execute_scalar_once(
83    array: &ArrayRef,
84    index: usize,
85    ctx: &mut ExecutionCtx,
86) -> VortexResult<Scalar> {
87    check_bounds(array, index)?;
88    check_dtype(
89        array,
90        array.dyn_array().probe_scalar_once(array, index, ctx),
91    )
92}
93
94/// One-off validity read: nothing outlives the call.
95#[inline]
96fn execute_is_valid_once(
97    array: &ArrayRef,
98    index: usize,
99    ctx: &mut ExecutionCtx,
100) -> VortexResult<bool> {
101    check_bounds(array, index)?;
102    is_valid_once(array, index, ctx)
103}
104
105#[inline]
106fn is_valid_once(array: &ArrayRef, index: usize, ctx: &mut ExecutionCtx) -> VortexResult<bool> {
107    if !array.dtype().is_nullable() {
108        return Ok(true);
109    }
110    match array.validity()? {
111        Validity::NonNullable | Validity::AllValid => Ok(true),
112        Validity::AllInvalid => Ok(false),
113        Validity::Array(validity) => is_valid_scalar(validity.execute_scalar(index, ctx)?, index),
114    }
115}
116
117/// Pass an encoding's result through, checking its dtype in debug builds.
118///
119/// Unwrapping and re-wrapping the result here would cost an extra copy of the scalar on every
120/// read.
121#[inline]
122pub(super) fn check_dtype(array: &ArrayRef, result: VortexResult<Scalar>) -> VortexResult<Scalar> {
123    result.inspect(|scalar| {
124        debug_assert_eq!(scalar.dtype(), array.dtype(), "Scalar dtype mismatch");
125    })
126}
127
128#[inline]
129pub(super) fn check_bounds(array: &ArrayRef, index: usize) -> VortexResult<()> {
130    if index >= array.len() {
131        return Err(out_of_bounds(index, array.len()));
132    }
133    Ok(())
134}
135
136/// Kept out of line so the error path, which captures a backtrace, does not count against the
137/// hot probe bodies when the inliner sizes them.
138#[cold]
139#[inline(never)]
140fn out_of_bounds(index: usize, len: usize) -> VortexError {
141    vortex_err!(OutOfBounds: index, 0, len)
142}
143
144/// The encoding state type of `V`'s operations vtable.
145pub type EncodingProbeState<V> =
146    <<V as VTable>::OperationsVTable as OperationsVTable<V>>::ProbeState;
147
148/// Everything an encoding's `probe_scalar` runs with: the typed view of the array being read
149/// and, for a repeated read, a borrow of the state its [`RepeatedArrayProbe`] keeps.
150///
151/// Passed to [`OperationsVTable::probe_scalar`].
152/// A one-off read gets [`ProbeState::once`], which holds only the view. A repeated read borrows
153/// the encoding's own state and the lazily created child probes. Neither owns anything, so
154/// building one per read is free.
155///
156/// Encodings never inspect the policy: [`ProbeState::slot`] hands out a probe over a child under
157/// whichever policy is in force, [`ProbeState::retained`] hands out the encoding state only when
158/// it is kept, and [`ProbeState::split`] gives both at once.
159pub struct ProbeState<'a, V: VTable> {
160    array: ArrayView<'a, V>,
161    retained: Option<&'a mut RepeatedState<EncodingProbeState<V>>>,
162}
163
164impl<'a, V: VTable> ProbeState<'a, V> {
165    /// State for a single read of `array`.
166    ///
167    /// Encodings use it to run their `probe_scalar` path from the deprecated `scalar_at`; it
168    /// goes away with `scalar_at`.
169    #[inline]
170    pub fn once(array: ArrayView<'a, V>) -> Self {
171        Self {
172            array,
173            retained: None,
174        }
175    }
176
177    /// State for a read through a [`RepeatedArrayProbe`], borrowing what it keeps.
178    #[inline]
179    pub(crate) fn repeated(
180        array: ArrayView<'a, V>,
181        retained: &'a mut RepeatedState<EncodingProbeState<V>>,
182    ) -> Self {
183        Self {
184            array,
185            retained: Some(retained),
186        }
187    }
188
189    /// The typed view of the array being read.
190    #[inline]
191    pub fn array(&self) -> ArrayView<'a, V> {
192        self.array
193    }
194
195    /// Whether the row at `index` of the array being read is valid.
196    /// Will use the cached state to resolve the query.
197    #[inline]
198    pub fn is_valid(&mut self, index: usize, ctx: &mut ExecutionCtx) -> VortexResult<bool> {
199        let array = self.array.array();
200        match &mut self.retained {
201            None => is_valid_once(array, index, ctx),
202            Some(retained) => retained.is_valid(array, index, ctx),
203        }
204    }
205
206    /// The encoding's retained state, or `None` for a one-off read.
207    ///
208    /// Encodings whose repeated algorithm differs from their one-off one branch on this. To hold
209    /// the state while reading children, use [`Self::split`].
210    #[inline]
211    pub fn retained(&mut self) -> Option<&mut EncodingProbeState<V>> {
212        self.retained.as_deref_mut().map(RepeatedState::state_mut)
213    }
214
215    /// The encoding's retained state and the array's children, borrowed apart, so a cache can
216    /// stay borrowed while children are read.
217    #[inline]
218    pub fn split(&mut self) -> (Option<&mut EncodingProbeState<V>>, ProbeChildren<'_, 'a, V>) {
219        let array = self.array;
220        match &mut self.retained {
221            None => (None, ProbeChildren { array, slots: None }),
222            Some(repeated) => {
223                let (state, slots) = repeated.split_mut();
224                let children = ProbeChildren {
225                    array,
226                    slots: Some(slots),
227                };
228                (Some(state), children)
229            }
230        }
231    }
232
233    /// A probe over the array's child in `slot`, under this state's policy.
234    ///
235    /// Shorthand for [`Self::split`] followed by [`ProbeChildren::slot`]. `None` if the slot is
236    /// absent, an error if it is out of bounds.
237    #[inline]
238    pub fn slot(&mut self, slot: usize) -> VortexResult<Option<ArrayProbe<'_>>> {
239        let parent = self.array.array();
240        match &mut self.retained {
241            None => Ok(child_of(parent, slot)?.map(ArrayProbe::Once)),
242            Some(repeated) => {
243                Ok(child_probe(repeated.split_mut().1, parent, slot)?.map(ArrayProbe::Repeated))
244            }
245        }
246    }
247}
248
249/// The child slots of the array a [`ProbeState`] reads, borrowed apart from the encoding state
250/// by [`ProbeState::split`].
251pub struct ProbeChildren<'s, 'a, V: VTable> {
252    array: ArrayView<'a, V>,
253    /// The retained slot table, or `None` for a one-off read.
254    slots: Option<&'s mut Vec<Option<RepeatedArrayProbe>>>,
255}
256
257impl<V: VTable> ProbeChildren<'_, '_, V> {
258    /// A probe over the child in `slot`, under the read's policy.
259    ///
260    /// For a one-off read this borrows the child; for a repeated read it is the
261    /// [`RepeatedArrayProbe`] kept in the slot table, created on first use. `None` if the slot
262    /// is absent, an error if it is out of bounds.
263    #[inline]
264    pub fn slot(&mut self, slot: usize) -> VortexResult<Option<ArrayProbe<'_>>> {
265        let parent = self.array.array();
266        match &mut self.slots {
267            None => Ok(child_of(parent, slot)?.map(ArrayProbe::Once)),
268            Some(slots) => Ok(child_probe(slots, parent, slot)?.map(ArrayProbe::Repeated)),
269        }
270    }
271}
272
273/// The child of `parent` in `slot`: `None` if the slot is absent, an error if it is out of bounds.
274#[inline]
275pub(super) fn child_of(parent: &ArrayRef, slot: usize) -> VortexResult<Option<&ArrayRef>> {
276    Ok(parent
277        .slots()
278        .get(slot)
279        .ok_or_else(|| vortex_err!("Probe slot {slot} is out of bounds"))?
280        .as_ref())
281}
282#[cfg(test)]
283mod tests {
284    use vortex_error::VortexResult;
285    use vortex_error::vortex_err;
286
287    use super::*;
288    use crate::VortexSessionExecute;
289    use crate::array::IntoArray;
290    use crate::arrays::BoolArray;
291    use crate::arrays::DictArray;
292    use crate::arrays::PrimitiveArray;
293    use crate::arrays::Struct;
294    use crate::arrays::StructArray;
295
296    fn nullable_ints() -> ArrayRef {
297        PrimitiveArray::from_option_iter([Some(10i32), None, Some(30)]).into_array()
298    }
299
300    fn check_reads(mut probe: ArrayProbe<'_>, ctx: &mut ExecutionCtx) -> VortexResult<()> {
301        assert!(probe.execute_scalar(3, ctx).is_err());
302        assert!(probe.execute_is_valid(3, ctx).is_err());
303        assert!(probe.execute_scalar(1, ctx)?.is_null());
304        assert!(!probe.execute_is_valid(1, ctx)?);
305        assert!(probe.execute_is_invalid(1, ctx)?);
306        assert_eq!(probe.execute_scalar(2, ctx)?, Scalar::from(Some(30i32)));
307        assert_eq!(probe.execute_scalar(0, ctx)?, Scalar::from(Some(10i32)));
308        Ok(())
309    }
310
311    #[test]
312    fn once_probe_checks_bounds_and_nulls() -> VortexResult<()> {
313        let mut ctx = crate::array_session().create_execution_ctx();
314        let array = nullable_ints();
315        check_reads(array.probe(), &mut ctx)
316    }
317
318    /// Bool has no `probe_scalar` of its own, so this reads through the default that checks
319    /// validity before `scalar_at`.
320    #[test]
321    fn default_probe_scalar_resolves_nulls() -> VortexResult<()> {
322        let mut ctx = crate::array_session().create_execution_ctx();
323        let array = BoolArray::from_iter([Some(true), None, Some(false)]).into_array();
324        check_bool_reads(array.probe(), &mut ctx)?;
325        check_bool_reads(array.repeated_probe().as_probe(), &mut ctx)
326    }
327
328    fn check_bool_reads(mut probe: ArrayProbe<'_>, ctx: &mut ExecutionCtx) -> VortexResult<()> {
329        assert_eq!(probe.execute_scalar(0, ctx)?, Scalar::from(Some(true)));
330        assert!(probe.execute_scalar(1, ctx)?.is_null());
331        assert_eq!(probe.execute_scalar(2, ctx)?, Scalar::from(Some(false)));
332        Ok(())
333    }
334
335    /// A null reached through a child comes back from the wrapper as a null of the wrapper's
336    /// dtype, on both the one-off and the retained path.
337    #[test]
338    fn wrapper_passes_child_nulls_through() -> VortexResult<()> {
339        let mut ctx = crate::array_session().create_execution_ctx();
340        let codes = PrimitiveArray::from_iter([0u32, 1, 0, 2]).into_array();
341        let values = PrimitiveArray::from_option_iter([Some(7i64), None, Some(9)]).into_array();
342        let array = DictArray::try_new(codes, values)?.into_array();
343        let expected = [Some(7i64), None, Some(7), Some(9)];
344        let mut once = array.probe();
345        let mut repeated = array.repeated_probe();
346        for (index, value) in expected.into_iter().enumerate() {
347            assert_eq!(once.execute_scalar(index, &mut ctx)?, Scalar::from(value));
348            assert_eq!(
349                repeated.execute_scalar(index, &mut ctx)?,
350                Scalar::from(value)
351            );
352        }
353        Ok(())
354    }
355
356    /// Nullable codes and nullable values each produce nulls, and one retained probe answers
357    /// validity and scalar reads on both, sliced or not.
358    #[test]
359    fn dict_with_nullable_codes_and_values() -> VortexResult<()> {
360        let mut ctx = crate::array_session().create_execution_ctx();
361        let codes = PrimitiveArray::from_option_iter([Some(0u32), None, Some(1), Some(2), None])
362            .into_array();
363        let values = PrimitiveArray::from_option_iter([Some(7i64), None, Some(9)]).into_array();
364        let array = DictArray::try_new(codes, values)?.into_array();
365        let expected = [Some(7i64), None, None, Some(9), None];
366        for (array, expected) in [
367            (array.clone(), &expected[..]),
368            (array.slice(1..4)?, &expected[1..4]),
369        ] {
370            let mut once = array.probe();
371            let mut repeated = array.repeated_probe();
372            for (index, value) in expected.iter().enumerate().rev() {
373                assert_eq!(once.execute_is_valid(index, &mut ctx)?, value.is_some());
374                assert_eq!(repeated.execute_is_valid(index, &mut ctx)?, value.is_some());
375                assert_eq!(once.execute_scalar(index, &mut ctx)?, Scalar::from(*value));
376                assert_eq!(
377                    repeated.execute_scalar(index, &mut ctx)?,
378                    Scalar::from(*value)
379                );
380            }
381        }
382        Ok(())
383    }
384
385    #[test]
386    fn state_validity_follows_read_policy() -> VortexResult<()> {
387        let mut ctx = crate::array_session().create_execution_ctx();
388        let array = nullable_ints();
389        let typed = array
390            .as_opt::<Primitive>()
391            .ok_or_else(|| vortex_err!("expected a primitive"))?;
392
393        let mut once = ProbeState::once(typed);
394        assert!(once.is_valid(0, &mut ctx)?);
395        assert!(!once.is_valid(1, &mut ctx)?);
396
397        let mut repeated = RepeatedState::<()>::default();
398        let mut state = ProbeState::repeated(typed, &mut repeated);
399        assert!(!state.is_valid(1, &mut ctx)?);
400        assert!(state.is_valid(2, &mut ctx)?);
401
402        let all_valid = PrimitiveArray::from_option_iter([Some(1i32)]).into_array();
403        let typed = all_valid
404            .as_opt::<Primitive>()
405            .ok_or_else(|| vortex_err!("expected a primitive"))?;
406        assert!(ProbeState::once(typed).is_valid(0, &mut ctx)?);
407        Ok(())
408    }
409
410    #[test]
411    fn once_state_reads_children_without_retaining() -> VortexResult<()> {
412        let mut ctx = crate::array_session().create_execution_ctx();
413        let array = struct_of_two_fields()?;
414        let typed = array
415            .as_opt::<Struct>()
416            .ok_or_else(|| vortex_err!("expected a struct"))?;
417        let mut state = ProbeState::once(typed);
418
419        assert!(state.slot(5).is_err());
420        // Slot 0 is the struct's absent validity.
421        assert!(state.slot(0)?.is_none());
422        {
423            let mut field = state.slot(2)?.ok_or_else(|| vortex_err!("missing field"))?;
424            assert!(matches!(field, ArrayProbe::Once(_)));
425            assert_eq!(field.array().len(), 2);
426            assert_eq!(field.execute_scalar(1, &mut ctx)?, Scalar::from(4i64));
427            assert!(field.execute_is_valid(0, &mut ctx)?);
428        }
429        assert!(state.retained().is_none());
430        let (retained, mut children) = state.split();
431        assert!(retained.is_none());
432        let mut field = children
433            .slot(1)?
434            .ok_or_else(|| vortex_err!("missing field"))?;
435        assert_eq!(field.execute_scalar(1, &mut ctx)?, Scalar::from(2i32));
436        Ok(())
437    }
438
439    fn struct_of_two_fields() -> VortexResult<ArrayRef> {
440        Ok(StructArray::from_fields(&[
441            ("a", PrimitiveArray::from_iter([1i32, 2]).into_array()),
442            ("b", PrimitiveArray::from_iter([3i64, 4]).into_array()),
443        ])?
444        .into_array())
445    }
446}