Skip to content

Commit 3f93504

Browse files
clauderobert3005
authored andcommitted
refactor(array): canonicalize chunked nested types through the builder
`pack_struct_chunks`, `swizzle_fixed_size_list_chunks` and `swizzle_list_chunks` existed because `append_to_builder` used to decode a builder's children: without them, canonicalizing a `ChunkedArray` would concatenate every chunk's children instead of reusing them. Their doc comments describe what the builders now do on their own, so all three are dead weight - the generic builder path produces the same swizzled array, with each chunk's children kept as chunks of the combined child. Only `Variant` still needs a hand-written pack, because there is no variant builder. This drops the one canonicalization path that allocated its buffers through the session allocator: `swizzle_list_chunks` allocated its offsets and sizes with `ctx.allocator()`, whereas `builder_with_capacity_in` still ignores the allocator it is handed, so chunked primitives, structs and FSLs already went around it. `list_canonicalize_uses_memory_session_allocator` guarded that one path and goes with it; restoring the property means teaching the builders to allocate through a `HostAllocatorRef`, not keeping a bespoke list swizzle alive. Signed-off-by: Claude <noreply@anthropic.com> Signed-off-by: Robert Kruszewski <robert@spiraldb.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 3019288 commit 3f93504

1 file changed

Lines changed: 54 additions & 231 deletions

File tree

vortex-array/src/arrays/chunked/vtable/canonical.rs

Lines changed: 54 additions & 231 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

44
use itertools::Itertools as _;
5-
use vortex_buffer::Buffer;
6-
use vortex_error::VortexExpect;
75
use vortex_error::VortexResult;
86
use vortex_error::vortex_ensure;
97
use vortex_error::vortex_err;
@@ -15,23 +13,11 @@ use crate::IntoArray;
1513
use crate::array::ArrayView;
1614
use crate::arrays::Chunked;
1715
use crate::arrays::ChunkedArray;
18-
use crate::arrays::FixedSizeListArray;
19-
use crate::arrays::ListViewArray;
20-
use crate::arrays::PrimitiveArray;
21-
use crate::arrays::StructArray;
2216
use crate::arrays::VariantArray;
2317
use crate::arrays::chunked::ChunkedArrayExt;
24-
use crate::arrays::fixed_size_list::FixedSizeListArraySlotsExt;
25-
use crate::arrays::listview::ListViewArraySlotsExt;
26-
use crate::arrays::listview::ListViewRebuildMode;
2718
use crate::arrays::variant::VariantArraySlotsExt;
2819
use crate::builders::builder_with_capacity_in;
29-
use crate::builtins::ArrayBuiltins;
3020
use crate::dtype::DType;
31-
use crate::dtype::Nullability;
32-
use crate::dtype::PType;
33-
use crate::memory::HostAllocatorExt;
34-
use crate::validity::Validity;
3521

3622
pub(super) fn _canonicalize(
3723
array: ArrayView<'_, Chunked>,
@@ -48,28 +34,11 @@ pub(super) fn _canonicalize(
4834
return array.chunk(0).clone().execute::<Canonical>(ctx);
4935
}
5036

51-
let owned_chunks: Vec<ArrayRef> = array.iter_chunks().cloned().collect();
5237
Ok(match array.dtype() {
53-
DType::Struct(..) => {
54-
let struct_array = pack_struct_chunks(owned_chunks, ctx)?;
55-
Canonical::Struct(struct_array)
38+
DType::Variant(_) => {
39+
let owned_chunks: Vec<ArrayRef> = array.iter_chunks().cloned().collect();
40+
Canonical::Variant(pack_variant_chunks(owned_chunks, ctx)?)
5641
}
57-
DType::List(elem_dtype, _) => Canonical::List(swizzle_list_chunks(
58-
&owned_chunks,
59-
array.array().validity()?,
60-
elem_dtype,
61-
ctx,
62-
)?),
63-
DType::FixedSizeList(elem_dtype, list_size, _) => {
64-
Canonical::FixedSizeList(swizzle_fixed_size_list_chunks(
65-
&owned_chunks,
66-
array.array().validity()?,
67-
elem_dtype,
68-
*list_size,
69-
ctx,
70-
)?)
71-
}
72-
DType::Variant(_) => Canonical::Variant(pack_variant_chunks(owned_chunks, ctx)?),
7342
_ => {
7443
let mut builder = builder_with_capacity_in(ctx.allocator(), array.dtype(), array.len());
7544
array.array().append_to_builder(builder.as_mut(), ctx)?;
@@ -78,17 +47,6 @@ pub(super) fn _canonicalize(
7847
})
7948
}
8049

81-
/// Packs many [`StructArray`]s to instead be a single [`StructArray`], where the [`DynArrayData`](crate::array::DynArrayData) for each
82-
/// field is a [`ChunkedArray`].
83-
///
84-
/// The caller guarantees there are at least 2 chunks.
85-
fn pack_struct_chunks(chunks: Vec<ArrayRef>, ctx: &mut ExecutionCtx) -> VortexResult<StructArray> {
86-
chunks
87-
.into_iter()
88-
.map(|c| c.execute::<StructArray>(ctx))
89-
.process_results(|iter| StructArray::try_concat(iter))?
90-
}
91-
9250
/// Packs many [`VariantArray`]s into one [`VariantArray`] with chunked children.
9351
///
9452
/// The caller guarantees there are at least 2 chunks.
@@ -147,151 +105,14 @@ fn pack_variant_chunks(
147105
VariantArray::try_new(core_storage, shredded)
148106
}
149107

150-
/// Packs [`ListViewArray`]s together into a chunked `ListViewArray`.
151-
///
152-
/// We use the existing arrays (chunks) to form a chunked array of `elements` (the child array).
153-
///
154-
/// The caller guarantees there are at least 2 chunks.
155-
fn swizzle_list_chunks(
156-
chunks: &[ArrayRef],
157-
validity: Validity,
158-
elem_dtype: &DType,
159-
ctx: &mut ExecutionCtx,
160-
) -> VortexResult<ListViewArray> {
161-
let len: usize = chunks.iter().map(|c| c.len()).sum();
162-
163-
assert_eq!(
164-
chunks[0]
165-
.dtype()
166-
.as_list_element_opt()
167-
.vortex_expect("DType was somehow not a list")
168-
.as_ref(),
169-
elem_dtype
170-
);
171-
172-
// Since each list array in `chunks` has offsets local to each array, we can reuse the existing
173-
// array's child `elements` as the chunks and recompute offsets.
174-
175-
let mut list_elements_chunks = Vec::with_capacity(chunks.len());
176-
let mut num_elements = 0;
177-
178-
// TODO(connor)[ListView]: We could potentially choose a smaller type here, but that would make
179-
// this much more complicated.
180-
// We (somewhat arbitrarily) choose `u64` for our offsets and sizes here. These can always be
181-
// narrowed later by the compressor.
182-
let allocator = ctx.allocator();
183-
let mut offsets = allocator.allocate_typed::<u64>(len)?;
184-
let mut sizes = allocator.allocate_typed::<u64>(len)?;
185-
let offsets_out: &mut [u64] = offsets.as_mut_slice_typed::<u64>()?;
186-
let sizes_slice_out: &mut [u64] = sizes.as_mut_slice_typed::<u64>()?;
187-
let mut next_list = 0usize;
188-
189-
for chunk in chunks {
190-
let chunk_array = chunk.clone().execute::<ListViewArray>(ctx)?;
191-
// By rebuilding as zero-copy to `List` and trimming all elements (to prevent gaps), we make
192-
// the final output `ListView` also zero-copyable to `List`.
193-
let chunk_array = chunk_array.rebuild(ListViewRebuildMode::MakeExact, ctx)?;
194-
195-
// Add the `elements` of the current array as a new chunk.
196-
list_elements_chunks.push(chunk_array.elements().clone());
197-
198-
// Cast offsets and sizes to `u64`.
199-
let offsets_arr = chunk_array
200-
.offsets()
201-
.clone()
202-
.cast(DType::Primitive(PType::U64, Nullability::NonNullable))
203-
.vortex_expect("Must be able to fit array offsets in u64")
204-
.execute::<PrimitiveArray>(ctx)?;
205-
206-
let sizes_arr = chunk_array
207-
.sizes()
208-
.clone()
209-
.cast(DType::Primitive(PType::U64, Nullability::NonNullable))
210-
.vortex_expect("Must be able to fit array offsets in u64")
211-
.execute::<PrimitiveArray>(ctx)?;
212-
213-
let offsets_slice = offsets_arr.as_slice::<u64>();
214-
let sizes_slice = sizes_arr.as_slice::<u64>();
215-
216-
// Append offsets and sizes, adjusting offsets to point into the combined array.
217-
for (&offset, &size) in offsets_slice.iter().zip(sizes_slice.iter()) {
218-
offsets_out[next_list] = offset + num_elements;
219-
sizes_slice_out[next_list] = size;
220-
next_list += 1;
221-
}
222-
223-
num_elements += chunk_array.elements().len() as u64;
224-
}
225-
debug_assert_eq!(next_list, len);
226-
227-
// SAFETY: elements are sliced from valid `ListViewArray`s (from `to_listview()`).
228-
let chunked_elements =
229-
unsafe { ChunkedArray::new_unchecked(list_elements_chunks, elem_dtype.clone()) }
230-
.into_array();
231-
232-
let offsets = PrimitiveArray::new(
233-
Buffer::<u64>::from_byte_buffer(offsets.freeze()),
234-
Validity::NonNullable,
235-
)
236-
.into_array();
237-
let sizes = PrimitiveArray::new(
238-
Buffer::<u64>::from_byte_buffer(sizes.freeze()),
239-
Validity::NonNullable,
240-
)
241-
.into_array();
242-
243-
// SAFETY:
244-
// - `offsets` and `sizes` are non-nullable u64 arrays of the same length
245-
// - Each `offset[i] + size[i]` list view is within bounds of elements array because it came
246-
// from valid chunks
247-
// - Validity came from the outer chunked array so it must have the same length
248-
// - Since we made sure that all chunks were zero-copyable to a list above, we know that the
249-
// final concatenated output is also zero-copyable to a list.
250-
Ok(unsafe {
251-
ListViewArray::new_unchecked(chunked_elements, offsets, sizes, validity)
252-
.with_zero_copy_to_list(true)
253-
})
254-
}
255-
256-
/// Packs [`FixedSizeListArray`]s together into a single [`FixedSizeListArray`] whose `elements`
257-
/// child is a [`ChunkedArray`].
258-
///
259-
/// Every chunk shares the same `list_size`, and each chunk's `elements` child is exactly
260-
/// `list_size * chunk.len()` long and starts at the first list, so we can reuse the chunks'
261-
/// `elements` children directly as the chunks of a combined `elements` array without copying.
262-
///
263-
/// The caller guarantees there are at least 2 chunks.
264-
fn swizzle_fixed_size_list_chunks(
265-
chunks: &[ArrayRef],
266-
validity: Validity,
267-
elem_dtype: &DType,
268-
list_size: u32,
269-
ctx: &mut ExecutionCtx,
270-
) -> VortexResult<FixedSizeListArray> {
271-
let len: usize = chunks.iter().map(|c| c.len()).sum();
272-
273-
let mut element_chunks = Vec::with_capacity(chunks.len());
274-
for chunk in chunks {
275-
let chunk_array = chunk.clone().execute::<FixedSizeListArray>(ctx)?;
276-
// A canonical `FixedSizeListArray` keeps its `elements` child trimmed to exactly
277-
// `list_size * chunk.len()` starting at the first list, so the children concatenate
278-
// cleanly into the combined `elements` array.
279-
element_chunks.push(chunk_array.elements().clone());
280-
}
281-
282-
let chunked_elements = ChunkedArray::try_new(element_chunks, elem_dtype.clone())?.into_array();
283-
284-
FixedSizeListArray::try_new(chunked_elements, list_size, validity, len)
285-
}
286-
287108
#[cfg(test)]
288109
mod tests {
289110
use std::sync::Arc;
290111
use std::sync::LazyLock;
291-
use std::sync::atomic::AtomicUsize;
292-
use std::sync::atomic::Ordering;
293112

113+
use rstest::rstest;
294114
use vortex_buffer::buffer;
115+
use vortex_error::VortexExpect;
295116
use vortex_error::VortexResult;
296117
use vortex_error::vortex_bail;
297118
use vortex_error::vortex_err;
@@ -301,15 +122,22 @@ mod tests {
301122
use crate::Canonical;
302123
use crate::IntoArray;
303124
use crate::VortexSessionExecute;
125+
use crate::arrays::Chunked;
304126
use crate::arrays::ChunkedArray;
305127
use crate::arrays::ConstantArray;
128+
use crate::arrays::FixedSizeList;
306129
use crate::arrays::FixedSizeListArray;
307130
use crate::arrays::ListArray;
131+
use crate::arrays::ListView;
308132
use crate::arrays::ListViewArray;
309133
use crate::arrays::PrimitiveArray;
134+
use crate::arrays::Struct;
310135
use crate::arrays::StructArray;
311136
use crate::arrays::VarBinViewArray;
312137
use crate::arrays::VariantArray;
138+
use crate::arrays::chunked::ChunkedArrayExt;
139+
use crate::arrays::fixed_size_list::FixedSizeListArraySlotsExt;
140+
use crate::arrays::listview::ListViewArraySlotsExt;
313141
use crate::arrays::struct_::StructArrayExt;
314142
use crate::arrays::variant::VariantArraySlotsExt;
315143
use crate::assert_arrays_eq;
@@ -318,32 +146,12 @@ mod tests {
318146
use crate::dtype::DType::Variant as VariantDType;
319147
use crate::dtype::Nullability::NonNullable;
320148
use crate::dtype::PType::I32;
321-
use crate::memory::DefaultHostAllocator;
322-
use crate::memory::HostAllocator;
323-
use crate::memory::MemorySessionExt;
324-
use crate::memory::WritableHostBuffer;
325149
use crate::scalar::Scalar;
326150
use crate::validity::Validity;
327151

328152
/// A shared session for these chunked-array tests, used to create execution contexts.
329153
static SESSION: LazyLock<VortexSession> = LazyLock::new(crate::array_session);
330154

331-
#[derive(Debug)]
332-
struct CountingAllocator {
333-
allocations: Arc<AtomicUsize>,
334-
}
335-
336-
impl HostAllocator for CountingAllocator {
337-
fn allocate(
338-
&self,
339-
len: usize,
340-
alignment: vortex_buffer::Alignment,
341-
) -> VortexResult<WritableHostBuffer> {
342-
self.allocations.fetch_add(1, Ordering::Relaxed);
343-
DefaultHostAllocator.allocate(len, alignment)
344-
}
345-
}
346-
347155
fn variant_scalar(value: i32) -> Scalar {
348156
Scalar::variant(Scalar::primitive(value, NonNullable))
349157
}
@@ -660,38 +468,53 @@ mod tests {
660468
Ok(())
661469
}
662470

663-
#[test]
664-
fn list_canonicalize_uses_memory_session_allocator() {
665-
let allocations = Arc::new(AtomicUsize::new(0));
666-
let session = crate::array_session().with_allocator(Arc::new(CountingAllocator {
667-
allocations: Arc::clone(&allocations),
668-
}));
669-
let mut ctx = session.create_execution_ctx();
670-
671-
let l1 = ListArray::try_new(
672-
buffer![1, 2, 3, 4].into_array(),
673-
buffer![0, 3].into_array(),
674-
Validity::NonNullable,
675-
)
676-
.unwrap();
677-
let l2 = ListArray::try_new(
678-
buffer![5, 6].into_array(),
471+
/// Canonicalizing a `ChunkedArray` reuses each chunk's children instead of concatenating
472+
/// them: a nested builder keeps a child chunked on exactly the boundaries it was appended on,
473+
/// however short the appended chunk is.
474+
#[rstest]
475+
#[case::struct_(
476+
StructArray::try_from_iter([("a", buffer![1i32, 2])])
477+
.vortex_expect("struct array")
478+
.into_array(),
479+
|array: &ArrayRef| array.as_::<Struct>().unmasked_field(0).clone()
480+
)]
481+
#[case::fixed_size_list(
482+
FixedSizeListArray::new(buffer![1i32, 2].into_array(), 2, Validity::NonNullable, 1)
483+
.into_array(),
484+
|array: &ArrayRef| array.as_::<FixedSizeList>().elements().clone()
485+
)]
486+
#[case::list(
487+
ListArray::try_new(
488+
buffer![1i32, 2].into_array(),
679489
buffer![0, 2].into_array(),
680490
Validity::NonNullable,
681491
)
682-
.unwrap();
492+
.vortex_expect("list array")
493+
.into_array(),
494+
|array: &ArrayRef| array.as_::<ListView>().elements().clone()
495+
)]
496+
fn canonicalize_reuses_chunk_children(
497+
#[case] chunk: ArrayRef,
498+
#[case] child_of: fn(&ArrayRef) -> ArrayRef,
499+
) -> VortexResult<()> {
500+
let mut ctx = SESSION.create_execution_ctx();
683501

684-
let chunked_list = ChunkedArray::try_new(
685-
vec![l1.into_array(), l2.into_array()],
686-
List(Arc::new(Primitive(I32, NonNullable)), NonNullable),
687-
)
688-
.unwrap()
689-
.into_array();
502+
let chunked =
503+
ChunkedArray::try_new(vec![chunk.clone(), chunk.clone()], chunk.dtype().clone())?
504+
.into_array();
505+
let canonical = chunked.execute::<Canonical>(&mut ctx)?.into_array();
690506

691-
drop(chunked_list.execute::<Canonical>(&mut ctx).unwrap());
692-
assert!(
693-
allocations.load(Ordering::Relaxed) >= 2,
694-
"expected offset+size allocations through MemorySession"
507+
let child = child_of(&canonical);
508+
assert_eq!(
509+
child.as_::<Chunked>().nchunks(),
510+
2,
511+
"each chunk's child should have become a chunk of the combined child",
695512
);
513+
514+
let expected =
515+
ChunkedArray::try_new(vec![chunk.clone(), chunk], canonical.dtype().clone())?;
516+
assert_arrays_eq!(&canonical, &expected, &mut ctx);
517+
518+
Ok(())
696519
}
697520
}

0 commit comments

Comments
 (0)