Skip to content

Commit 0bd5b74

Browse files
committed
Reject negative signed FSL take indices
Signed-off-by: Daniel King <dan@spiraldb.com>
1 parent 7f870c0 commit 0bd5b74

2 files changed

Lines changed: 99 additions & 28 deletions

File tree

vortex-array/src/arrays/fixed_size_list/compute/take.rs

Lines changed: 69 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@ use crate::arrays::dict::TakeExecute;
2323
use crate::arrays::fixed_size_list::FixedSizeListArrayExt;
2424
use crate::arrays::primitive::PrimitiveArrayExt;
2525
use crate::builders::builder_with_capacity;
26+
use crate::builtins::ArrayBuiltins;
27+
use crate::dtype::DType;
2628
use crate::dtype::IntegerPType;
2729
use crate::executor::ExecutionCtx;
2830
use crate::match_each_unsigned_integer_ptype;
31+
use crate::scalar::Scalar;
2932
use crate::validity::Validity;
3033

3134
/// Take implementation for [`FixedSizeListArray`].
@@ -43,40 +46,86 @@ impl TakeExecute for FixedSizeList {
4346
return take_empty_fsl(array, indices, ctx).map(Some);
4447
}
4548

46-
let indices_ptype = indices.dtype().as_ptype().to_unsigned();
47-
match_each_unsigned_integer_ptype!(indices_ptype, |I| {
48-
take_non_empty_with_indices::<I>(array, indices, ctx)
49-
})
50-
.map(Some)
49+
take_non_empty_fsl(array, indices, ctx).map(Some)
5150
}
5251
}
5352

53+
fn take_non_empty_fsl(
54+
array: ArrayView<'_, FixedSizeList>,
55+
indices: &ArrayRef,
56+
ctx: &mut ExecutionCtx,
57+
) -> VortexResult<ArrayRef> {
58+
debug_assert!(!array.is_empty());
59+
60+
let DType::Primitive(ptype, nullability) = indices.dtype() else {
61+
vortex_bail!("Invalid indices dtype: {}", indices.dtype())
62+
};
63+
if !ptype.is_int() {
64+
vortex_bail!("Invalid indices dtype: {}", indices.dtype());
65+
}
66+
67+
let indices_validity = indices.validity()?;
68+
let indices_validity_mask = indices_validity.execute_mask(indices.len(), ctx)?;
69+
// Null index lanes are semantically ignored. Zero them before checked signed-to-unsigned casts
70+
// so a negative physical payload in a null lane does not fail the take.
71+
let indices_nulls_zeroed = if indices_validity_mask.all_true() {
72+
indices.clone()
73+
} else {
74+
indices
75+
.clone()
76+
.fill_null(Scalar::from(0).cast(indices.dtype())?)?
77+
};
78+
79+
let unsigned_indices = if ptype.is_unsigned_int() {
80+
indices_nulls_zeroed.execute::<PrimitiveArray>(ctx)?
81+
} else {
82+
indices_nulls_zeroed
83+
.cast(DType::Primitive(ptype.to_unsigned(), *nullability))?
84+
.execute::<PrimitiveArray>(ctx)?
85+
};
86+
87+
match_each_unsigned_integer_ptype!(unsigned_indices.ptype(), |I| {
88+
take_non_empty_with_indices::<I>(
89+
array,
90+
indices,
91+
unsigned_indices.as_view(),
92+
&indices_validity_mask,
93+
nullability.is_nullable(),
94+
ctx,
95+
)
96+
})
97+
}
98+
5499
fn take_non_empty_with_indices<I: IntegerPType>(
55100
array: ArrayView<'_, FixedSizeList>,
56101
indices: &ArrayRef,
102+
indices_array: ArrayView<'_, Primitive>,
103+
indices_validity: &Mask,
104+
indices_are_nullable: bool,
57105
ctx: &mut ExecutionCtx,
58106
) -> VortexResult<ArrayRef> {
59107
debug_assert!(!array.is_empty());
60108

61109
let list_size = array.list_size() as usize;
62110

63-
let indices_array = indices.clone().execute::<PrimitiveArray>(ctx)?;
64-
// Bit-identical reinterpret so `as_slice::<I>` (with unsigned `I`) matches. Negative signed
65-
// inputs become large unsigned values and are rejected by the bounds check below.
66-
let indices_array = indices_array.reinterpret_cast(indices_array.ptype().to_unsigned());
67-
68111
if list_size == 0 {
69-
return take_non_empty_degenerate_fsl::<I>(array, indices, indices_array.as_view(), ctx);
112+
return take_non_empty_degenerate_fsl::<I>(array, indices, indices_array, indices_validity);
70113
}
71114

72-
take_non_empty_non_degenerate_fsl::<I>(array, indices_array.as_view(), ctx)
115+
take_non_empty_non_degenerate_fsl::<I>(
116+
array,
117+
indices_array,
118+
indices_validity,
119+
indices_are_nullable,
120+
ctx,
121+
)
73122
}
74123

75124
fn take_non_empty_degenerate_fsl<I: IntegerPType>(
76125
array: ArrayView<'_, FixedSizeList>,
77126
indices: &ArrayRef,
78127
indices_array: ArrayView<'_, Primitive>,
79-
ctx: &mut ExecutionCtx,
128+
indices_validity: &Mask,
80129
) -> VortexResult<ArrayRef> {
81130
debug_assert!(!array.is_empty());
82131
debug_assert_eq!(array.list_size(), 0);
@@ -85,7 +134,7 @@ fn take_non_empty_degenerate_fsl<I: IntegerPType>(
85134
"degenerate list must have empty elements"
86135
);
87136

88-
validate_valid_indices::<I>(&indices_array, array.as_ref().len(), ctx)?;
137+
validate_valid_indices::<I>(&indices_array, indices_validity, array.as_ref().len())?;
89138
let new_validity = array.validity()?.take(indices)?;
90139
let new_len = indices_array.len();
91140

@@ -139,13 +188,15 @@ fn take_empty_fsl(
139188
fn take_non_empty_non_degenerate_fsl<I: IntegerPType>(
140189
array: ArrayView<'_, FixedSizeList>,
141190
indices_array: ArrayView<'_, Primitive>,
191+
indices_validity: &Mask,
192+
indices_are_nullable: bool,
142193
ctx: &mut ExecutionCtx,
143194
) -> VortexResult<ArrayRef> {
144195
debug_assert!(!array.is_empty());
145196
debug_assert_ne!(array.list_size(), 0);
146197

147-
if array.dtype().is_nullable() || indices_array.dtype().is_nullable() {
148-
take_nullable_non_empty_fsl::<I>(array, indices_array, ctx)
198+
if array.dtype().is_nullable() || indices_are_nullable {
199+
take_nullable_non_empty_fsl::<I>(array, indices_array, indices_validity, ctx)
149200
} else {
150201
take_non_nullable_non_empty_fsl::<I>(array, indices_array)
151202
}
@@ -195,6 +246,7 @@ fn take_non_nullable_non_empty_fsl<I: IntegerPType>(
195246
fn take_nullable_non_empty_fsl<I: IntegerPType>(
196247
array: ArrayView<'_, FixedSizeList>,
197248
indices_array: ArrayView<'_, Primitive>,
249+
indices_validity: &Mask,
198250
ctx: &mut ExecutionCtx,
199251
) -> VortexResult<ArrayRef> {
200252
debug_assert!(!array.is_empty());
@@ -209,7 +261,6 @@ fn take_nullable_non_empty_fsl<I: IntegerPType>(
209261
let array_validity = array
210262
.fixed_size_list_validity()
211263
.execute_mask(array.as_ref().len(), ctx)?;
212-
let indices_validity = indices_validity_mask(&indices_array, ctx)?;
213264

214265
let mut starts = BufferMut::<u64>::with_capacity(new_len);
215266
let mut new_validity_builder = BitBufferMut::with_capacity(new_len);
@@ -254,22 +305,12 @@ fn take_nullable_non_empty_fsl<I: IntegerPType>(
254305
.into_array())
255306
}
256307

257-
fn indices_validity_mask(
258-
indices_array: &ArrayView<'_, Primitive>,
259-
ctx: &mut ExecutionCtx,
260-
) -> VortexResult<Mask> {
261-
indices_array
262-
.validity()?
263-
.execute_mask(indices_array.as_ref().len(), ctx)
264-
}
265-
266308
fn validate_valid_indices<I: IntegerPType>(
267309
indices_array: &ArrayView<'_, Primitive>,
310+
indices_validity: &Mask,
268311
array_len: usize,
269-
ctx: &mut ExecutionCtx,
270312
) -> VortexResult<()> {
271313
let indices: &[I] = indices_array.as_slice::<I>();
272-
let indices_validity = indices_validity_mask(indices_array, ctx)?;
273314

274315
for (&data_idx, is_index_valid) in indices.iter().zip(indices_validity.iter()) {
275316
if is_index_valid {

vortex-array/src/arrays/fixed_size_list/tests/take.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,36 @@ fn test_take_nullable_indices_ignores_out_of_bounds_null_value() {
307307
assert_arrays_eq!(expected, result, &mut ctx);
308308
}
309309

310+
#[test]
311+
fn test_take_rejects_negative_signed_index() -> VortexResult<()> {
312+
let mut ctx = array_session().create_execution_ctx();
313+
let elements = buffer![1i32, 2, 3, 4].into_array();
314+
let fsl = FixedSizeListArray::new(elements.into_array(), 2, Validity::NonNullable, 2);
315+
let indices = buffer![-1i32].into_array();
316+
317+
let result = <FixedSizeList as TakeExecute>::take(fsl.as_view(), &indices, &mut ctx);
318+
319+
assert!(result.is_err());
320+
Ok(())
321+
}
322+
323+
#[test]
324+
fn test_take_nullable_indices_ignores_negative_null_value() -> VortexResult<()> {
325+
let mut ctx = array_session().create_execution_ctx();
326+
let elements = buffer![1i32, 2, 3, 4].into_array();
327+
let fsl = FixedSizeListArray::new(elements.into_array(), 2, Validity::NonNullable, 2);
328+
let indices =
329+
PrimitiveArray::new(buffer![-1i32, 1], Validity::from_iter([false, true])).into_array();
330+
331+
let result = <FixedSizeList as TakeExecute>::take(fsl.as_view(), &indices, &mut ctx)?
332+
.ok_or_else(|| vortex_err!("FixedSizeList TakeExecute returned no result"))?;
333+
334+
assert_eq!(result.len(), 2);
335+
assert!(result.execute_scalar(0, &mut ctx)?.is_null());
336+
assert!(!result.execute_scalar(1, &mut ctx)?.is_null());
337+
Ok(())
338+
}
339+
310340
#[test]
311341
fn test_take_rejects_overflowing_valid_index() {
312342
let mut ctx = array_session().create_execution_ctx();

0 commit comments

Comments
 (0)