Skip to content

Commit 1748e31

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

6 files changed

Lines changed: 552 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: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use vortex_buffer::ByteBufferMut;
5+
use vortex_buffer::buffer;
6+
use vortex_error::VortexResult;
7+
use vortex_session::registry::ReadContext;
8+
9+
use crate::ArrayContext;
10+
use crate::IntoArray;
11+
use crate::VortexSessionExecute;
12+
use crate::array_session;
13+
use crate::arrays::ConstantArray;
14+
use crate::arrays::PiecewiseSequential;
15+
use crate::arrays::PiecewiseSequentialArray;
16+
use crate::arrays::PrimitiveArray;
17+
use crate::assert_arrays_eq;
18+
use crate::serde::SerializeOptions;
19+
use crate::serde::SerializedArray;
20+
21+
#[test]
22+
fn materializes_piecewise_indices() -> VortexResult<()> {
23+
let starts = buffer![3u64, 15, 21].into_array();
24+
let lengths = buffer![3u64, 3, 3].into_array();
25+
let array = PiecewiseSequentialArray::try_new(starts, lengths, 9)?.into_array();
26+
27+
let expected = PrimitiveArray::from_iter([3u64, 4, 5, 15, 16, 17, 21, 22, 23]).into_array();
28+
assert_arrays_eq!(array, expected, &mut array_session().create_execution_ctx());
29+
Ok(())
30+
}
31+
32+
#[test]
33+
fn materializes_repeated_and_empty_ranges() -> VortexResult<()> {
34+
let starts = buffer![5u64, 2, 5].into_array();
35+
let lengths = buffer![2u64, 0, 2].into_array();
36+
let array = PiecewiseSequentialArray::try_new(starts, lengths, 4)?.into_array();
37+
38+
let expected = PrimitiveArray::from_iter([5u64, 6, 5, 6]).into_array();
39+
assert_arrays_eq!(array, expected, &mut array_session().create_execution_ctx());
40+
Ok(())
41+
}
42+
43+
#[test]
44+
fn supports_constant_lengths() -> VortexResult<()> {
45+
let starts = buffer![0u64, 10, 20].into_array();
46+
let lengths = ConstantArray::new(2u64, 3).into_array();
47+
let array = PiecewiseSequentialArray::try_new(starts, lengths, 6)?.into_array();
48+
49+
let expected = PrimitiveArray::from_iter([0u64, 1, 10, 11, 20, 21]).into_array();
50+
assert_arrays_eq!(array, expected, &mut array_session().create_execution_ctx());
51+
Ok(())
52+
}
53+
54+
#[test]
55+
fn scalar_at_maps_into_piece() -> VortexResult<()> {
56+
let starts = buffer![3u64, 15, 21].into_array();
57+
let lengths = buffer![3u64, 3, 3].into_array();
58+
let array = PiecewiseSequentialArray::try_new(starts, lengths, 9)?.into_array();
59+
let mut ctx = array_session().create_execution_ctx();
60+
61+
assert_eq!(array.execute_scalar(0, &mut ctx)?, 3u64.into());
62+
assert_eq!(array.execute_scalar(4, &mut ctx)?, 16u64.into());
63+
assert_eq!(array.execute_scalar(8, &mut ctx)?, 23u64.into());
64+
Ok(())
65+
}
66+
67+
#[test]
68+
fn constructor_defers_range_value_validation() -> VortexResult<()> {
69+
let starts = buffer![u64::MAX].into_array();
70+
let lengths = buffer![2u64].into_array();
71+
let array = PiecewiseSequentialArray::try_new(starts, lengths, 2)?.into_array();
72+
73+
assert!(
74+
array
75+
.execute::<PrimitiveArray>(&mut array_session().create_execution_ctx())
76+
.is_err()
77+
);
78+
Ok(())
79+
}
80+
81+
#[test]
82+
fn execution_checks_declared_length() -> VortexResult<()> {
83+
let starts = buffer![0u64, 3].into_array();
84+
let lengths = buffer![2u64, 2].into_array();
85+
let array = PiecewiseSequentialArray::try_new(starts, lengths, 3)?.into_array();
86+
87+
assert!(
88+
array
89+
.execute::<PrimitiveArray>(&mut array_session().create_execution_ctx())
90+
.is_err()
91+
);
92+
Ok(())
93+
}
94+
95+
#[test]
96+
fn serde_roundtrip_preserves_piecewise_indices() -> VortexResult<()> {
97+
let array = PiecewiseSequentialArray::try_new(
98+
buffer![3u32, 15, 21].into_array(),
99+
buffer![2u16, 0, 2].into_array(),
100+
4,
101+
)?
102+
.into_array();
103+
let dtype = array.dtype().clone();
104+
let len = array.len();
105+
106+
let array_ctx = ArrayContext::empty();
107+
let serialized = array.serialize(&array_ctx, &array_session(), &SerializeOptions::default())?;
108+
109+
let mut concat = ByteBufferMut::empty();
110+
for buffer in serialized {
111+
concat.extend_from_slice(buffer.as_ref());
112+
}
113+
114+
let parts = SerializedArray::try_from(concat.freeze())?;
115+
let decoded = parts.decode(
116+
&dtype,
117+
len,
118+
&ReadContext::new(array_ctx.to_ids()),
119+
&array_session(),
120+
)?;
121+
122+
assert!(decoded.is::<PiecewiseSequential>());
123+
assert_arrays_eq!(
124+
decoded,
125+
PrimitiveArray::from_iter([3u64, 4, 21, 22]).into_array(),
126+
&mut array_session().create_execution_ctx()
127+
);
128+
Ok(())
129+
}

0 commit comments

Comments
 (0)