Skip to content

Commit 3edf6c8

Browse files
committed
Add PiecewiseSequential index array
Signed-off-by: Daniel King <dan@spiraldb.com>
1 parent 17f843a commit 3edf6c8

5 files changed

Lines changed: 446 additions & 0 deletions

File tree

vortex-array/src/arrays/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ pub mod patched;
8888
pub use patched::Patched;
8989
pub use patched::PatchedArray;
9090

91+
pub mod piecewise_sequential;
92+
pub use piecewise_sequential::PiecewiseSequential;
93+
pub use piecewise_sequential::PiecewiseSequentialArray;
94+
9195
pub mod primitive;
9296
pub use primitive::Primitive;
9397
pub use primitive::PrimitiveArray;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use smallvec::smallvec;
5+
use vortex_error::VortexResult;
6+
7+
use crate::ArrayRef;
8+
use crate::array::Array;
9+
use crate::array::ArrayParts;
10+
use crate::array::EmptyArrayData;
11+
use crate::array::TypedArrayRef;
12+
use crate::array_slots;
13+
use crate::arrays::PiecewiseSequential;
14+
use crate::dtype::PType;
15+
16+
#[array_slots(PiecewiseSequential)]
17+
pub struct PiecewiseSequentialSlots {
18+
/// The inclusive start index of each sequential piece.
19+
pub starts: ArrayRef,
20+
/// The length of each sequential piece.
21+
pub lengths: ArrayRef,
22+
}
23+
24+
/// Extension methods for [`PiecewiseSequentialArray`].
25+
pub trait PiecewiseSequentialArrayExt:
26+
TypedArrayRef<PiecewiseSequential> + PiecewiseSequentialArraySlotsExt
27+
{
28+
}
29+
impl<T: TypedArrayRef<PiecewiseSequential>> PiecewiseSequentialArrayExt for T {}
30+
31+
impl Array<PiecewiseSequential> {
32+
/// Constructs a new `PiecewiseSequentialArray` from start and length arrays.
33+
///
34+
/// This validates only structural invariants: both children must be non-nullable unsigned
35+
/// integer arrays with matching lengths, and the outer array length is the declared expanded
36+
/// index length. Individual ranges are checked when the index array is executed or consumed by
37+
/// a take implementation.
38+
pub fn try_new(starts: ArrayRef, lengths: ArrayRef, len: usize) -> VortexResult<Self> {
39+
Array::try_from_parts(
40+
ArrayParts::new(PiecewiseSequential, PType::U64.into(), len, EmptyArrayData)
41+
.with_slots(smallvec![Some(starts), Some(lengths)]),
42+
)
43+
}
44+
45+
/// Constructs a new `PiecewiseSequentialArray` without validation.
46+
///
47+
/// # Safety
48+
///
49+
/// The caller must guarantee the same structural invariants as [`Self::try_new`].
50+
pub unsafe fn new_unchecked(starts: ArrayRef, lengths: ArrayRef, len: usize) -> Self {
51+
unsafe {
52+
Array::from_parts_unchecked(
53+
ArrayParts::new(PiecewiseSequential, PType::U64.into(), len, EmptyArrayData)
54+
.with_slots(smallvec![Some(starts), Some(lengths)]),
55+
)
56+
}
57+
}
58+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Index encoding for concatenated sequential ranges.
5+
//!
6+
//! A `PiecewiseSequentialArray` represents the expanded index sequence
7+
//! `starts[i]..starts[i] + lengths[i]` for each piece `i`. It is intended for take operations that
8+
//! can gather contiguous runs without materializing one index per element.
9+
10+
use itertools::Itertools;
11+
use num_traits::AsPrimitive;
12+
use vortex_buffer::BufferMut;
13+
use vortex_error::VortexResult;
14+
use vortex_error::vortex_bail;
15+
use vortex_error::vortex_ensure;
16+
17+
use crate::ArrayRef;
18+
use crate::arrays::PrimitiveArray;
19+
use crate::dtype::UnsignedPType;
20+
21+
pub mod array;
22+
mod vtable;
23+
24+
#[cfg(test)]
25+
mod tests;
26+
27+
pub use array::PiecewiseSequentialArrayExt;
28+
pub use vtable::*;
29+
30+
pub(crate) fn check_index_arrays(starts: &ArrayRef, lengths: &ArrayRef) -> VortexResult<()> {
31+
check_index_array("starts", starts)?;
32+
check_index_array("lengths", lengths)?;
33+
vortex_ensure!(
34+
starts.len() == lengths.len(),
35+
"PiecewiseSequentialArray starts length {} does not match lengths length {}",
36+
starts.len(),
37+
lengths.len()
38+
);
39+
Ok(())
40+
}
41+
42+
fn check_index_array(name: &str, array: &ArrayRef) -> VortexResult<()> {
43+
vortex_ensure!(
44+
array.dtype().is_unsigned_int(),
45+
"PiecewiseSequentialArray {name} must have unsigned integer dtype, got {}",
46+
array.dtype()
47+
);
48+
vortex_ensure!(
49+
!array.dtype().is_nullable(),
50+
"PiecewiseSequentialArray {name} must be non-nullable, got {}",
51+
array.dtype()
52+
);
53+
Ok(())
54+
}
55+
56+
#[inline]
57+
pub(crate) fn index_value_to_usize<T: UnsignedPType>(value: T) -> usize {
58+
value.as_()
59+
}
60+
61+
#[inline]
62+
pub(crate) fn index_value_to_u64<T: UnsignedPType + AsPrimitive<u64>>(value: T) -> u64 {
63+
value.as_()
64+
}
65+
66+
#[inline]
67+
pub(crate) fn checked_range_end(start: u64, length: usize) -> VortexResult<u64> {
68+
start
69+
.checked_add(length as u64)
70+
.ok_or_else(|| vortex_error::vortex_err!("PiecewiseSequentialArray range overflows u64"))
71+
}
72+
73+
pub(crate) fn materialize_ranges<S, L>(
74+
starts: &PrimitiveArray,
75+
lengths: &PrimitiveArray,
76+
output_len: usize,
77+
) -> VortexResult<BufferMut<u64>>
78+
where
79+
S: UnsignedPType + AsPrimitive<u64>,
80+
L: UnsignedPType,
81+
{
82+
let starts = starts.as_slice::<S>();
83+
let lengths = lengths.as_slice::<L>();
84+
let mut values = BufferMut::with_capacity(output_len);
85+
let mut computed_len = 0usize;
86+
87+
for (&start, &length) in starts.iter().zip_eq(lengths) {
88+
let start = index_value_to_u64(start);
89+
let length = index_value_to_usize(length);
90+
checked_range_end(start, length)?;
91+
computed_len = computed_len.checked_add(length).ok_or_else(|| {
92+
vortex_error::vortex_err!("PiecewiseSequentialArray output length overflows usize")
93+
})?;
94+
95+
values.extend((0..length).map(|offset| start + offset as u64));
96+
}
97+
98+
if computed_len != output_len {
99+
vortex_bail!(
100+
"PiecewiseSequentialArray expanded length {computed_len} does not match declared length {output_len}"
101+
);
102+
}
103+
Ok(values)
104+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use vortex_buffer::buffer;
5+
use vortex_error::VortexResult;
6+
7+
use crate::IntoArray;
8+
use crate::VortexSessionExecute;
9+
use crate::array_session;
10+
use crate::arrays::ConstantArray;
11+
use crate::arrays::PiecewiseSequentialArray;
12+
use crate::arrays::PrimitiveArray;
13+
use crate::assert_arrays_eq;
14+
15+
#[test]
16+
fn materializes_piecewise_indices() -> VortexResult<()> {
17+
let starts = buffer![3u64, 15, 21].into_array();
18+
let lengths = buffer![3u64, 3, 3].into_array();
19+
let array = PiecewiseSequentialArray::try_new(starts, lengths, 9)?.into_array();
20+
21+
let expected = PrimitiveArray::from_iter([3u64, 4, 5, 15, 16, 17, 21, 22, 23]).into_array();
22+
assert_arrays_eq!(array, expected, &mut array_session().create_execution_ctx());
23+
Ok(())
24+
}
25+
26+
#[test]
27+
fn materializes_repeated_and_empty_ranges() -> VortexResult<()> {
28+
let starts = buffer![5u64, 2, 5].into_array();
29+
let lengths = buffer![2u64, 0, 2].into_array();
30+
let array = PiecewiseSequentialArray::try_new(starts, lengths, 4)?.into_array();
31+
32+
let expected = PrimitiveArray::from_iter([5u64, 6, 5, 6]).into_array();
33+
assert_arrays_eq!(array, expected, &mut array_session().create_execution_ctx());
34+
Ok(())
35+
}
36+
37+
#[test]
38+
fn supports_constant_lengths() -> VortexResult<()> {
39+
let starts = buffer![0u64, 10, 20].into_array();
40+
let lengths = ConstantArray::new(2u64, 3).into_array();
41+
let array = PiecewiseSequentialArray::try_new(starts, lengths, 6)?.into_array();
42+
43+
let expected = PrimitiveArray::from_iter([0u64, 1, 10, 11, 20, 21]).into_array();
44+
assert_arrays_eq!(array, expected, &mut array_session().create_execution_ctx());
45+
Ok(())
46+
}
47+
48+
#[test]
49+
fn scalar_at_maps_into_piece() -> VortexResult<()> {
50+
let starts = buffer![3u64, 15, 21].into_array();
51+
let lengths = buffer![3u64, 3, 3].into_array();
52+
let array = PiecewiseSequentialArray::try_new(starts, lengths, 9)?.into_array();
53+
let mut ctx = array_session().create_execution_ctx();
54+
55+
assert_eq!(array.execute_scalar(0, &mut ctx)?, 3u64.into());
56+
assert_eq!(array.execute_scalar(4, &mut ctx)?, 16u64.into());
57+
assert_eq!(array.execute_scalar(8, &mut ctx)?, 23u64.into());
58+
Ok(())
59+
}
60+
61+
#[test]
62+
fn constructor_defers_range_value_validation() -> VortexResult<()> {
63+
let starts = buffer![u64::MAX].into_array();
64+
let lengths = buffer![2u64].into_array();
65+
let array = PiecewiseSequentialArray::try_new(starts, lengths, 2)?.into_array();
66+
67+
assert!(
68+
array
69+
.execute::<PrimitiveArray>(&mut array_session().create_execution_ctx())
70+
.is_err()
71+
);
72+
Ok(())
73+
}
74+
75+
#[test]
76+
fn execution_checks_declared_length() -> VortexResult<()> {
77+
let starts = buffer![0u64, 3].into_array();
78+
let lengths = buffer![2u64, 2].into_array();
79+
let array = PiecewiseSequentialArray::try_new(starts, lengths, 3)?.into_array();
80+
81+
assert!(
82+
array
83+
.execute::<PrimitiveArray>(&mut array_session().create_execution_ctx())
84+
.is_err()
85+
);
86+
Ok(())
87+
}

0 commit comments

Comments
 (0)