Skip to content

Commit c5e075a

Browse files
authored
Add PiecewiseSequence index array (#8799)
This array allows FixedSizeList, List, and ListView to implement take without materializing a dense array of indices of size `O(N_TAKE_INDICES * LIST_SIZE)`. Their elements child arrays can, in turn, use efficient memcpy calls instead of copying individual indices. ## Summary Adds `PiecewiseSequenceArray`, a serializable index encoding for piecewise arithmetic sequences: `starts[i] + j * multipliers[i]` for `j` in `0..lengths[i]`. This PR only introduces the index array and its execution/scalar behavior. It intentionally does not specialize `take`; dependent PRs consume this encoding for the constant-multiplier-1 contiguous case. ## Validation - `cargo fmt -p vortex-array` - `cargo test -p vortex-array piecewise_sequence` --------- Signed-off-by: Daniel King <dan@spiraldb.com>
1 parent e6eeaae commit c5e075a

6 files changed

Lines changed: 630 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_sequence;
92+
pub use piecewise_sequence::PiecewiseSequence;
93+
pub use piecewise_sequence::PiecewiseSequenceArray;
94+
9195
pub mod primitive;
9296
pub use primitive::Primitive;
9397
pub use primitive::PrimitiveArray;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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::PiecewiseSequence;
14+
use crate::dtype::PType;
15+
16+
#[array_slots(PiecewiseSequence)]
17+
pub struct PiecewiseSequenceSlots {
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+
/// The distance between consecutive indices in each piece.
23+
pub multipliers: ArrayRef,
24+
}
25+
26+
/// Extension methods for [`Array`] values using the [`PiecewiseSequence`] encoding.
27+
pub trait PiecewiseSequenceArrayExt:
28+
TypedArrayRef<PiecewiseSequence> + PiecewiseSequenceArraySlotsExt
29+
{
30+
}
31+
impl<T: TypedArrayRef<PiecewiseSequence>> PiecewiseSequenceArrayExt for T {}
32+
33+
impl Array<PiecewiseSequence> {
34+
/// Constructs a new `PiecewiseSequenceArray` from start, length, and multiplier arrays.
35+
///
36+
/// This validates only structural invariants: all children must be non-nullable unsigned
37+
/// integer arrays with matching lengths, and the outer array length is the declared expanded
38+
/// index length. Individual ranges are checked when the index array is executed or consumed by
39+
/// a take implementation.
40+
pub fn try_new(
41+
starts: ArrayRef,
42+
lengths: ArrayRef,
43+
multipliers: ArrayRef,
44+
len: usize,
45+
) -> VortexResult<Self> {
46+
Array::try_from_parts(
47+
ArrayParts::new(PiecewiseSequence, PType::U64.into(), len, EmptyArrayData)
48+
.with_slots(smallvec![Some(starts), Some(lengths), Some(multipliers)]),
49+
)
50+
}
51+
52+
/// Constructs a new `PiecewiseSequenceArray` without validation.
53+
///
54+
/// # Safety
55+
///
56+
/// The caller must guarantee the same structural invariants as [`Self::try_new`].
57+
pub unsafe fn new_unchecked(
58+
starts: ArrayRef,
59+
lengths: ArrayRef,
60+
multipliers: ArrayRef,
61+
len: usize,
62+
) -> Self {
63+
unsafe {
64+
Array::from_parts_unchecked(
65+
ArrayParts::new(PiecewiseSequence, PType::U64.into(), len, EmptyArrayData)
66+
.with_slots(smallvec![Some(starts), Some(lengths), Some(multipliers)]),
67+
)
68+
}
69+
}
70+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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 `PiecewiseSequenceArray` represents the expanded index sequence
7+
//! `starts[i] + j * multipliers[i]` for `j` in `0..lengths[i]` for each piece `i`. It is
8+
//! intended for take operations that can gather regular runs without materializing one index per
9+
//! element.
10+
11+
use itertools::Itertools;
12+
use vortex_buffer::BufferMut;
13+
use vortex_error::VortexResult;
14+
use vortex_error::vortex_bail;
15+
use vortex_error::vortex_ensure;
16+
use vortex_error::vortex_err;
17+
18+
use crate::ArrayRef;
19+
use crate::array::ArrayView;
20+
use crate::arrays::PrimitiveArray;
21+
use crate::arrays::piecewise_sequence::array::PiecewiseSequenceArraySlotsExt;
22+
use crate::dtype::UnsignedPType;
23+
use crate::executor::ExecutionCtx;
24+
25+
pub mod array;
26+
mod vtable;
27+
28+
#[cfg(test)]
29+
mod tests;
30+
31+
pub use array::PiecewiseSequenceArrayExt;
32+
pub use vtable::*;
33+
34+
pub(crate) fn check_index_arrays(
35+
starts: &ArrayRef,
36+
lengths: &ArrayRef,
37+
multipliers: &ArrayRef,
38+
) -> VortexResult<()> {
39+
check_index_array("starts", starts)?;
40+
check_index_array("lengths", lengths)?;
41+
check_index_array("multipliers", multipliers)?;
42+
vortex_ensure!(
43+
starts.len() == lengths.len(),
44+
"PiecewiseSequenceArray starts length {} does not match lengths length {}",
45+
starts.len(),
46+
lengths.len()
47+
);
48+
vortex_ensure!(
49+
starts.len() == multipliers.len(),
50+
"PiecewiseSequenceArray starts length {} does not match multipliers length {}",
51+
starts.len(),
52+
multipliers.len()
53+
);
54+
Ok(())
55+
}
56+
57+
pub(crate) fn execute_index_arrays(
58+
array: ArrayView<'_, PiecewiseSequence>,
59+
ctx: &mut ExecutionCtx,
60+
) -> VortexResult<(PrimitiveArray, PrimitiveArray, PrimitiveArray)> {
61+
let starts = array.starts().clone().execute::<PrimitiveArray>(ctx)?;
62+
let lengths = array.lengths().clone().execute::<PrimitiveArray>(ctx)?;
63+
let multipliers = array.multipliers().clone().execute::<PrimitiveArray>(ctx)?;
64+
check_index_arrays(starts.as_ref(), lengths.as_ref(), multipliers.as_ref())?;
65+
Ok((starts, lengths, multipliers))
66+
}
67+
68+
fn check_index_array(name: &str, array: &ArrayRef) -> VortexResult<()> {
69+
vortex_ensure!(
70+
array.dtype().is_unsigned_int(),
71+
"PiecewiseSequenceArray {name} must have unsigned integer dtype, got {}",
72+
array.dtype()
73+
);
74+
vortex_ensure!(
75+
!array.dtype().is_nullable(),
76+
"PiecewiseSequenceArray {name} must be non-nullable, got {}",
77+
array.dtype()
78+
);
79+
Ok(())
80+
}
81+
82+
pub(crate) fn materialize_ranges<S, L, M>(
83+
starts: &PrimitiveArray,
84+
lengths: &PrimitiveArray,
85+
multipliers: &PrimitiveArray,
86+
output_len: usize,
87+
) -> VortexResult<BufferMut<u64>>
88+
where
89+
S: UnsignedPType,
90+
L: UnsignedPType,
91+
M: UnsignedPType,
92+
{
93+
let starts = starts.as_slice::<S>();
94+
let lengths = lengths.as_slice::<L>();
95+
let multipliers = multipliers.as_slice::<M>();
96+
let mut values = BufferMut::with_capacity(output_len);
97+
let mut computed_len = 0usize;
98+
99+
for ((&start, &length), &multiplier) in starts.iter().zip_eq(lengths).zip_eq(multipliers) {
100+
let start: usize = start.as_();
101+
let length: usize = length.as_();
102+
let multiplier: usize = multiplier.as_();
103+
if length != 0 {
104+
let last_offset = length - 1;
105+
let last_delta = last_offset
106+
.checked_mul(multiplier)
107+
.ok_or_else(|| vortex_err!("PiecewiseSequenceArray range overflows usize"))?;
108+
start
109+
.checked_add(last_delta)
110+
.ok_or_else(|| vortex_err!("PiecewiseSequenceArray range overflows usize"))?;
111+
}
112+
computed_len = computed_len
113+
.checked_add(length)
114+
.ok_or_else(|| vortex_err!("PiecewiseSequenceArray output length overflows usize"))?;
115+
116+
values.extend((0..length).map(|offset| (start + offset * multiplier) as u64));
117+
}
118+
119+
if computed_len != output_len {
120+
vortex_bail!(
121+
"PiecewiseSequenceArray expanded length {computed_len} does not match declared length {output_len}"
122+
);
123+
}
124+
Ok(values)
125+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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::PiecewiseSequence;
15+
use crate::arrays::PiecewiseSequenceArray;
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 multipliers = ConstantArray::new(1u64, 3).into_array();
26+
let array = PiecewiseSequenceArray::try_new(starts, lengths, multipliers, 9)?.into_array();
27+
28+
let expected = PrimitiveArray::from_iter([3u64, 4, 5, 15, 16, 17, 21, 22, 23]).into_array();
29+
assert_arrays_eq!(array, expected, &mut array_session().create_execution_ctx());
30+
Ok(())
31+
}
32+
33+
#[test]
34+
fn materializes_repeated_and_empty_ranges() -> VortexResult<()> {
35+
let starts = buffer![5u64, 2, 5].into_array();
36+
let lengths = buffer![2u64, 0, 2].into_array();
37+
let multipliers = ConstantArray::new(1u64, 3).into_array();
38+
let array = PiecewiseSequenceArray::try_new(starts, lengths, multipliers, 4)?.into_array();
39+
40+
let expected = PrimitiveArray::from_iter([5u64, 6, 5, 6]).into_array();
41+
assert_arrays_eq!(array, expected, &mut array_session().create_execution_ctx());
42+
Ok(())
43+
}
44+
45+
#[test]
46+
fn materializes_multiplied_ranges() -> VortexResult<()> {
47+
let starts = buffer![3u64, 15].into_array();
48+
let lengths = buffer![3u64, 2].into_array();
49+
let multipliers = buffer![2u64, 4].into_array();
50+
let array = PiecewiseSequenceArray::try_new(starts, lengths, multipliers, 5)?.into_array();
51+
52+
let expected = PrimitiveArray::from_iter([3u64, 5, 7, 15, 19]).into_array();
53+
assert_arrays_eq!(array, expected, &mut array_session().create_execution_ctx());
54+
Ok(())
55+
}
56+
57+
#[test]
58+
fn supports_constant_lengths() -> VortexResult<()> {
59+
let starts = buffer![0u64, 10, 20].into_array();
60+
let lengths = ConstantArray::new(2u64, 3).into_array();
61+
let multipliers = ConstantArray::new(1u64, 3).into_array();
62+
let array = PiecewiseSequenceArray::try_new(starts, lengths, multipliers, 6)?.into_array();
63+
64+
let expected = PrimitiveArray::from_iter([0u64, 1, 10, 11, 20, 21]).into_array();
65+
assert_arrays_eq!(array, expected, &mut array_session().create_execution_ctx());
66+
Ok(())
67+
}
68+
69+
#[test]
70+
fn scalar_at_maps_into_piece() -> VortexResult<()> {
71+
let starts = buffer![3u64, 15, 21].into_array();
72+
let lengths = buffer![3u64, 3, 3].into_array();
73+
let multipliers = buffer![1u64, 1, 1].into_array();
74+
let array = PiecewiseSequenceArray::try_new(starts, lengths, multipliers, 9)?.into_array();
75+
let mut ctx = array_session().create_execution_ctx();
76+
77+
assert_eq!(array.execute_scalar(0, &mut ctx)?, 3u64.into());
78+
assert_eq!(array.execute_scalar(4, &mut ctx)?, 16u64.into());
79+
assert_eq!(array.execute_scalar(8, &mut ctx)?, 23u64.into());
80+
Ok(())
81+
}
82+
83+
#[test]
84+
fn constructor_defers_range_value_validation() -> VortexResult<()> {
85+
let starts = buffer![u64::MAX].into_array();
86+
let lengths = buffer![2u64].into_array();
87+
let multipliers = buffer![1u64].into_array();
88+
let array = PiecewiseSequenceArray::try_new(starts, lengths, multipliers, 2)?.into_array();
89+
90+
let err = array
91+
.execute::<PrimitiveArray>(&mut array_session().create_execution_ctx())
92+
.unwrap_err();
93+
assert!(
94+
err.to_string()
95+
.contains("PiecewiseSequenceArray range overflows usize"),
96+
"{err}"
97+
);
98+
Ok(())
99+
}
100+
101+
#[test]
102+
fn execution_checks_declared_length() -> VortexResult<()> {
103+
let starts = buffer![0u64, 3].into_array();
104+
let lengths = buffer![2u64, 2].into_array();
105+
let multipliers = ConstantArray::new(1u64, 2).into_array();
106+
let array = PiecewiseSequenceArray::try_new(starts, lengths, multipliers, 3)?.into_array();
107+
108+
let err = array
109+
.execute::<PrimitiveArray>(&mut array_session().create_execution_ctx())
110+
.unwrap_err();
111+
assert!(
112+
err.to_string()
113+
.contains("PiecewiseSequenceArray expanded length 4 does not match declared length 3"),
114+
"{err}"
115+
);
116+
Ok(())
117+
}
118+
119+
#[test]
120+
fn serde_roundtrip_preserves_piecewise_indices() -> VortexResult<()> {
121+
let array = PiecewiseSequenceArray::try_new(
122+
buffer![3u32, 15, 21].into_array(),
123+
buffer![2u16, 0, 2].into_array(),
124+
buffer![2u8, 1, 3].into_array(),
125+
4,
126+
)?
127+
.into_array();
128+
let dtype = array.dtype().clone();
129+
let len = array.len();
130+
131+
let array_ctx = ArrayContext::empty();
132+
let serialized = array.serialize(&array_ctx, &array_session(), &SerializeOptions::default())?;
133+
134+
let mut concat = ByteBufferMut::empty();
135+
for buffer in serialized {
136+
concat.extend_from_slice(buffer.as_ref());
137+
}
138+
139+
let parts = SerializedArray::try_from(concat.freeze())?;
140+
let decoded = parts.decode(
141+
&dtype,
142+
len,
143+
&ReadContext::new(array_ctx.to_ids()),
144+
&array_session(),
145+
)?;
146+
147+
assert!(decoded.is::<PiecewiseSequence>());
148+
assert_arrays_eq!(
149+
decoded,
150+
PrimitiveArray::from_iter([3u64, 5, 21, 24]).into_array(),
151+
&mut array_session().create_execution_ctx()
152+
);
153+
Ok(())
154+
}

0 commit comments

Comments
 (0)