Skip to content

Commit 12082ae

Browse files
robert3005claude
andcommitted
Address review findings on SearchSortedPrimitiveArray
- Fix RefCell double-borrow panic in IndexOrd<Option<T>>::index_cmp: the RefMut guard from is_valid was still alive when value() re-borrowed the ctx, so searching any valid element with an optional needle panicked with BorrowMutError. Add regression tests covering Option<T> searches with and without nulls. - Stop swallowing the underlying error in Patches search_index_binary_search_scalar; the map_err replaced real execution errors with a misleading "indices must be a primitive array" message. - Pass the in-scope ExecutionCtx to find_physical_index in RunEnd scalar_at and slice instead of spinning up a fresh LEGACY_SESSION ctx per call. - Add doc comments to the new public SearchSortedPrimitiveArray API and document the null-as-zero semantics of plain T searches. - Restore VortexResult<()> + ? style in test_search_sorted_primitive. Signed-off-by: "Robert Kruszewski" <robert@spiraldb.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 9a5accc commit 12082ae

5 files changed

Lines changed: 63 additions & 17 deletions

File tree

encodings/runend/src/kernel.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use vortex_session::VortexSession;
2525
use crate::RunEnd;
2626
use crate::array::RunEndArrayExt;
2727
use crate::compute::take_from::RunEndTakeFrom;
28+
use crate::ops::find_physical_index;
2829
use crate::ops::find_slice_end_index;
2930

3031
pub(super) fn initialize(session: &VortexSession) {
@@ -64,7 +65,7 @@ fn slice(
6465
) -> VortexResult<ArrayRef> {
6566
let new_length = range.len();
6667

67-
let slice_begin = array.find_physical_index(range.start)?;
68+
let slice_begin = find_physical_index(array.ends(), range.start + array.offset(), ctx)?;
6869
let slice_end = find_slice_end_index(array.ends(), range.end + array.offset(), ctx)?;
6970

7071
// If the sliced range contains only a single run, opt to return a ConstantArray.

encodings/runend/src/ops.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,8 @@ impl OperationsVTable<RunEnd> for RunEnd {
2222
index: usize,
2323
ctx: &mut ExecutionCtx,
2424
) -> VortexResult<Scalar> {
25-
array
26-
.values()
27-
.execute_scalar(array.find_physical_index(index)?, ctx)
25+
let physical_index = find_physical_index(array.ends(), index + array.offset(), ctx)?;
26+
array.values().execute_scalar(physical_index, ctx)
2827
}
2928
}
3029

vortex-array/src/arrays/primitive/tests.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
use std::sync::LazyLock;
55

66
use vortex_buffer::buffer;
7-
use vortex_error::VortexExpect;
87
use vortex_session::VortexSession;
98

109
use crate::ArrayRef;
@@ -32,11 +31,11 @@ fn test_search_sorted_primitive(
3231
#[case] value: i32,
3332
#[case] side: SearchSortedSide,
3433
#[case] expected: SearchResult,
35-
) {
34+
) -> vortex_error::VortexResult<()> {
3635
let res = SearchSortedPrimitiveArray::<i32>::new(&array, &mut SESSION.create_execution_ctx())
37-
.search_sorted(&value, side)
38-
.vortex_expect("Failed to search sorted");
36+
.search_sorted(&value, side)?;
3937
assert_eq!(res, expected);
38+
Ok(())
4039
}
4140

4241
#[test]

vortex-array/src/patches.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1009,7 +1009,6 @@ fn search_index_binary_search_scalar(
10091009
match_each_unsigned_integer_ptype!(indices.dtype().as_ptype(), |T| {
10101010
SearchSortedPrimitiveArray::<T>::new(indices, &mut LEGACY_SESSION.create_execution_ctx())
10111011
.search_sorted(&needle, SearchSortedSide::Left)
1012-
.map_err(|_| vortex_err!("indices must be a primitive array"))
10131012
})
10141013
}
10151014

vortex-array/src/search_sorted/primitive.rs

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,21 @@ use crate::ExecutionCtx;
1212
use crate::dtype::NativePType;
1313
use crate::search_sorted::IndexOrd;
1414

15+
/// A [`SearchSorted`](crate::search_sorted::SearchSorted) adapter over a sorted primitive-typed
16+
/// array, comparing elements as the native type `T`.
17+
///
18+
/// Values can be searched as `T`, `Option<T>`, or `usize`. Searching as `T` or `usize` treats
19+
/// null elements as `T::zero()`; use `Option<T>` when the array may contain nulls, in which case
20+
/// nulls sort before all non-null values.
1521
pub struct SearchSortedPrimitiveArray<'a, T>(
1622
&'a ArrayRef,
1723
RefCell<&'a mut ExecutionCtx>,
1824
PhantomData<T>,
1925
);
2026

2127
impl<'a, T: NativePType> SearchSortedPrimitiveArray<'a, T> {
28+
/// Wraps `array` for searching, panicking if the array's [`PType`](crate::dtype::PType) is
29+
/// not `T::PTYPE`.
2230
pub fn new(array: &'a ArrayRef, ctx: &'a mut ExecutionCtx) -> Self {
2331
assert_eq!(
2432
array.dtype().as_ptype(),
@@ -28,11 +36,11 @@ impl<'a, T: NativePType> SearchSortedPrimitiveArray<'a, T> {
2836
Self(array, RefCell::new(ctx), PhantomData)
2937
}
3038

39+
/// Returns the value at `idx`, with nulls mapped to `T::zero()`.
3140
fn value(&self, idx: usize) -> VortexResult<T> {
32-
let ctx_mut = &mut self.1.borrow_mut();
3341
Ok(self
3442
.0
35-
.execute_scalar(idx, ctx_mut)?
43+
.execute_scalar(idx, &mut self.1.borrow_mut())?
3644
.as_primitive()
3745
.typed_value::<T>()
3846
.unwrap_or_else(|| T::zero()))
@@ -52,12 +60,9 @@ impl<T: NativePType> IndexOrd<T> for SearchSortedPrimitiveArray<'_, T> {
5260

5361
impl<T: NativePType> IndexOrd<Option<T>> for SearchSortedPrimitiveArray<'_, T> {
5462
fn index_cmp(&self, idx: usize, elem: &Option<T>) -> VortexResult<Option<Ordering>> {
55-
let ctx_mut = &mut self.1.borrow_mut();
56-
let value = self
57-
.0
58-
.is_valid(idx, ctx_mut)?
59-
.then(|| self.value(idx))
60-
.transpose()?;
63+
// The borrow must end before `self.value` re-borrows the ctx.
64+
let valid = self.0.is_valid(idx, &mut self.1.borrow_mut())?;
65+
let value = valid.then(|| self.value(idx)).transpose()?;
6166

6267
Ok(match (value, elem.as_ref()) {
6368
(Some(l), Some(r)) => Some(l.total_compare(*r)),
@@ -87,3 +92,46 @@ impl<T: NativePType> IndexOrd<usize> for SearchSortedPrimitiveArray<'_, T> {
8792
self.0.len()
8893
}
8994
}
95+
96+
#[cfg(test)]
97+
mod tests {
98+
use vortex_buffer::buffer;
99+
use vortex_error::VortexResult;
100+
101+
use crate::IntoArray;
102+
use crate::array_session;
103+
use crate::arrays::PrimitiveArray;
104+
use crate::executor::VortexSessionExecute;
105+
use crate::search_sorted::SearchResult;
106+
use crate::search_sorted::SearchSorted;
107+
use crate::search_sorted::SearchSortedPrimitiveArray;
108+
use crate::search_sorted::SearchSortedSide;
109+
use crate::validity::Validity;
110+
111+
#[test]
112+
fn search_sorted_optional_value() -> VortexResult<()> {
113+
let array = PrimitiveArray::new(buffer![1i32, 2, 3, 4], Validity::AllValid).into_array();
114+
let mut ctx = array_session().create_execution_ctx();
115+
let res = SearchSortedPrimitiveArray::<i32>::new(&array, &mut ctx)
116+
.search_sorted(&Some(3i32), SearchSortedSide::Left)?;
117+
assert_eq!(res, SearchResult::Found(2));
118+
Ok(())
119+
}
120+
121+
#[test]
122+
fn search_sorted_optional_value_with_nulls() -> VortexResult<()> {
123+
let array =
124+
PrimitiveArray::from_option_iter([None, None, Some(2i32), Some(3)]).into_array();
125+
let mut ctx = array_session().create_execution_ctx();
126+
let searcher = SearchSortedPrimitiveArray::<i32>::new(&array, &mut ctx);
127+
assert_eq!(
128+
searcher.search_sorted(&Some(2i32), SearchSortedSide::Left)?,
129+
SearchResult::Found(2)
130+
);
131+
assert_eq!(
132+
searcher.search_sorted(&None, SearchSortedSide::Right)?,
133+
SearchResult::Found(2)
134+
);
135+
Ok(())
136+
}
137+
}

0 commit comments

Comments
 (0)