Skip to main content

diskann_quantization/
views.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6use std::num::NonZeroUsize;
7
8use diskann_utils::views::{DenseData, MutDenseData};
9use std::ops::{Index, IndexMut};
10use thiserror::Error;
11
12//////////////////
13// ChunkOffsets //
14//////////////////
15
16/// A wrapper class for PQ chunk offsets.
17///
18/// Upon construction, this class guarantees that the underlying chunk offset plan is valid.
19/// A valid PQ chunk offset plan records the starting offsets of each chunk such that chunk
20/// `i` of a slice `x` can be accessed using `x[offsets[i]..offsets[i+1]]`.
21///
22/// In particular, a valid PQ chunk offset plan has the following properties:
23///
24/// * It has a length of at least 2.
25/// * Its first entry is 0.
26/// * For `i` in `0..offsets.len()`, `offsets[i] < offsets[i+1]`.
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct ChunkOffsetsBase<T>
29where
30    T: DenseData<Elem = usize>,
31{
32    // Pre-compute the associated dimension for better locality.
33    //
34    // We could extract this as `offsets.last().unwrap() - 1`, but hoisting it out into
35    // the struct means it is readily available when needed.
36    dim: NonZeroUsize,
37    // Chunk Offsets
38    offsets: T,
39}
40
41#[derive(Error, Debug)]
42#[non_exhaustive]
43pub enum ChunkOffsetError {
44    #[error("offsets must have a length of at least 2, found {0}")]
45    LengthNotAtLeastTwo(usize),
46    #[error("offsets must begin at 0, not {0}")]
47    DoesNotBeginWithZero(usize),
48    #[error(
49        "offsets must be strictly increasing, \
50         instead entry {start_val} at position {start} is followed by {next_val}"
51    )]
52    NonMonotonic {
53        start_val: usize,
54        start: usize,
55        next_val: usize,
56    },
57}
58
59/// Error returned by [`ChunkOffsets::partition`].
60#[derive(Error, Debug)]
61#[error("num_chunks {num_chunks} must not exceed dim {dim}")]
62pub struct PartitionError {
63    pub num_chunks: usize,
64    pub dim: usize,
65}
66
67/// Error returned by [`ChunkOffsetsView::partition_into`].
68#[derive(Error, Debug)]
69#[non_exhaustive]
70pub enum PartitionIntoError {
71    #[error("scratch must have a length of at least 2, found {0}")]
72    ScratchTooSmall(usize),
73    #[error(transparent)]
74    PartitionError(#[from] PartitionError),
75}
76
77impl<T> ChunkOffsetsBase<T>
78where
79    T: DenseData<Elem = usize>,
80{
81    /// Construct a new `ChunkOffset` from a raw slice.
82    ///
83    /// Returns an error if:
84    /// * The length of `offsets` is less than 2.
85    /// * The entries in `offsets` are not strictly increasing.
86    pub fn new(offsets: T) -> Result<Self, ChunkOffsetError> {
87        let slice = offsets.as_slice();
88
89        // Validate that the length is correct.
90        let len = slice.len();
91        if len < 2 {
92            return Err(ChunkOffsetError::LengthNotAtLeastTwo(len));
93        }
94
95        // Check that we don't start at zero.
96        let start = slice[0];
97        if start != 0 {
98            return Err(ChunkOffsetError::DoesNotBeginWithZero(start));
99        }
100
101        // What follows is a convoluted dance to safely get the source dimension as a
102        // `NonZeroUsize` while validating the monotonicity of the offsets in the provided
103        // slice.
104        //
105        // We can seed the `NonZeroUsize` with the knowledge that we've already checked
106        // that the length of the `slice` vector is at least two.
107        let mut last: NonZeroUsize = match NonZeroUsize::new(slice[1]) {
108            Some(x) => Ok(x),
109            None => Err(ChunkOffsetError::NonMonotonic {
110                start_val: start,
111                start: 0,
112                next_val: 0,
113            }),
114        }?;
115
116        // Now that we've successfully initialized a `NonZeroUsize` - we can use it going
117        // forward.
118        //
119        // Validate that `offsets` is monotonic.
120        for i in 2..slice.len() {
121            let start_val = slice[i - 1];
122            let next_val = NonZeroUsize::new(slice[i]);
123            last = match next_val {
124                Some(next_val) => {
125                    if start_val >= next_val.get() {
126                        Err(ChunkOffsetError::NonMonotonic {
127                            start_val,
128                            start: i - 1,
129                            next_val: next_val.get(),
130                        })
131                    } else {
132                        Ok(next_val)
133                    }
134                }
135                // If we hit this case, then `slice[i]` was zero.
136                None => Err(ChunkOffsetError::NonMonotonic {
137                    start_val,
138                    start: i - 1,
139                    next_val: 0,
140                }),
141            }?;
142        }
143
144        // The last entry in `offset` is one-past the end.
145        Ok(Self { dim: last, offsets })
146    }
147
148    /// Return the number of chunks associated with this mapping.
149    ///
150    /// This will be one-less than the length of the provided slice.
151    pub fn len(&self) -> usize {
152        // This invariant should hold by construction, and allows us to safely subtract
153        // 1 from the length of the underlying `offsets` span.
154        debug_assert!(self.offsets.as_slice().len() >= 2);
155        self.offsets.as_slice().len() - 1
156    }
157
158    /// Return whether the offsets are empty.
159    pub fn is_empty(&self) -> bool {
160        // by class invariant, there must always be at least one chunk.
161        false
162    }
163
164    /// Return the dimensionality of the vector data associated with this chunking schema.
165    pub fn dim(&self) -> usize {
166        self.dim.get()
167    }
168
169    /// Return the dimensionality of the vector data associated with this chunking schema.
170    ///
171    /// By class invariant, the dimensionality must be nonzero, and this expressed in the
172    /// retuen type.
173    ///
174    /// This method cannot fail and will not panic.
175    pub fn dim_nonzero(&self) -> NonZeroUsize {
176        self.dim
177    }
178
179    /// Return a range containing the start and one-past-the-end indices for chunk `i`.
180    ///
181    /// # Panics
182    ///
183    /// Panics if `i >= self.len()`.
184    #[expect(
185        clippy::panic,
186        reason = "this is documented to panic for out-of-bounds `i`"
187    )]
188    pub fn at(&self, i: usize) -> core::ops::Range<usize> {
189        match self.get(i) {
190            Some(range) => range,
191            None => panic!("index {i} must be less than len {}", self.len()),
192        }
193    }
194
195    /// Return a range containing the start and one-past-the-end indices for chunk `i`.
196    ///
197    /// Returns `None` is `i >= self.len()`.
198    pub fn get(&self, i: usize) -> Option<core::ops::Range<usize>> {
199        if i < self.len() {
200            let slice = self.offsets.as_slice();
201            Some(slice[i]..slice[i + 1])
202        } else {
203            None
204        }
205    }
206
207    /// Return `self` as a view.
208    pub fn as_view(&self) -> ChunkOffsetsView<'_> {
209        ChunkOffsetsBase {
210            dim: self.dim,
211            offsets: self.offsets.as_slice(),
212        }
213    }
214
215    /// Return a `'static` copy of `self`.
216    pub fn to_owned(&self) -> ChunkOffsets {
217        ChunkOffsetsBase {
218            dim: self.dim,
219            offsets: self.offsets.as_slice().into(),
220        }
221    }
222
223    /// Return the underlying data as a slice.
224    pub fn as_slice(&self) -> &[usize] {
225        self.offsets.as_slice()
226    }
227
228    /// Return the maximum chunk dimension.
229    pub fn max_chunk_dim(&self) -> NonZeroUsize {
230        let mut max = NonZeroUsize::MIN;
231        let mut itr = self.offsets.as_slice().iter();
232
233        let Some(mut previous) = itr.next() else {
234            // NOTE: this is unreachable since we maintain the invariant that the number
235            // of chunks is at least 1.
236            return max;
237        };
238
239        for next in itr {
240            // This cannot underflow because offsets are verified to be strictly monotonic.
241            if let Some(dim) = NonZeroUsize::new(next - previous) {
242                max = max.max(dim);
243            }
244
245            previous = next;
246        }
247
248        max
249    }
250}
251
252pub type ChunkOffsetsView<'a> = ChunkOffsetsBase<&'a [usize]>;
253pub type ChunkOffsets = ChunkOffsetsBase<Box<[usize]>>;
254
255/// Allow chunk offsets view to be converted directly to slices.
256impl<'a> From<ChunkOffsetsView<'a>> for &'a [usize] {
257    fn from(view: ChunkOffsetsView<'a>) -> Self {
258        view.offsets
259    }
260}
261
262impl ChunkOffsets {
263    /// Build a chunk-offset plan that partitions `dim` into `num_chunks`
264    /// near-equal chunks. The first `dim.get() % num_chunks.get()` chunks are
265    /// one element larger than the rest.
266    ///
267    /// Returns an error if the requested partition is not valid (e.g.
268    /// `num_chunks.get() > dim.get()`).
269    pub fn partition(dim: NonZeroUsize, num_chunks: NonZeroUsize) -> Result<Self, PartitionError> {
270        if num_chunks.get() > dim.get() {
271            return Err(PartitionError {
272                num_chunks: num_chunks.get(),
273                dim: dim.get(),
274            });
275        }
276        let mut offsets = vec![0usize; num_chunks.get() + 1].into_boxed_slice();
277        fill_chunk_offsets(dim, &mut offsets);
278        Ok(Self { dim, offsets })
279    }
280}
281
282impl<'a> ChunkOffsetsView<'a> {
283    /// Fill the caller-owned `scratch` buffer with the partition for `dim`
284    /// into `scratch.len() - 1` chunks and return a validated view borrowing it.
285    ///
286    /// See [`ChunkOffsets::partition`] for the partitioning rule.
287    ///
288    /// Returns an error if `scratch.len() < 2` or if the requested partition is not
289    /// valid (e.g. `scratch.len() - 1 > dim.get()`).
290    pub fn partition_into(
291        dim: NonZeroUsize,
292        scratch: &'a mut [usize],
293    ) -> Result<Self, PartitionIntoError> {
294        if scratch.len() < 2 {
295            return Err(PartitionIntoError::ScratchTooSmall(scratch.len()));
296        }
297        let num_chunks = scratch.len() - 1;
298        if num_chunks > dim.get() {
299            return Err(PartitionError {
300                num_chunks,
301                dim: dim.get(),
302            }
303            .into());
304        }
305
306        fill_chunk_offsets(dim, scratch);
307        Ok(Self {
308            dim,
309            offsets: scratch,
310        })
311    }
312}
313
314/// Internal helper: fill `offsets` with the prefix-sum
315/// partitioning of `dimensions` into `num_chunks` near-equal chunks, where
316/// `num_chunks = offsets.len() - 1`.
317///
318/// The first `dimensions.get() % num_chunks` chunks are one element larger than the
319/// rest, so each chunk has size `dimensions.get() / num_chunks` or
320/// `dimensions.get() / num_chunks + 1` and the total covers `[0, dimensions.get()]`.
321///
322/// # Panics
323///
324/// Panics if `offsets.len() <= 1`.
325fn fill_chunk_offsets(dimensions: NonZeroUsize, offsets: &mut [usize]) {
326    let num_chunks = offsets.len() - 1;
327    let dimensions = dimensions.get();
328    let mut chunk_offset: usize = 0;
329    offsets[0] = chunk_offset;
330    for chunk_index in 0..num_chunks {
331        chunk_offset += dimensions / num_chunks;
332        if chunk_index < (dimensions % num_chunks) {
333            chunk_offset += 1;
334        }
335        offsets[chunk_index + 1] = chunk_offset;
336    }
337}
338
339///////////////
340// ChunkView //
341///////////////
342
343/// A view over a slice that partitions the slice into chunks corresponding to a valid
344/// PQ chunking configuration.
345///
346/// This class maintains the invariant that the provided chunking configuration if valid
347/// and that the data being partitioned has the correct length.
348#[derive(Debug, Clone, Copy)]
349pub struct ChunkViewImpl<'a, T>
350where
351    T: DenseData,
352{
353    data: T,
354    offsets: ChunkOffsetsView<'a>,
355}
356
357#[derive(Error, Debug)]
358#[non_exhaustive]
359#[error(
360    "error in chunk view construction, got a slice of length {got} but \
361         the provided chunking schema expects a length of {should}"
362)]
363pub struct ChunkViewError {
364    got: usize,
365    should: usize,
366}
367
368impl<'a, T> ChunkViewImpl<'a, T>
369where
370    T: DenseData,
371{
372    /// Construct a new `ChunkView`.
373    ///
374    /// Returns an error if `data.len() != offsets.dim()`.
375    pub fn new<U>(data: U, offsets: ChunkOffsetsView<'a>) -> Result<Self, ChunkViewError>
376    where
377        T: From<U>,
378    {
379        let data: T = data.into();
380
381        // Use the `offsets` as the source of truth because it is more likely that a
382        // `ChunkOffsetsView` will be constructed once and reused, so is more likely to be
383        // the desired outcome.
384        let got = data.as_slice().len();
385        let should = offsets.dim();
386        if got != should {
387            Err(ChunkViewError { got, should })
388        } else {
389            Ok(Self { data, offsets })
390        }
391    }
392
393    /// Return the number of partitions in the chunking view.
394    pub fn len(&self) -> usize {
395        self.offsets.len()
396    }
397
398    pub fn is_empty(&self) -> bool {
399        self.offsets.is_empty()
400    }
401}
402
403/// Return the `i`th chunk of the view.
404///
405/// # Panics
406///
407/// Panics if `i >= self.len()`.
408impl<T> Index<usize> for ChunkViewImpl<'_, T>
409where
410    T: DenseData,
411{
412    type Output = [T::Elem];
413
414    fn index(&self, i: usize) -> &Self::Output {
415        &(self.data.as_slice())[self.offsets.at(i)]
416    }
417}
418
419/// Return the `i`th chunk of the view.
420///
421/// # Panics
422///
423/// Panics if `i >= self.len()`.
424impl<T> IndexMut<usize> for ChunkViewImpl<'_, T>
425where
426    T: MutDenseData,
427{
428    fn index_mut(&mut self, i: usize) -> &mut Self::Output {
429        &mut (self.data.as_mut_slice())[self.offsets.at(i)]
430    }
431}
432
433pub type ChunkView<'a, T> = ChunkViewImpl<'a, &'a [T]>;
434pub type MutChunkView<'a, T> = ChunkViewImpl<'a, &'a mut [T]>;
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439    use diskann_utils::lazy_format;
440
441    /// This function is only callable with copyable types.
442    ///
443    /// This lets us test for types we expect to be `Copy`.
444    fn is_copyable<T: Copy>(_x: T) -> bool {
445        true
446    }
447
448    //////////////////
449    // ChunkOffsets //
450    //////////////////
451
452    #[test]
453    fn chunk_offset_happy_path() {
454        let offsets_raw: Vec<usize> = vec![0, 1, 3, 6, 10, 12, 13, 14];
455        let offsets = ChunkOffsetsView::new(offsets_raw.as_slice()).unwrap();
456
457        assert_eq!(offsets.len(), offsets_raw.len() - 1);
458        assert_eq!(offsets.dim(), *offsets_raw.last().unwrap());
459        assert!(!offsets.is_empty());
460
461        assert_eq!(offsets.at(0), 0..1);
462        assert_eq!(offsets.get(0).unwrap(), 0..1);
463        assert_eq!(offsets.at(1), 1..3);
464        assert_eq!(offsets.get(1).unwrap(), 1..3);
465        assert_eq!(offsets.at(2), 3..6);
466        assert_eq!(offsets.get(2).unwrap(), 3..6);
467        assert_eq!(offsets.at(3), 6..10);
468        assert_eq!(offsets.get(3).unwrap(), 6..10);
469        assert_eq!(offsets.at(4), 10..12);
470        assert_eq!(offsets.get(4).unwrap(), 10..12);
471        assert_eq!(offsets.at(5), 12..13);
472        assert_eq!(offsets.get(5).unwrap(), 12..13);
473        assert_eq!(offsets.at(6), 13..14);
474        assert_eq!(offsets.get(6).unwrap(), 13..14);
475
476        assert!(offsets.get(7).is_none());
477        assert!(offsets.get(8).is_none());
478
479        // Finally, make sure the type is copyable.
480        assert!(is_copyable(offsets));
481        // Make sure `as_slice()` properly round-trips.
482        assert_eq!(offsets.as_slice(), offsets_raw.as_slice());
483
484        // `to_owned`
485        let offsets_owned = offsets.to_owned();
486        assert_eq!(offsets_owned.as_slice(), offsets_raw.as_slice());
487        assert_ne!(
488            offsets_owned.as_slice().as_ptr(),
489            offsets_raw.as_slice().as_ptr()
490        );
491        assert_eq!(offsets_owned.dim, offsets.dim);
492
493        // `as_view`.
494        let offsets_view = offsets_owned.as_view();
495        assert_eq!(offsets_view, offsets);
496        // ensure that pointers are preserved.
497        assert_eq!(
498            offsets_view.as_slice().as_ptr(),
499            offsets_owned.as_slice().as_ptr()
500        );
501
502        // `max_chunk_dim`
503        assert_eq!(offsets.max_chunk_dim().get(), 4);
504    }
505
506    #[test]
507    fn chunk_offset_max_dim() {
508        let offsets = ChunkOffsetsView::new(&[0, 10, 11, 14]).unwrap();
509        assert_eq!(offsets.max_chunk_dim().get(), 10);
510
511        let offsets = ChunkOffsetsView::new(&[0, 1, 2, 3]).unwrap();
512        assert_eq!(offsets.max_chunk_dim().get(), 1);
513
514        let offsets = ChunkOffsetsView::new(&[0, 1, 2, 5, 20, 21]).unwrap();
515        assert_eq!(offsets.max_chunk_dim().get(), 15);
516    }
517
518    #[test]
519    #[should_panic(expected = "index 5 must be less than len 3")]
520    fn chunk_offset_indexing_panic() {
521        let offsets = ChunkOffsets::new(Box::new([0, 1, 2, 3])).unwrap();
522        assert!(offsets.get(5).is_none());
523
524        // panics
525        let _ = offsets.at(5);
526    }
527
528    // Construction errors.
529    #[test]
530    fn chunk_offset_construction_errors() {
531        // Pass an empty slice.
532        let offsets = ChunkOffsets::new(Box::new([]));
533        assert_eq!(
534            offsets.unwrap_err().to_string(),
535            "offsets must have a length of at least 2, found 0"
536        );
537
538        // Pass a slice with length 1.
539        let offsets = ChunkOffsets::new(Box::new([0]));
540        assert_eq!(
541            offsets.unwrap_err().to_string(),
542            "offsets must have a length of at least 2, found 1"
543        );
544
545        // Doesn't start with zero.
546        let offsets = ChunkOffsets::new(Box::new([10, 11, 12, 13]));
547        assert_eq!(
548            offsets.unwrap_err().to_string(),
549            "offsets must begin at 0, not 10"
550        );
551
552        // Non-monotonic cases - zero sized chunk
553        let offsets = ChunkOffsets::new(Box::new([0, 10, 20, 30, 30, 40, 41]));
554        assert_eq!(
555            offsets.unwrap_err().to_string(),
556            "offsets must be strictly increasing, instead entry 30 at position 3 \
557            is followed by 30"
558        );
559
560        // Non-monotonic cases - decreasing size
561        let offsets = ChunkOffsets::new(Box::new([0, 10, 9, 10, 20]));
562        assert_eq!(
563            offsets.unwrap_err().to_string(),
564            "offsets must be strictly increasing, instead entry 10 at position 1 \
565            is followed by 9"
566        );
567
568        // Non-monotonic cases - some dimension after the first is zero.
569        let offsets = ChunkOffsets::new(Box::new([0, 10, 11, 12, 0]));
570        assert_eq!(
571            offsets.unwrap_err().to_string(),
572            "offsets must be strictly increasing, instead entry 12 at position 3 \
573            is followed by 0"
574        );
575
576        // Non-monotonic cases - second entry is zero.
577        let offsets = ChunkOffsets::new(Box::new([0, 0, 11, 12, 20]));
578        assert_eq!(
579            offsets.unwrap_err().to_string(),
580            "offsets must be strictly increasing, instead entry 0 at position 0 \
581            is followed by 0"
582        );
583    }
584
585    //////////////////////
586    // partition builders //
587    //////////////////////
588
589    #[test]
590    fn partition_happy_path() {
591        let nz = |x: usize| NonZeroUsize::new(x).unwrap();
592
593        // Even split: 9 / 3 = 3 each.
594        let offsets = ChunkOffsets::partition(nz(9), nz(3)).unwrap();
595        assert_eq!(offsets.as_slice(), &[0, 3, 6, 9]);
596        assert_eq!(offsets.dim(), 9);
597        assert_eq!(offsets.len(), 3);
598
599        // Uneven split: 8 / 3 = 2 r 2 -> first two chunks get an extra element.
600        let offsets = ChunkOffsets::partition(nz(8), nz(3)).unwrap();
601        assert_eq!(offsets.as_slice(), &[0, 3, 6, 8]);
602
603        // Single chunk degenerate case.
604        let offsets = ChunkOffsets::partition(nz(5), nz(1)).unwrap();
605        assert_eq!(offsets.as_slice(), &[0, 5]);
606
607        // dimensions == num_pq_chunks: each chunk is size 1.
608        let offsets = ChunkOffsets::partition(nz(4), nz(4)).unwrap();
609        assert_eq!(offsets.as_slice(), &[0, 1, 2, 3, 4]);
610
611        // The view-into variant matches the owning constructor; num_chunks is
612        // inferred from `scratch.len() - 1`.
613        let mut scratch = [0usize; 4];
614        let view = ChunkOffsetsView::partition_into(nz(8), &mut scratch).unwrap();
615        assert_eq!(view.as_slice(), &[0, 3, 6, 8]);
616        assert_eq!(view.dim(), 8);
617        assert_eq!(view.len(), 3);
618        assert_eq!(scratch.as_slice(), &[0, 3, 6, 8]);
619    }
620
621    #[test]
622    fn partition_construction_errors() {
623        let nz = |x: usize| NonZeroUsize::new(x).unwrap();
624
625        // num_chunks > dim -> TooManyChunks (caught explicitly before partitioning).
626        let err = ChunkOffsets::partition(nz(3), nz(5)).unwrap_err();
627        assert!(
628            matches!(
629                err,
630                PartitionError {
631                    num_chunks: 5,
632                    dim: 3
633                }
634            ),
635            "expected TooManyChunks, got {err:?}"
636        );
637
638        // Scratch length < 2 -> ScratchTooSmall (cannot infer num_chunks).
639        let mut too_short = [0usize; 1];
640        let err = ChunkOffsetsView::partition_into(nz(8), &mut too_short).unwrap_err();
641        assert!(matches!(err, PartitionIntoError::ScratchTooSmall(1)));
642
643        // num_chunks > dim via the view builder too.
644        let mut scratch = [0usize; 6];
645        let err = ChunkOffsetsView::partition_into(nz(3), &mut scratch).unwrap_err();
646        assert!(matches!(
647            err,
648            PartitionIntoError::PartitionError(PartitionError {
649                num_chunks: 5,
650                dim: 3
651            })
652        ));
653    }
654
655    ///////////////
656    // ChunkView //
657    ///////////////
658
659    fn check_chunk_view<T>(
660        view: &ChunkViewImpl<'_, T>,
661        data: &[i32],
662        offsets: &[usize],
663        context: &dyn std::fmt::Display,
664    ) where
665        T: DenseData<Elem = i32>,
666    {
667        assert_eq!(view.len(), offsets.len() - 1, "{}", context);
668
669        // Ensure that each yielded slice matches that we retrieve manually.
670        for i in 0..view.len() {
671            let context = lazy_format!("start = {}, {}", i, context);
672            let start = offsets[i];
673            let stop = offsets[i + 1];
674
675            let expected = &data[start..stop];
676            let retrieved = &view[i];
677
678            assert_eq!(retrieved, expected, "{}", context);
679        }
680    }
681
682    #[test]
683    fn test_immutable_chunkview() {
684        let data: Vec<i32> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
685        //                        |-----|  |--|  |--------|  |
686        //                          c0      c1       c2      c3
687        let offsets: Vec<usize> = vec![0, 3, 5, 9, 10];
688
689        let chunks = ChunkOffsetsView::new(offsets.as_slice()).unwrap();
690        let chunk_view = ChunkView::new(data.as_slice(), chunks).unwrap();
691
692        assert_eq!(chunk_view.len(), offsets.len() - 1);
693        assert_eq!(chunk_view.len(), chunks.len());
694
695        assert!(is_copyable(chunk_view));
696        let context = lazy_format!("chunkview happy path");
697        check_chunk_view(&chunk_view, &data, &offsets, &context);
698    }
699
700    #[test]
701    fn test_chunkview_construction_error() {
702        let data: Vec<i32> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
703        //                        |-----|  |--|  |--------|  |
704        //                          c0      c1       c2      c3
705        let offsets: Vec<usize> = vec![0, 3, 5, 9]; // One too short.
706
707        let chunks = ChunkOffsetsView::new(offsets.as_slice()).unwrap();
708        let chunk_view = ChunkView::new(data.as_slice(), chunks);
709        assert!(chunk_view.is_err());
710        assert_eq!(
711            chunk_view.unwrap_err().to_string(),
712            "error in chunk view construction, got a slice of length 10 but \
713             the provided chunking schema expects a length of 9"
714        );
715    }
716
717    #[test]
718    fn test_mutable_chunkview() {
719        let mut data: Vec<i32> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
720        //                            |-----|  |--|  |--------|  |
721        //                              c0      c1       c2      c3
722        let offsets: Vec<usize> = vec![0, 3, 5, 9, 10];
723
724        // We need to clone the original data before constructing the mutable chunk view
725        // to avoid both a mutable and immutable borrow in the checking function.
726        let data_clone = data.clone();
727
728        let chunks = ChunkOffsetsView::new(offsets.as_slice()).unwrap();
729        let mut chunk_view = MutChunkView::new(data.as_mut_slice(), chunks).unwrap();
730
731        assert_eq!(chunk_view.len(), offsets.len() - 1);
732        assert_eq!(chunk_view.len(), chunks.len());
733
734        let context = lazy_format!("mutchunkview happy path");
735        check_chunk_view(&chunk_view, &data_clone, &offsets, &context);
736
737        // Make sure that we can assign through the view.
738        for i in 0..chunk_view.len() {
739            let i_i32: i32 = i.try_into().unwrap();
740
741            chunk_view[i].iter_mut().for_each(|d| *d = i_i32);
742        }
743
744        // chunk 0
745        assert_eq!(data[0], 0);
746        assert_eq!(data[1], 0);
747        assert_eq!(data[2], 0);
748
749        // chunk 1
750        assert_eq!(data[3], 1);
751        assert_eq!(data[4], 1);
752
753        // chunk 2
754        assert_eq!(data[5], 2);
755        assert_eq!(data[6], 2);
756        assert_eq!(data[7], 2);
757        assert_eq!(data[8], 2);
758
759        // chunk 3
760        assert_eq!(data[9], 3);
761    }
762}