Skip to main content

vortex_array/search_sorted/
primitive.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::cell::RefCell;
5use std::cmp::Ordering;
6use std::marker::PhantomData;
7
8use vortex_error::VortexResult;
9
10use crate::ArrayRef;
11use crate::ExecutionCtx;
12use crate::RepeatedArrayProbe;
13use crate::arrays::Primitive;
14use crate::arrays::primitive::PrimitiveArrayExt;
15use crate::dtype::NativePType;
16use crate::search_sorted::IndexOrd;
17
18/// A [`SearchSorted`](crate::search_sorted::SearchSorted) adapter over a sorted primitive-typed
19/// array, comparing elements as the native type `T`.
20///
21/// Reads go through a [`RepeatedArrayProbe`], so the encoding state and validity resolved on the
22/// first comparison are reused by the rest of the search rather than rebuilt per probe.
23///
24/// Values can be searched as `T`, `Option<T>`, or `usize`. Searching as `T` or `usize` treats
25/// null elements as `T::zero()`; use `Option<T>` when the array may contain nulls, in which case
26/// nulls sort before all non-null values.
27pub struct SearchSortedPrimitiveArray<'a, T> {
28    reader: Reader<'a, T>,
29    len: usize,
30    ctx: RefCell<&'a mut ExecutionCtx>,
31    _ptype: PhantomData<T>,
32}
33
34/// Where a comparison reads its element from.
35enum Reader<'a, T> {
36    /// The array's own buffer. Only for a canonical, non-nullable, host-backed array, where
37    /// every element is a `T` at a known offset and none of them is null.
38    Values(&'a [T]),
39    /// Anything else, through one probe reused by the whole search.
40    Probe(RefCell<RepeatedArrayProbe>),
41}
42
43impl<'a, T: NativePType> SearchSortedPrimitiveArray<'a, T> {
44    /// Wraps `array` for searching, panicking if the array's [`PType`](crate::dtype::PType) is
45    /// not `T::PTYPE`.
46    pub fn new(array: &'a ArrayRef, ctx: &'a mut ExecutionCtx) -> Self {
47        assert_eq!(
48            array.dtype().as_ptype(),
49            T::PTYPE,
50            "Array PType must match primitive type"
51        );
52        Self {
53            reader: Self::reader(array),
54            len: array.len(),
55            ctx: RefCell::new(ctx),
56            _ptype: PhantomData,
57        }
58    }
59
60    /// Reads the buffer directly when the array can offer one, and probes otherwise.
61    ///
62    /// A nullable array is excluded because the buffer holds an arbitrary value where an element
63    /// is null, and a device-backed array because its buffer is not addressable here.
64    fn reader(array: &'a ArrayRef) -> Reader<'a, T> {
65        if !array.dtype().is_nullable()
66            && let Some(primitive) = array.as_opt::<Primitive>()
67            && primitive.buffer_handle().as_host_opt().is_some()
68            && let Some(values) = primitive.data().as_slice::<T>().get(..array.len())
69        {
70            return Reader::Values(values);
71        }
72        Reader::Probe(RefCell::new(array.repeated_probe()))
73    }
74
75    /// Returns the value at `idx`, or `None` if the element is null.
76    ///
77    /// The probe and the context are separate cells, so the two borrows never overlap.
78    fn typed_value(&self, idx: usize) -> VortexResult<Option<T>> {
79        let probe = match &self.reader {
80            Reader::Values(values) => return Ok(Some(values[idx])),
81            Reader::Probe(probe) => probe,
82        };
83        Ok(probe
84            .borrow_mut()
85            .execute_scalar(idx, &mut self.ctx.borrow_mut())?
86            .as_primitive()
87            .typed_value::<T>())
88    }
89
90    /// Returns the value at `idx`, with nulls mapped to `T::zero()`.
91    fn value(&self, idx: usize) -> VortexResult<T> {
92        Ok(self.typed_value(idx)?.unwrap_or_else(|| T::zero()))
93    }
94}
95
96impl<T: NativePType> IndexOrd<T> for SearchSortedPrimitiveArray<'_, T> {
97    fn index_cmp(&self, idx: usize, elem: &T) -> VortexResult<Option<Ordering>> {
98        let value = self.value(idx)?;
99        Ok(Some(value.total_compare(*elem)))
100    }
101
102    fn index_len(&self) -> usize {
103        self.len
104    }
105}
106
107impl<T: NativePType> IndexOrd<Option<T>> for SearchSortedPrimitiveArray<'_, T> {
108    fn index_cmp(&self, idx: usize, elem: &Option<T>) -> VortexResult<Option<Ordering>> {
109        // A null element reads back as a null scalar, so one read answers both whether the
110        // element is valid and what its value is.
111        let value = self.typed_value(idx)?;
112
113        Ok(match (value, elem.as_ref()) {
114            (Some(l), Some(r)) => Some(l.total_compare(*r)),
115            (Some(_), None) => Some(Ordering::Greater),
116            (None, Some(_)) => Some(Ordering::Less),
117            (None, None) => Some(Ordering::Equal),
118        })
119    }
120
121    fn index_len(&self) -> usize {
122        self.len
123    }
124}
125
126impl<T: NativePType> IndexOrd<usize> for SearchSortedPrimitiveArray<'_, T> {
127    fn index_cmp(&self, idx: usize, elem: &usize) -> VortexResult<Option<Ordering>> {
128        let value = self.value(idx)?;
129
130        let Some(elem_t) = T::from_usize(*elem) else {
131            return Ok(Some(Ordering::Less));
132        };
133
134        Ok(Some(value.total_compare(elem_t)))
135    }
136
137    fn index_len(&self) -> usize {
138        self.len
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use vortex_buffer::buffer;
145    use vortex_error::VortexResult;
146
147    use super::Reader;
148    use crate::IntoArray;
149    use crate::array_session;
150    use crate::arrays::PrimitiveArray;
151    use crate::executor::VortexSessionExecute;
152    use crate::search_sorted::SearchResult;
153    use crate::search_sorted::SearchSorted;
154    use crate::search_sorted::SearchSortedPrimitiveArray;
155    use crate::search_sorted::SearchSortedSide;
156    use crate::validity::Validity;
157
158    #[test]
159    fn search_sorted_reads_canonical_values_directly() -> VortexResult<()> {
160        let mut ctx = array_session().create_execution_ctx();
161        let array = PrimitiveArray::new(buffer![1i32, 2, 3, 4], Validity::NonNullable).into_array();
162        let searcher = SearchSortedPrimitiveArray::<i32>::new(&array, &mut ctx);
163        assert!(matches!(searcher.reader, Reader::Values(_)));
164        assert_eq!(
165            searcher.search_sorted(&3i32, SearchSortedSide::Left)?,
166            SearchResult::Found(2)
167        );
168        Ok(())
169    }
170
171    #[test]
172    fn search_sorted_nullable_values_respects_validity() -> VortexResult<()> {
173        let mut ctx = array_session().create_execution_ctx();
174        let array = PrimitiveArray::new(
175            buffer![99i32, 2, 3, 4],
176            Validity::from_iter([false, true, true, true]),
177        )
178        .into_array();
179        let searcher = SearchSortedPrimitiveArray::<i32>::new(&array, &mut ctx);
180        assert_eq!(
181            searcher.search_sorted(&None::<i32>, SearchSortedSide::Left)?,
182            SearchResult::Found(0)
183        );
184        assert_eq!(
185            searcher.search_sorted(&Some(3i32), SearchSortedSide::Left)?,
186            SearchResult::Found(2)
187        );
188        Ok(())
189    }
190
191    #[test]
192    fn search_sorted_optional_value() -> VortexResult<()> {
193        let array = PrimitiveArray::new(buffer![1i32, 2, 3, 4], Validity::AllValid).into_array();
194        let mut ctx = array_session().create_execution_ctx();
195        let res = SearchSortedPrimitiveArray::<i32>::new(&array, &mut ctx)
196            .search_sorted(&Some(3i32), SearchSortedSide::Left)?;
197        assert_eq!(res, SearchResult::Found(2));
198        Ok(())
199    }
200
201    #[test]
202    fn search_sorted_optional_value_with_nulls() -> VortexResult<()> {
203        let array =
204            PrimitiveArray::from_option_iter([None, None, Some(2i32), Some(3)]).into_array();
205        let mut ctx = array_session().create_execution_ctx();
206        let searcher = SearchSortedPrimitiveArray::<i32>::new(&array, &mut ctx);
207        assert_eq!(
208            searcher.search_sorted(&Some(2i32), SearchSortedSide::Left)?,
209            SearchResult::Found(2)
210        );
211        assert_eq!(
212            searcher.search_sorted(&None, SearchSortedSide::Right)?,
213            SearchResult::Found(2)
214        );
215        Ok(())
216    }
217}