1use std::fmt::Display;
5use std::fmt::Formatter;
6use std::ops::Range;
7
8use vortex_buffer::Buffer;
9use vortex_buffer::BufferMut;
10use vortex_error::VortexResult;
11use vortex_error::vortex_ensure;
12use vortex_error::vortex_ensure_eq;
13use vortex_error::vortex_err;
14
15use crate::ArrayRef;
16use crate::ArraySlots;
17use crate::Canonical;
18use crate::ExecutionCtx;
19use crate::IntoArray;
20use crate::VortexSessionExecute;
21use crate::array::Array;
22use crate::array::ArrayParts;
23use crate::array::TypedArrayRef;
24use crate::array_slots;
25use crate::arrays::Patched;
26use crate::arrays::PrimitiveArray;
27use crate::arrays::patched::TransposedPatches;
28use crate::arrays::patched::patch_lanes;
29use crate::buffer::BufferHandle;
30use crate::dtype::DType;
31use crate::dtype::IntegerPType;
32use crate::dtype::NativePType;
33use crate::dtype::PType;
34use crate::legacy_session;
35use crate::match_each_native_ptype;
36use crate::match_each_unsigned_integer_ptype;
37use crate::patches::Patches;
38use crate::validity::Validity;
39
40#[derive(Debug, Clone)]
41pub struct PatchedData {
42 pub(super) n_lanes: usize,
46
47 pub(super) offset: usize,
53}
54
55#[array_slots(Patched)]
56pub struct PatchedSlots {
57 #[slot(0)]
59 pub inner: ArrayRef,
60 #[slot(1)]
62 pub lane_offsets: ArrayRef,
63 #[slot(2)]
65 pub patch_indices: ArrayRef,
66 #[slot(3)]
68 pub patch_values: ArrayRef,
69}
70
71impl Display for PatchedData {
72 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
73 write!(f, "n_lanes: {}, offset: {}", self.n_lanes, self.offset)
74 }
75}
76
77impl PatchedData {
78 pub(crate) fn validate(
79 &self,
80 dtype: &DType,
81 len: usize,
82 slots: &PatchedSlotsView,
83 ) -> VortexResult<()> {
84 vortex_ensure_eq!(
85 slots.inner.dtype(),
86 dtype,
87 "PatchedArray base dtype does not match outer dtype",
88 );
89 vortex_ensure_eq!(
90 slots.inner.len(),
91 len,
92 "PatchedArray base len does not match outer len",
93 );
94 vortex_ensure_eq!(
95 slots.patch_indices.len(),
96 slots.patch_values.len(),
97 "PatchedArray patch indices len does not match patch values len",
98 );
99 Ok(())
100 }
101}
102
103pub trait PatchedArrayExt: PatchedArraySlotsExt {
104 #[inline]
105 fn n_lanes(&self) -> usize {
106 self.n_lanes
107 }
108
109 #[inline]
110 fn offset(&self) -> usize {
111 self.offset
112 }
113
114 #[inline]
115 #[allow(clippy::disallowed_methods)]
116 fn lane_range(&self, chunk: usize, lane: usize) -> VortexResult<Range<usize>> {
117 assert!(chunk * 1024 <= self.as_ref().len() + self.offset());
118 assert!(lane < self.n_lanes());
119
120 let start = self.lane_offsets().execute_scalar(
121 chunk * self.n_lanes() + lane,
122 &mut legacy_session().create_execution_ctx(),
123 )?;
124 let stop = self.lane_offsets().execute_scalar(
125 chunk * self.n_lanes() + lane + 1,
126 &mut legacy_session().create_execution_ctx(),
127 )?;
128
129 let start = start
130 .as_primitive()
131 .as_::<usize>()
132 .ok_or_else(|| vortex_err!("could not cast lane_offset to usize"))?;
133
134 let stop = stop
135 .as_primitive()
136 .as_::<usize>()
137 .ok_or_else(|| vortex_err!("could not cast lane_offset to usize"))?;
138
139 Ok(start..stop)
140 }
141
142 fn slice_chunks(&self, chunks: Range<usize>) -> VortexResult<Array<Patched>> {
143 let lane_offsets_start = chunks.start * self.n_lanes();
144 let lane_offsets_stop = chunks.end * self.n_lanes() + 1;
145
146 let sliced_lane_offsets = self
147 .lane_offsets()
148 .slice(lane_offsets_start..lane_offsets_stop)?;
149 let indices = self.patch_indices().clone();
150 let values = self.patch_values().clone();
151
152 let begin = (chunks.start * 1024).saturating_sub(self.offset());
153 let end = (chunks.end * 1024)
154 .saturating_sub(self.offset())
155 .min(self.as_ref().len());
156
157 let offset = if chunks.start == 0 { self.offset() } else { 0 };
158 let inner = self.inner().slice(begin..end)?;
159 let len = inner.len();
160 let dtype = self.as_ref().dtype().clone();
161 let slots = PatchedSlots {
162 inner,
163 lane_offsets: sliced_lane_offsets,
164 patch_indices: indices,
165 patch_values: values,
166 }
167 .into_slots();
168
169 Ok(unsafe { Patched::new_unchecked(dtype, len, slots, self.n_lanes(), offset) })
170 }
171}
172
173impl<T: TypedArrayRef<Patched>> PatchedArrayExt for T {}
174
175impl Patched {
176 pub fn from_array_and_patches(
177 inner: ArrayRef,
178 patches: &Patches,
179 ctx: &mut ExecutionCtx,
180 ) -> VortexResult<Array<Patched>> {
181 vortex_ensure!(
182 inner.dtype().eq_with_nullability_superset(patches.dtype()),
183 "array DType must match patches DType"
184 );
185
186 vortex_ensure!(
187 inner.dtype().is_primitive(),
188 "Creating PatchedArray from Patches only supported for primitive arrays"
189 );
190
191 vortex_ensure!(
192 patches.num_patches() <= u32::MAX as usize,
193 "PatchedArray does not support > u32::MAX patch values"
194 );
195
196 vortex_ensure!(
197 patches.values().all_valid(ctx)?,
198 "PatchedArray cannot be built from Patches with nulls"
199 );
200
201 let values_ptype = patches.dtype().as_ptype();
202
203 let TransposedPatches {
204 n_lanes,
205 lane_offsets,
206 indices,
207 values,
208 } = transpose_patches(patches, ctx)?;
209
210 let lane_offsets = PrimitiveArray::from_buffer_handle(
211 BufferHandle::new_host(lane_offsets),
212 PType::U32,
213 Validity::NonNullable,
214 )
215 .into_array();
216 let indices = PrimitiveArray::from_buffer_handle(
217 BufferHandle::new_host(indices),
218 PType::U16,
219 Validity::NonNullable,
220 )
221 .into_array();
222 let values = PrimitiveArray::from_buffer_handle(
223 BufferHandle::new_host(values),
224 values_ptype,
225 Validity::NonNullable,
226 )
227 .into_array();
228
229 let dtype = inner.dtype().clone();
230 let len = inner.len();
231 let slots = PatchedSlots {
232 inner,
233 lane_offsets,
234 patch_indices: indices,
235 patch_values: values,
236 }
237 .into_slots();
238 Ok(unsafe { Self::new_unchecked(dtype, len, slots, n_lanes, 0) })
239 }
240
241 pub(crate) unsafe fn new_unchecked(
242 dtype: DType,
243 len: usize,
244 slots: ArraySlots,
245 n_lanes: usize,
246 offset: usize,
247 ) -> Array<Patched> {
248 unsafe {
249 Array::from_parts_unchecked(ArrayParts::new(
250 Patched,
251 dtype,
252 len,
253 PatchedData { n_lanes, offset },
254 slots,
255 ))
256 }
257 }
258}
259
260fn transpose_patches(patches: &Patches, ctx: &mut ExecutionCtx) -> VortexResult<TransposedPatches> {
262 let array_len = patches.array_len();
263 let offset = patches.offset();
264
265 let indices = patches
266 .indices()
267 .clone()
268 .execute::<Canonical>(ctx)?
269 .into_primitive();
270
271 let values = patches
272 .values()
273 .clone()
274 .execute::<Canonical>(ctx)?
275 .into_primitive();
276
277 let indices_ptype = indices.ptype();
278 let values_ptype = values.ptype();
279
280 let indices = indices.buffer_handle().clone().unwrap_host();
281 let values = values.buffer_handle().clone().unwrap_host();
282
283 match_each_unsigned_integer_ptype!(indices_ptype, |I| {
284 match_each_native_ptype!(values_ptype, |V| {
285 let indices: Buffer<I> = Buffer::from_byte_buffer(indices);
286 let values: Buffer<V> = Buffer::from_byte_buffer(values);
287
288 Ok(transpose(
289 indices.as_slice(),
290 values.as_slice(),
291 offset,
292 array_len,
293 ))
294 })
295 })
296}
297
298#[expect(clippy::cast_possible_truncation)]
299fn transpose<I: IntegerPType, V: NativePType>(
300 indices_in: &[I],
301 values_in: &[V],
302 offset: usize,
303 array_len: usize,
304) -> TransposedPatches {
305 let n_chunks = array_len.div_ceil(1024);
307 assert!(
308 n_chunks <= u32::MAX as usize,
309 "Cannot transpose patches for array with >= 4 trillion elements"
310 );
311
312 let n_lanes = patch_lanes::<V>();
313
314 let mut indices_buffer = BufferMut::with_capacity(indices_in.len());
316 let mut values_buffer = BufferMut::with_capacity(values_in.len());
317
318 let mut lane_offsets: BufferMut<u32> = BufferMut::zeroed(n_chunks * n_lanes + 1);
320
321 for index in indices_in {
323 let index = index.as_() - offset;
324 let chunk = index / 1024;
325 let lane = index % n_lanes;
326
327 lane_offsets[chunk * n_lanes + lane + 1] += 1;
328 }
329
330 for index in 1..lane_offsets.len() {
331 lane_offsets[index] += lane_offsets[index - 1];
332 }
333
334 let indices_out = indices_buffer.spare_capacity_mut();
336 let values_out = values_buffer.spare_capacity_mut();
337 for (index, &value) in std::iter::zip(indices_in, values_in) {
338 let index = index.as_() - offset;
339 let chunk = index / 1024;
340 let lane = index % n_lanes;
341
342 let position = &mut lane_offsets[chunk * n_lanes + lane];
343 indices_out[*position as usize].write((index % 1024) as u16);
344 values_out[*position as usize].write(value);
345 *position += 1;
346 }
347
348 unsafe {
349 indices_buffer.set_len(indices_in.len());
350 values_buffer.set_len(values_in.len());
351 }
352
353 for index in indices_in {
354 let index = index.as_() - offset;
355 let chunk = index / 1024;
356 let lane = index % n_lanes;
357
358 lane_offsets[chunk * n_lanes + lane] -= 1;
359 }
360
361 TransposedPatches {
362 n_lanes,
363 lane_offsets: lane_offsets.freeze().into_byte_buffer(),
364 indices: indices_buffer.freeze().into_byte_buffer(),
365 values: values_buffer.freeze().into_byte_buffer(),
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use vortex_buffer::Buffer;
372 use vortex_buffer::buffer;
373
374 use super::PatchedSlots;
375 use crate::ArrayRef;
376 use crate::IntoArray;
377 use crate::array_slots;
378 use crate::arrays::Chunked;
379 use crate::arrays::Null;
380 use crate::arrays::PrimitiveArray;
381 use crate::arrays::Union;
382 use crate::validity::Validity;
383
384 #[array_slots(Null)]
385 struct OptionalPatchedSlots {
386 #[slot(0)]
387 required: ArrayRef,
388 #[slot(1)]
389 maybe: Option<ArrayRef>,
390 }
391
392 #[array_slots(Chunked)]
393 struct VariadicSlots {
394 #[slot(0)]
395 offsets: ArrayRef,
396 #[slot(1)]
397 maybe_validity: Option<ArrayRef>,
398 #[slot(2..)]
399 chunks: Vec<ArrayRef>,
400 }
401
402 #[array_slots(Union)]
405 struct ShuffledVariadicSlots {
406 #[slot(2..)]
407 chunks: Vec<ArrayRef>,
408 #[slot(1)]
409 maybe_validity: Option<ArrayRef>,
410 #[slot(0)]
411 offsets: ArrayRef,
412 }
413
414 #[test]
415 fn generated_slots_round_trip() {
416 let required = PrimitiveArray::new(buffer![1u8, 2, 3], Validity::NonNullable).into_array();
417 let optional = PrimitiveArray::new(buffer![4u8, 5, 6], Validity::NonNullable).into_array();
418
419 let slot_vec = vec![Some(required.clone()), Some(optional.clone())];
420 let view = OptionalPatchedSlotsView::from_slots(&slot_vec);
421 assert_eq!(view.required.len(), 3);
422 assert_eq!(view.maybe.expect("optional slot").len(), 3);
423
424 let cloned = OptionalPatchedSlots::from_slots(slot_vec.into());
425 assert_eq!(cloned.required.len(), required.len());
426 assert_eq!(cloned.maybe.expect("optional clone").len(), optional.len());
427
428 let rebuilt = PatchedSlots::from_slots(
429 vec![
430 Some(required.clone()),
431 Some(optional.clone()),
432 Some(required.clone()),
433 Some(optional.clone()),
434 ]
435 .into(),
436 );
437 assert_eq!(rebuilt.inner.len(), required.len());
438 assert_eq!(rebuilt.patch_values.len(), optional.len());
439 }
440
441 #[test]
442 fn variadic_slots_round_trip() {
443 let offsets = PrimitiveArray::new(buffer![0u64, 3, 5], Validity::NonNullable).into_array();
444 let chunk0 = PrimitiveArray::new(buffer![1u8, 2, 3], Validity::NonNullable).into_array();
445 let chunk1 = PrimitiveArray::new(buffer![4u8, 5], Validity::NonNullable).into_array();
446
447 assert_eq!(VariadicSlots::OFFSETS, 0);
448 assert_eq!(VariadicSlots::MAYBE_VALIDITY, 1);
449 assert_eq!(VariadicSlots::CHUNKS_OFFSET, 2);
450 assert_eq!(VariadicSlots::FIXED_COUNT, 2);
451 assert_eq!(VariadicSlots::slot_name(0), "offsets");
452 assert_eq!(VariadicSlots::slot_name(3), "chunks[1]");
453
454 let slot_vec = vec![Some(offsets.clone()), None, Some(chunk0), Some(chunk1)];
455
456 let view = VariadicSlotsView::from_slots(&slot_vec);
457 assert_eq!(view.offsets.len(), 3);
458 assert!(view.maybe_validity.is_none());
459 assert_eq!(view.chunks.len(), 2);
460 assert_eq!(view.chunks[0].len(), 3);
461 assert_eq!(view.chunks.get(1).map(|c| c.len()), Some(2));
462 assert!(view.chunks.get(2).is_none());
463 assert_eq!(
464 view.chunks.iter().map(|c| c.len()).collect::<Vec<_>>(),
465 vec![3, 2]
466 );
467
468 let owned = view.to_owned();
469 assert_eq!(owned.chunks.len(), 2);
470
471 let owned = VariadicSlots::from_slots(slot_vec.into());
472 assert_eq!(owned.offsets.len(), offsets.len());
473 assert!(owned.maybe_validity.is_none());
474 assert_eq!(owned.chunks.len(), 2);
475
476 let slots = owned.into_slots();
477 assert_eq!(slots.len(), 4);
478 assert!(slots[1].is_none());
479 assert_eq!(
480 slots[VariadicSlots::CHUNKS_OFFSET]
481 .as_ref()
482 .map(|c| c.len()),
483 Some(3)
484 );
485 }
486
487 #[test]
488 fn variadic_slots_empty_tail() {
489 let offsets = PrimitiveArray::new(buffer![0u64], Validity::NonNullable).into_array();
490 let slot_vec = vec![Some(offsets), None];
491
492 let view = VariadicSlotsView::from_slots(&slot_vec);
493 assert!(view.chunks.is_empty());
494
495 let owned = VariadicSlots::from_slots(slot_vec.into());
496 assert!(owned.chunks.is_empty());
497 assert_eq!(owned.into_slots().len(), 2);
498 }
499
500 #[test]
501 fn variadic_slots_spilled_tail_keeps_order() {
502 let offsets = PrimitiveArray::new(buffer![0u64], Validity::NonNullable).into_array();
503 let chunks: Vec<ArrayRef> = (0..5u8)
504 .map(|len| {
505 PrimitiveArray::new((0..len).collect::<Buffer<u8>>(), Validity::NonNullable)
506 .into_array()
507 })
508 .collect();
509
510 let slots = VariadicSlots {
511 offsets: offsets.clone(),
512 maybe_validity: None,
513 chunks,
514 }
515 .into_slots();
516
517 assert!(slots.spilled());
518 assert_eq!(slots.len(), VariadicSlots::FIXED_COUNT + 5);
519 assert_eq!(
520 slots[VariadicSlots::OFFSETS].as_ref().map(|s| s.len()),
521 Some(offsets.len())
522 );
523 assert!(slots[VariadicSlots::MAYBE_VALIDITY].is_none());
524 assert_eq!(
525 slots[VariadicSlots::CHUNKS_OFFSET..]
526 .iter()
527 .map(|s| s.as_ref().map(|c| c.len()))
528 .collect::<Vec<_>>(),
529 (0..5).map(Some).collect::<Vec<_>>()
530 );
531 }
532
533 #[test]
534 fn slot_indices_follow_annotations_not_declaration_order() {
535 assert_eq!(
536 ShuffledVariadicSlots::OFFSETS,
537 VariadicSlots::OFFSETS,
538 "field declaration order must not move a slot"
539 );
540 assert_eq!(
541 ShuffledVariadicSlots::MAYBE_VALIDITY,
542 VariadicSlots::MAYBE_VALIDITY
543 );
544 assert_eq!(
545 ShuffledVariadicSlots::CHUNKS_OFFSET,
546 VariadicSlots::CHUNKS_OFFSET
547 );
548 assert_eq!(
549 ShuffledVariadicSlots::FIXED_COUNT,
550 VariadicSlots::FIXED_COUNT
551 );
552 assert_eq!(ShuffledVariadicSlots::slot_name(0), "offsets");
553 assert_eq!(ShuffledVariadicSlots::slot_name(1), "maybe_validity");
554 assert_eq!(ShuffledVariadicSlots::slot_name(3), "chunks[1]");
555 }
556
557 #[test]
558 fn shuffled_declaration_order_round_trips_through_storage() {
559 let offsets = PrimitiveArray::new(buffer![0u64, 3], Validity::NonNullable).into_array();
560 let validity = PrimitiveArray::new(buffer![1u8, 1], Validity::NonNullable).into_array();
561 let chunk = PrimitiveArray::new(buffer![1u8, 2, 3], Validity::NonNullable).into_array();
562
563 let slot_vec = vec![
564 Some(offsets.clone()),
565 Some(validity.clone()),
566 Some(chunk.clone()),
567 ];
568
569 let view = ShuffledVariadicSlotsView::from_slots(&slot_vec);
570 assert_eq!(view.offsets.len(), offsets.len());
571 assert_eq!(view.maybe_validity.map(|v| v.len()), Some(validity.len()));
572 assert_eq!(view.chunks.len(), 1);
573 assert_eq!(view.chunks[0].len(), chunk.len());
574
575 let round_tripped = ShuffledVariadicSlots::from_slots(slot_vec.into()).into_slots();
577 assert_eq!(round_tripped.len(), 3);
578 assert_eq!(
579 round_tripped[ShuffledVariadicSlots::OFFSETS]
580 .as_ref()
581 .map(|s| s.len()),
582 Some(offsets.len())
583 );
584 assert_eq!(
585 round_tripped[ShuffledVariadicSlots::MAYBE_VALIDITY]
586 .as_ref()
587 .map(|s| s.len()),
588 Some(validity.len())
589 );
590 assert_eq!(
591 round_tripped[ShuffledVariadicSlots::CHUNKS_OFFSET]
592 .as_ref()
593 .map(|s| s.len()),
594 Some(chunk.len())
595 );
596 }
597}