Skip to main content

lance_encoding/encodings/logical/primitive/
fullzip_decode.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::any::Any;
5use std::collections::VecDeque;
6use std::fmt::Debug;
7use std::ops::Range;
8use std::sync::Arc;
9use std::vec;
10
11use arrow_buffer::ScalarBuffer;
12use bytes::Bytes;
13use futures::FutureExt;
14use futures::future::BoxFuture;
15use itertools::Itertools;
16use lance_core::Result;
17use lance_core::cache::{Context, DeepSizeOf};
18use lance_core::error::Error;
19
20use crate::EncodingsIo;
21use crate::buffer::LanceBuffer;
22use crate::compression::{
23    DecompressionStrategy, FixedPerValueDecompressor, VariablePerValueDecompressor,
24};
25use crate::data::{BlockInfo, DataBlockBuilder, FixedWidthDataBlock, VariableWidthBlock};
26use crate::decoder::{DecodePageTask, DecodedPage, StructuralPageDecoder};
27use crate::encodings::logical::primitive::fullzip::PerValueDataBlock;
28use crate::format::{EncodingUtils, metadata};
29use crate::repdef::{
30    ControlWordParser, DefinitionInterpretation, RepDefUnraveler, max_visible_level,
31};
32use crate::utils::bytepack::ByteUnpacker;
33
34use super::{
35    CachedPageData, NoCachedPageData, PageInitialization, PageInitializationBuffers, PageLoadTask,
36    StructuralPageScheduler,
37};
38
39#[derive(Debug, Clone, Copy)]
40struct FullZipRepIndexDetails {
41    buf_position: u64,
42    bytes_per_value: u64, // Will be 1, 2, 4, or 8
43}
44
45#[derive(Debug)]
46enum PerValueDecompressor {
47    Fixed(Arc<dyn FixedPerValueDecompressor>),
48    Variable(Arc<dyn VariablePerValueDecompressor>),
49}
50
51#[derive(Debug)]
52struct FullZipDecodeDetails {
53    value_decompressor: PerValueDecompressor,
54    def_meaning: Arc<[DefinitionInterpretation]>,
55    ctrl_word_parser: ControlWordParser,
56    max_rep: u16,
57    max_visible_def: u16,
58}
59
60/// Describes where FullZip byte ranges should be read from.
61///
62/// FullZip decoding always needs a list of byte ranges, but those bytes can come
63/// from two different places:
64/// - Remote I/O (normal path): ranges are fetched from the underlying `EncodingsIo`.
65/// - A prefetched full page (full scan fast path): the entire page has already been
66///   loaded once and ranges should be sliced from memory.
67///
68/// This abstraction keeps scheduling code focused on "which ranges are needed"
69/// instead of "how bytes are fetched", and it lets full-page scans avoid the
70/// two-stage rep-index -> data I/O pipeline.
71#[derive(Debug, Clone)]
72enum FullZipReadSource {
73    /// Fetch ranges from the storage backend through the encoding I/O interface.
74    Remote(Arc<dyn EncodingsIo>),
75    /// Slice ranges from an already-loaded FullZip page buffer.
76    PrefetchedPage { base_offset: u64, data: LanceBuffer },
77}
78
79impl FullZipReadSource {
80    /// Materialize the requested ranges as decode-ready `LanceBuffer`s.
81    ///
82    /// The returned buffers preserve the input range order.
83    fn fetch(
84        &self,
85        ranges: &[Range<u64>],
86        priority: u64,
87    ) -> BoxFuture<'static, Result<VecDeque<LanceBuffer>>> {
88        match self {
89            Self::Remote(io) => {
90                let io = io.clone();
91                let ranges = ranges.to_vec();
92                async move {
93                    let data = io.submit_request(ranges, priority).await?;
94                    Ok(data
95                        .into_iter()
96                        .map(|bytes| LanceBuffer::from_bytes(bytes, 1))
97                        .collect::<VecDeque<_>>())
98                }
99                .boxed()
100            }
101            Self::PrefetchedPage { base_offset, data } => {
102                let base_offset = *base_offset;
103                let data = data.clone();
104                let page_end = base_offset + data.len() as u64;
105                std::future::ready(
106                    ranges
107                        .iter()
108                        .map(|range| {
109                            if range.start > range.end
110                                || range.start < base_offset
111                                || range.end > page_end
112                            {
113                                return Err(Error::internal(format!(
114                                    "Requested range {:?} is outside page range {}..{}",
115                                    range, base_offset, page_end
116                                )));
117                            }
118                            let start = (range.start - base_offset) as usize;
119                            let len = (range.end - range.start) as usize;
120                            Ok(data.slice_with_length(start, len))
121                        })
122                        .collect::<Result<VecDeque<_>>>(),
123                )
124                .boxed()
125            }
126        }
127    }
128}
129
130/// A scheduler for full-zip encoded data
131///
132/// When the data type has a fixed-width then we simply need to map from
133/// row ranges to byte ranges using the fixed-width of the data type.
134///
135/// When the data type is variable-width or has any repetition then a
136/// repetition index is required.
137#[derive(Debug)]
138pub struct FullZipScheduler {
139    data_buf_position: u64,
140    pub(super) data_buf_size: u64,
141    rep_index: Option<FullZipRepIndexDetails>,
142    priority: u64,
143    rows_in_page: u64,
144    bits_per_offset: u8,
145    details: Arc<FullZipDecodeDetails>,
146    /// Cached state containing the decoded repetition index
147    cached_state: Option<Arc<FullZipCacheableState>>,
148    /// Whether repetition index metadata should be cached during initialize.
149    pub(super) enable_cache: bool,
150}
151
152impl FullZipScheduler {
153    pub(super) fn try_new(
154        buffer_offsets_and_sizes: &[(u64, u64)],
155        priority: u64,
156        rows_in_page: u64,
157        layout: &metadata::FullZipLayout,
158        decompressors: &dyn DecompressionStrategy,
159    ) -> Result<Self> {
160        let (data_buf_position, data_buf_size) = buffer_offsets_and_sizes[0];
161        let rep_index = buffer_offsets_and_sizes.get(1).map(|(pos, len)| {
162            let num_reps = rows_in_page + 1;
163            let bytes_per_rep = len / num_reps;
164            debug_assert_eq!(len % num_reps, 0);
165            debug_assert!(
166                bytes_per_rep == 1
167                    || bytes_per_rep == 2
168                    || bytes_per_rep == 4
169                    || bytes_per_rep == 8
170            );
171            FullZipRepIndexDetails {
172                buf_position: *pos,
173                bytes_per_value: bytes_per_rep,
174            }
175        });
176
177        let value_decompressor = match layout.details {
178            Some(metadata::full_zip_layout::Details::BitsPerValue(_)) => {
179                let decompressor = decompressors.create_fixed_per_value_decompressor(
180                    layout.value_compression.as_ref().unwrap(),
181                )?;
182                PerValueDecompressor::Fixed(decompressor.into())
183            }
184            Some(metadata::full_zip_layout::Details::BitsPerOffset(_)) => {
185                let decompressor = decompressors.create_variable_per_value_decompressor(
186                    layout.value_compression.as_ref().unwrap(),
187                )?;
188                PerValueDecompressor::Variable(decompressor.into())
189            }
190            None => {
191                panic!("Full-zip layout must have a `details` field");
192            }
193        };
194        let ctrl_word_parser = ControlWordParser::new(
195            layout.bits_rep.try_into().unwrap(),
196            layout.bits_def.try_into().unwrap(),
197        );
198        let def_meaning = layout
199            .layers
200            .iter()
201            .map(|l| EncodingUtils::repdef_layer_to_def_interp(*l))
202            .collect::<Vec<_>>();
203
204        let max_rep = def_meaning.iter().filter(|d| d.is_list()).count() as u16;
205        let max_visible_def = max_visible_level(&def_meaning);
206
207        let bits_per_offset = match layout.details {
208            Some(metadata::full_zip_layout::Details::BitsPerValue(_)) => 32,
209            Some(metadata::full_zip_layout::Details::BitsPerOffset(bits_per_offset)) => {
210                bits_per_offset as u8
211            }
212            None => panic!("Full-zip layout must have a `details` field"),
213        };
214
215        let details = Arc::new(FullZipDecodeDetails {
216            value_decompressor,
217            def_meaning: def_meaning.into(),
218            ctrl_word_parser,
219            max_rep,
220            max_visible_def,
221        });
222        Ok(Self {
223            data_buf_position,
224            data_buf_size,
225            rep_index,
226            details,
227            priority,
228            rows_in_page,
229            bits_per_offset,
230            cached_state: None,
231            enable_cache: false,
232        })
233    }
234
235    fn covers_entire_page(ranges: &[Range<u64>], rows_in_page: u64) -> bool {
236        if ranges.is_empty() {
237            return false;
238        }
239        let mut expected_start = 0;
240        for range in ranges {
241            if range.start != expected_start || range.end > rows_in_page || range.end < range.start
242            {
243                return false;
244            }
245            expected_start = range.end;
246        }
247        expected_start == rows_in_page
248    }
249
250    fn create_page_load_task(
251        io_future: BoxFuture<'static, Result<Vec<Bytes>>>,
252        num_rows: u64,
253        details: Arc<FullZipDecodeDetails>,
254        bits_per_offset: u8,
255    ) -> PageLoadTask {
256        let load_task = async move {
257            let buffers = io_future.await?;
258            let data = buffers
259                .into_iter()
260                .map(|bytes| LanceBuffer::from_bytes(bytes, 1))
261                .collect::<VecDeque<_>>();
262            Self::create_decoder(details, data, num_rows, bits_per_offset)
263        }
264        .boxed();
265        PageLoadTask {
266            decoder_fut: load_task,
267            num_rows,
268        }
269    }
270
271    /// Creates a decoder from the loaded data
272    fn create_decoder(
273        details: Arc<FullZipDecodeDetails>,
274        data: VecDeque<LanceBuffer>,
275        num_rows: u64,
276        bits_per_offset: u8,
277    ) -> Result<Box<dyn StructuralPageDecoder>> {
278        match &details.value_decompressor {
279            PerValueDecompressor::Fixed(decompressor) => {
280                let bits_per_value = decompressor.bits_per_value();
281                if bits_per_value % 8 != 0 {
282                    return Err(lance_core::Error::not_supported_source("Bit-packed full-zip encoding (non-byte-aligned values) is not yet implemented".into()));
283                }
284                let bytes_per_value = bits_per_value / 8;
285                let total_bytes_per_value =
286                    bytes_per_value as usize + details.ctrl_word_parser.bytes_per_word();
287                // total_bytes_per_value == 0 is valid for constant-null FSL pages written by
288                // earlier encoders that produced bits_per_value=0 with no ctrl-word bytes.
289                // FixedFullZipDecoder::drain handles this case by producing AllNull output
290                // without touching the (empty) data buffer.
291                Ok(Box::new(FixedFullZipDecoder {
292                    details,
293                    data,
294                    num_rows,
295                    offset_in_current: 0,
296                    bytes_per_value: bytes_per_value as usize,
297                    total_bytes_per_value,
298                }) as Box<dyn StructuralPageDecoder>)
299            }
300            PerValueDecompressor::Variable(_decompressor) => {
301                Ok(Box::new(VariableFullZipDecoder::new(
302                    details,
303                    data,
304                    num_rows,
305                    bits_per_offset,
306                    bits_per_offset,
307                )?))
308            }
309        }
310    }
311
312    /// Extracts byte ranges from a repetition index buffer
313    /// The buffer contains pairs of (start, end) values for each range
314    fn extract_byte_ranges_from_pairs(
315        buffer: LanceBuffer,
316        bytes_per_value: u64,
317        data_buf_position: u64,
318    ) -> Vec<Range<u64>> {
319        ByteUnpacker::new(buffer, bytes_per_value as usize)
320            .chunks(2)
321            .into_iter()
322            .map(|mut c| {
323                let start = c.next().unwrap() + data_buf_position;
324                let end = c.next().unwrap() + data_buf_position;
325                start..end
326            })
327            .collect::<Vec<_>>()
328    }
329
330    /// Extracts byte ranges from a cached repetition index buffer
331    /// The buffer contains all values and we need to extract specific ranges
332    fn extract_byte_ranges_from_cached(
333        buffer: &LanceBuffer,
334        ranges: &[Range<u64>],
335        bytes_per_value: u64,
336        data_buf_position: u64,
337    ) -> Vec<Range<u64>> {
338        ranges
339            .iter()
340            .map(|r| {
341                let start_offset = (r.start * bytes_per_value) as usize;
342                let end_offset = (r.end * bytes_per_value) as usize;
343
344                let start_slice = &buffer[start_offset..start_offset + bytes_per_value as usize];
345                let start_val =
346                    ByteUnpacker::new(start_slice.iter().copied(), bytes_per_value as usize)
347                        .next()
348                        .unwrap();
349
350                let end_slice = &buffer[end_offset..end_offset + bytes_per_value as usize];
351                let end_val =
352                    ByteUnpacker::new(end_slice.iter().copied(), bytes_per_value as usize)
353                        .next()
354                        .unwrap();
355
356                (data_buf_position + start_val)..(data_buf_position + end_val)
357            })
358            .collect()
359    }
360
361    /// Computes the ranges in the repetition index that need to be loaded
362    fn compute_rep_index_ranges(
363        ranges: &[Range<u64>],
364        rep_index: &FullZipRepIndexDetails,
365    ) -> Vec<Range<u64>> {
366        ranges
367            .iter()
368            .flat_map(|r| {
369                let first_val_start =
370                    rep_index.buf_position + (r.start * rep_index.bytes_per_value);
371                let first_val_end = first_val_start + rep_index.bytes_per_value;
372                let last_val_start = rep_index.buf_position + (r.end * rep_index.bytes_per_value);
373                let last_val_end = last_val_start + rep_index.bytes_per_value;
374                [first_val_start..first_val_end, last_val_start..last_val_end]
375            })
376            .collect()
377    }
378
379    /// Schedules ranges in the presence of a repetition index
380    fn schedule_ranges_rep(
381        &self,
382        ranges: &[Range<u64>],
383        io: &Arc<dyn EncodingsIo>,
384        rep_index: FullZipRepIndexDetails,
385    ) -> Result<Vec<PageLoadTask>> {
386        let num_rows = ranges.iter().map(|r| r.end - r.start).sum();
387        let data_buf_position = self.data_buf_position;
388        let priority = self.priority;
389        let details = self.details.clone();
390        let bits_per_offset = self.bits_per_offset;
391
392        if Self::covers_entire_page(ranges, self.rows_in_page) {
393            let full_range = self.data_buf_position..(self.data_buf_position + self.data_buf_size);
394            let page_data = io.submit_single(full_range.clone(), priority);
395            let load_task = async move {
396                let page_data = page_data.await?;
397                let source = FullZipReadSource::PrefetchedPage {
398                    base_offset: full_range.start,
399                    data: LanceBuffer::from_bytes(page_data, 1),
400                };
401                let read_ranges = vec![full_range];
402                let data = source.fetch(&read_ranges, priority).await?;
403                Self::create_decoder(details, data, num_rows, bits_per_offset)
404            }
405            .boxed();
406            let page_load_task = PageLoadTask {
407                decoder_fut: load_task,
408                num_rows,
409            };
410            return Ok(vec![page_load_task]);
411        }
412
413        if let Some(cached_state) = &self.cached_state {
414            let byte_ranges = Self::extract_byte_ranges_from_cached(
415                &cached_state.rep_index_buffer,
416                ranges,
417                rep_index.bytes_per_value,
418                data_buf_position,
419            );
420            let io_future = io.submit_request(byte_ranges, priority);
421            let page_load_task =
422                Self::create_page_load_task(io_future, num_rows, details, bits_per_offset);
423            return Ok(vec![page_load_task]);
424        }
425
426        let rep_ranges = Self::compute_rep_index_ranges(ranges, &rep_index);
427        let rep_data = io.submit_request(rep_ranges, priority);
428        let io_clone = io.clone();
429        let load_task = async move {
430            let rep_data = rep_data.await?;
431            let rep_buffer = LanceBuffer::concat(
432                &rep_data
433                    .into_iter()
434                    .map(|d| LanceBuffer::from_bytes(d, 1))
435                    .collect::<Vec<_>>(),
436            );
437            let byte_ranges = Self::extract_byte_ranges_from_pairs(
438                rep_buffer,
439                rep_index.bytes_per_value,
440                data_buf_position,
441            );
442            let source = FullZipReadSource::Remote(io_clone);
443            let data = source.fetch(&byte_ranges, priority).await?;
444            Self::create_decoder(details, data, num_rows, bits_per_offset)
445        }
446        .boxed();
447        let page_load_task = PageLoadTask {
448            decoder_fut: load_task,
449            num_rows,
450        };
451        Ok(vec![page_load_task])
452    }
453
454    // In the simple case there is no repetition and we just have large fixed-width
455    // rows of data.  We can just map row ranges to byte ranges directly using the
456    // fixed-width of the data type.
457    fn schedule_ranges_simple(
458        &self,
459        ranges: &[Range<u64>],
460        io: &Arc<dyn EncodingsIo>,
461    ) -> Result<Vec<PageLoadTask>> {
462        // Convert row ranges to item ranges (i.e. multiply by items per row)
463        let num_rows = ranges.iter().map(|r| r.end - r.start).sum();
464
465        let PerValueDecompressor::Fixed(decompressor) = &self.details.value_decompressor else {
466            unreachable!()
467        };
468
469        // Convert item ranges to byte ranges (i.e. multiply by bytes per item)
470        let bits_per_value = decompressor.bits_per_value();
471        if !bits_per_value.is_multiple_of(8) {
472            return Err(Error::invalid_input_source(
473                format!(
474                    "Full-zip fixed-width values must be byte aligned, got {} bits per value",
475                    bits_per_value
476                )
477                .into(),
478            ));
479        }
480        let bytes_per_value = bits_per_value / 8;
481        let bytes_per_cw = self.details.ctrl_word_parser.bytes_per_word();
482        let total_bytes_per_value = bytes_per_value + bytes_per_cw as u64;
483        let byte_ranges = ranges
484            .iter()
485            .map(|r| {
486                debug_assert!(r.end <= self.rows_in_page);
487                let start = self.data_buf_position + r.start * total_bytes_per_value;
488                let end = self.data_buf_position + r.end * total_bytes_per_value;
489                start..end
490            })
491            .collect::<Vec<_>>();
492
493        let io_future = io.submit_request(byte_ranges, self.priority);
494        let page_load_task = Self::create_page_load_task(
495            io_future,
496            num_rows,
497            self.details.clone(),
498            self.bits_per_offset,
499        );
500        Ok(vec![page_load_task])
501    }
502}
503
504/// Cacheable state for FullZip encoding, storing the decoded repetition index
505#[derive(Debug)]
506struct FullZipCacheableState {
507    /// The raw repetition index buffer for future decoding
508    rep_index_buffer: LanceBuffer,
509}
510
511impl DeepSizeOf for FullZipCacheableState {
512    fn deep_size_of_children(&self, _context: &mut Context) -> usize {
513        self.rep_index_buffer.len()
514    }
515}
516
517impl CachedPageData for FullZipCacheableState {
518    fn as_arc_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync + 'static> {
519        self
520    }
521}
522
523impl StructuralPageScheduler for FullZipScheduler {
524    fn needs_initialization(&self) -> bool {
525        self.enable_cache && self.rep_index.is_some()
526    }
527
528    fn init_layout(&self) -> Result<PageInitialization> {
529        let repetition_index_range = if self.enable_cache
530            && let Some(rep_index) = self.rep_index
531        {
532            let total_size = (self.rows_in_page + 1) * rep_index.bytes_per_value;
533            Some(rep_index.buf_position..(rep_index.buf_position + total_size))
534        } else {
535            None
536        };
537        Ok(PageInitialization::new([repetition_index_range]))
538    }
539
540    fn init_from_buffers<'a>(
541        &'a mut self,
542        buffers: PageInitializationBuffers,
543        _io: &Arc<dyn EncodingsIo>,
544    ) -> BoxFuture<'a, Result<Arc<dyn CachedPageData>>> {
545        let [rep_index_buffer] = match buffers.try_into_array("full-zip") {
546            Ok(buffers) => buffers,
547            Err(error) => return std::future::ready(Err(error)).boxed(),
548        };
549        if let Some(rep_index_buffer) = rep_index_buffer {
550            let state = Arc::new(FullZipCacheableState {
551                rep_index_buffer: LanceBuffer::from_bytes(rep_index_buffer, 1),
552            });
553            std::future::ready(Ok(state as Arc<dyn CachedPageData>)).boxed()
554        } else {
555            std::future::ready(Ok(Arc::new(NoCachedPageData) as Arc<dyn CachedPageData>)).boxed()
556        }
557    }
558
559    /// Loads previously cached repetition index data from the cache system.
560    /// This method is called when a scheduler instance needs to use cached data
561    /// that was initialized by another instance or in a previous operation.
562    fn try_load(&mut self, cache: &Arc<dyn CachedPageData>) -> Result<()> {
563        self.cached_state = Some(cache.clone().as_arc_any().downcast().map_err(|_| {
564            Error::invalid_input_source("Cached full-zip page data has an unexpected type".into())
565        })?);
566        Ok(())
567    }
568
569    fn schedule_ranges(
570        &self,
571        ranges: &[Range<u64>],
572        io: &Arc<dyn EncodingsIo>,
573    ) -> Result<Vec<PageLoadTask>> {
574        if let Some(rep_index) = self.rep_index {
575            self.schedule_ranges_rep(ranges, io, rep_index)
576        } else {
577            self.schedule_ranges_simple(ranges, io)
578        }
579    }
580}
581
582/// A decoder for full-zip encoded data when the data has a fixed-width
583///
584/// Here we need to unzip the control words from the values themselves and
585/// then decompress the requested values.
586///
587/// We use a PerValueDecompressor because we will only be decompressing the
588/// requested data.  This decoder / scheduler does not do any read amplification.
589#[derive(Debug)]
590struct FixedFullZipDecoder {
591    details: Arc<FullZipDecodeDetails>,
592    data: VecDeque<LanceBuffer>,
593    offset_in_current: usize,
594    bytes_per_value: usize,
595    total_bytes_per_value: usize,
596    num_rows: u64,
597}
598
599impl FixedFullZipDecoder {
600    fn slice_next_task(&mut self, num_rows: u64) -> Result<FullZipDecodeTaskItem> {
601        debug_assert!(num_rows > 0);
602        let cur_buf = self.data.front_mut().ok_or_else(|| {
603            Error::corrupt_file_named(
604                "fixed_full_zip",
605                format!("page data ran out with {num_rows} row(s) left to decode"),
606            )
607        })?;
608        let start = self.offset_in_current;
609        if self.details.ctrl_word_parser.has_rep() {
610            // This is a slightly slower path.  In order to figure out where to split we need to
611            // examine the rep index so we can convert num_lists to num_rows
612            let mut rows_started = 0;
613            // We always need at least one value.  Now loop through until we have passed num_rows
614            // values
615            let mut num_items = 0;
616            let bytes_per_word = self.details.ctrl_word_parser.bytes_per_word();
617            while self.offset_in_current < cur_buf.len() {
618                // The item walk is driven by the control words themselves, so a page that
619                // disagrees with its own layout can step past the end of the buffer.  Check
620                // before every read and every advance so that a malformed page is reported
621                // instead of panicking in `slice_with_length`.
622                let bytes_avail = cur_buf.len() - self.offset_in_current;
623                if bytes_avail < bytes_per_word {
624                    return Err(Error::corrupt_file_named(
625                        "fixed_full_zip",
626                        format!(
627                            "truncated control word: {bytes_avail} byte(s) remain in the page \
628                             buffer but a control word requires {bytes_per_word}"
629                        ),
630                    ));
631                }
632                let control = self.details.ctrl_word_parser.parse_desc(
633                    &cur_buf[self.offset_in_current..],
634                    self.details.max_rep,
635                    self.details.max_visible_def,
636                );
637                if control.is_new_row {
638                    if rows_started == num_rows {
639                        break;
640                    }
641                    rows_started += 1;
642                }
643                num_items += 1;
644                let bytes_in_item = if control.is_visible {
645                    self.total_bytes_per_value
646                } else {
647                    bytes_per_word
648                };
649                if bytes_in_item > bytes_avail {
650                    return Err(Error::corrupt_file_named(
651                        "fixed_full_zip",
652                        format!(
653                            "truncated item: {bytes_avail} byte(s) remain in the page buffer at \
654                             offset {} but the item requires {bytes_in_item}",
655                            self.offset_in_current
656                        ),
657                    ));
658                }
659                self.offset_in_current += bytes_in_item;
660            }
661
662            let task_slice = cur_buf.slice_with_length(start, self.offset_in_current - start);
663            if self.offset_in_current == cur_buf.len() {
664                self.data.pop_front();
665                self.offset_in_current = 0;
666            }
667
668            Ok(FullZipDecodeTaskItem {
669                data: PerValueDataBlock::Fixed(FixedWidthDataBlock {
670                    data: task_slice,
671                    bits_per_value: self.bytes_per_value as u64 * 8,
672                    num_values: num_items,
673                    block_info: BlockInfo::new(),
674                }),
675                rows_in_buf: rows_started,
676            })
677        } else {
678            // If there's no repetition we can calculate the slicing point by just multiplying
679            // the number of rows by the total bytes per value
680            let cur_buf = self.data.front_mut().unwrap();
681            let bytes_avail = cur_buf.len() - self.offset_in_current;
682            let offset_in_cur = self.offset_in_current;
683
684            let bytes_needed = num_rows as usize * self.total_bytes_per_value;
685            let mut rows_taken = num_rows;
686            let task_slice = if bytes_needed >= bytes_avail {
687                self.offset_in_current = 0;
688                rows_taken = bytes_avail as u64 / self.total_bytes_per_value as u64;
689                self.data
690                    .pop_front()
691                    .unwrap()
692                    .slice_with_length(offset_in_cur, bytes_avail)
693            } else {
694                self.offset_in_current += bytes_needed;
695                cur_buf.slice_with_length(offset_in_cur, bytes_needed)
696            };
697            Ok(FullZipDecodeTaskItem {
698                data: PerValueDataBlock::Fixed(FixedWidthDataBlock {
699                    data: task_slice,
700                    bits_per_value: self.bytes_per_value as u64 * 8,
701                    num_values: rows_taken,
702                    block_info: BlockInfo::new(),
703                }),
704                rows_in_buf: rows_taken,
705            })
706        }
707    }
708}
709
710impl StructuralPageDecoder for FixedFullZipDecoder {
711    fn drain(&mut self, num_rows: u64) -> Result<Box<dyn DecodePageTask>> {
712        if self.total_bytes_per_value == 0 {
713            // No bytes per row: constant-null page with no ctrl-word bytes.
714            // The decompressor (ConstantDecompressor) ignores its input and returns AllNull.
715            return Ok(Box::new(FixedFullZipDecodeTask {
716                details: self.details.clone(),
717                data: vec![FullZipDecodeTaskItem {
718                    data: PerValueDataBlock::Fixed(FixedWidthDataBlock {
719                        data: LanceBuffer::empty(),
720                        bits_per_value: 0,
721                        num_values: num_rows,
722                        block_info: BlockInfo::new(),
723                    }),
724                    rows_in_buf: num_rows,
725                }],
726                bytes_per_value: 0,
727                num_rows: num_rows as usize,
728            }));
729        }
730        let mut task_data = Vec::with_capacity(self.data.len());
731        let mut remaining = num_rows;
732        while remaining > 0 {
733            let task_item = self.slice_next_task(remaining)?;
734            remaining -= task_item.rows_in_buf;
735            task_data.push(task_item);
736        }
737        Ok(Box::new(FixedFullZipDecodeTask {
738            details: self.details.clone(),
739            data: task_data,
740            bytes_per_value: self.bytes_per_value,
741            num_rows: num_rows as usize,
742        }))
743    }
744
745    fn num_rows(&self) -> u64 {
746        self.num_rows
747    }
748}
749
750/// A decoder for full-zip encoded data when the data has a variable-width
751///
752/// Here we need to unzip the control words AND lengths from the values and
753/// then decompress the requested values.
754#[derive(Debug)]
755struct VariableFullZipDecoder {
756    details: Arc<FullZipDecodeDetails>,
757    decompressor: Arc<dyn VariablePerValueDecompressor>,
758    data: LanceBuffer,
759    offsets: LanceBuffer,
760    rep: ScalarBuffer<u16>,
761    def: ScalarBuffer<u16>,
762    repdef_starts: Vec<usize>,
763    data_starts: Vec<usize>,
764    offset_starts: Vec<usize>,
765    visible_item_counts: Vec<u64>,
766    bits_per_offset: u8,
767    current_idx: usize,
768    num_rows: u64,
769}
770
771impl VariableFullZipDecoder {
772    fn new(
773        details: Arc<FullZipDecodeDetails>,
774        data: VecDeque<LanceBuffer>,
775        num_rows: u64,
776        in_bits_per_length: u8,
777        out_bits_per_offset: u8,
778    ) -> Result<Self> {
779        let decompressor = match details.value_decompressor {
780            PerValueDecompressor::Variable(ref d) => d.clone(),
781            _ => unreachable!(),
782        };
783
784        assert_eq!(in_bits_per_length % 8, 0);
785        assert!(out_bits_per_offset == 32 || out_bits_per_offset == 64);
786
787        let mut decoder = Self {
788            details,
789            decompressor,
790            data: LanceBuffer::empty(),
791            offsets: LanceBuffer::empty(),
792            rep: LanceBuffer::empty().borrow_to_typed_slice(),
793            def: LanceBuffer::empty().borrow_to_typed_slice(),
794            bits_per_offset: out_bits_per_offset,
795            repdef_starts: Vec::with_capacity(num_rows as usize + 1),
796            data_starts: Vec::with_capacity(num_rows as usize + 1),
797            offset_starts: Vec::with_capacity(num_rows as usize + 1),
798            visible_item_counts: Vec::with_capacity(num_rows as usize + 1),
799            current_idx: 0,
800            num_rows,
801        };
802
803        // There's no great time to do this and this is the least worst time.  If we don't unzip then
804        // we can't slice the data during the decode phase.  This is because we need the offsets to be
805        // unpacked to know where the values start and end.
806        //
807        // We don't want to unzip on the decode thread because that is a single-threaded path
808        // We don't want to unzip on the scheduling thread because that is a single-threaded path
809        //
810        // Fortunately, we know variable length data will always be read indirectly and so we can do it
811        // here, which should be on the indirect thread.  The primary disadvantage to doing it here is that
812        // we load all the data into memory and then throw it away only to load it all into memory again during
813        // the decode.
814        //
815        // There are some alternatives to investigate:
816        //   - Instead of just reading the beginning and end of the rep index we could read the entire
817        //     range in between.  This will give us the break points that we need for slicing and won't increase
818        //     the number of IOPs but it will mean we are doing more total I/O and we need to load the rep index
819        //     even when doing a full scan.
820        //   - We could force each decode task to do a full unzip of all the data.  Each decode task now
821        //     has to do more work but the work is all fused.
822        //   - We could just try doing this work on the decode thread and see if it is a problem.
823        decoder.unzip(data, in_bits_per_length, out_bits_per_offset, num_rows)?;
824
825        Ok(decoder)
826    }
827
828    fn slice_batch_data_and_rebase_offsets_typed<T>(
829        data: &LanceBuffer,
830        offsets: &LanceBuffer,
831    ) -> Result<(LanceBuffer, LanceBuffer)>
832    where
833        T: arrow_buffer::ArrowNativeType
834            + Copy
835            + PartialOrd
836            + std::ops::Sub<Output = T>
837            + std::fmt::Display
838            + TryInto<usize>,
839    {
840        let offsets_slice = offsets.borrow_to_typed_slice::<T>();
841        let offsets_slice = offsets_slice.as_ref();
842        if offsets_slice.is_empty() {
843            return Err(Error::internal(
844                "Variable offsets cannot be empty".to_string(),
845            ));
846        }
847
848        let base = offsets_slice[0];
849        let end = *offsets_slice.last().unwrap();
850        if end < base {
851            return Err(Error::internal(format!(
852                "Invalid variable offsets: end ({end}) is less than base ({base})"
853            )));
854        }
855
856        let data_start = base.try_into().map_err(|_| {
857            Error::internal(format!("Variable offset ({base}) does not fit into usize"))
858        })?;
859        let data_end = end.try_into().map_err(|_| {
860            Error::internal(format!("Variable offset ({end}) does not fit into usize"))
861        })?;
862        if data_end > data.len() {
863            return Err(Error::internal(format!(
864                "Invalid variable offsets: end ({data_end}) exceeds data len ({})",
865                data.len()
866            )));
867        }
868
869        let mut rebased_offsets = Vec::with_capacity(offsets_slice.len());
870        for &offset in offsets_slice {
871            if offset < base {
872                return Err(Error::internal(format!(
873                    "Invalid variable offsets: offset ({offset}) is less than base ({base})"
874                )));
875            }
876            rebased_offsets.push(offset - base);
877        }
878
879        let sliced_data = data.slice_with_length(data_start, data_end - data_start);
880        // Copy into a compact buffer so each output batch owns only what it references.
881        let sliced_data = LanceBuffer::copy_slice(&sliced_data);
882        let rebased_offsets = LanceBuffer::reinterpret_vec(rebased_offsets);
883        Ok((sliced_data, rebased_offsets))
884    }
885
886    fn slice_batch_data_and_rebase_offsets(
887        data: &LanceBuffer,
888        offsets: &LanceBuffer,
889        bits_per_offset: u8,
890    ) -> Result<(LanceBuffer, LanceBuffer)> {
891        match bits_per_offset {
892            32 => Self::slice_batch_data_and_rebase_offsets_typed::<u32>(data, offsets),
893            64 => Self::slice_batch_data_and_rebase_offsets_typed::<u64>(data, offsets),
894            _ => Err(Error::internal(format!(
895                "Unsupported bits_per_offset={bits_per_offset}"
896            ))),
897        }
898    }
899
900    /// Reads a single length prefix from the front of `data`.
901    ///
902    /// The bytes come from the file. A page whose item walk ends with a partial
903    /// trailing item leaves fewer than `bits_per_offset / 8` bytes here, so this
904    /// is bounds checked and reports a corrupt file rather than reading past the
905    /// end of the buffer.
906    fn parse_length(data: &[u8], bits_per_offset: u8) -> Result<u64> {
907        let width = bits_per_offset as usize / 8;
908        if data.len() < width {
909            return Err(Error::corrupt_file_named(
910                "variable_full_zip",
911                format!(
912                    "truncated length prefix: {} byte(s) remain in the page buffer but a \
913                     {}-bit length prefix requires {}",
914                    data.len(),
915                    bits_per_offset,
916                    width
917                ),
918            ));
919        }
920        Ok(match bits_per_offset {
921            8 => data[0] as u64,
922            16 => u16::from_le_bytes(data[..2].try_into().unwrap()) as u64,
923            32 => u32::from_le_bytes(data[..4].try_into().unwrap()) as u64,
924            64 => u64::from_le_bytes(data[..8].try_into().unwrap()),
925            _ => unreachable!(),
926        })
927    }
928
929    fn unzip(
930        &mut self,
931        data: VecDeque<LanceBuffer>,
932        in_bits_per_length: u8,
933        out_bits_per_offset: u8,
934        num_rows: u64,
935    ) -> Result<()> {
936        // This undercounts if there are lists but, at this point, we don't really know how many items we have
937        let mut rep = Vec::with_capacity(num_rows as usize);
938        let mut def = Vec::with_capacity(num_rows as usize);
939        let bytes_cw = self.details.ctrl_word_parser.bytes_per_word() * num_rows as usize;
940
941        // This undercounts if there are lists
942        // It can also overcount if there are invisible items
943        let bytes_per_offset = out_bits_per_offset as usize / 8;
944        let bytes_offsets = bytes_per_offset * (num_rows as usize + 1);
945        let mut offsets_data = Vec::with_capacity(bytes_offsets);
946
947        let bytes_per_length = in_bits_per_length as usize / 8;
948        let bytes_lengths = bytes_per_length * num_rows as usize;
949
950        let bytes_data = data.iter().map(|d| d.len()).sum::<usize>();
951        // This overcounts since bytes_lengths and bytes_cw are undercounts
952        // It can also undercount if there are invisible items (hence the saturating_sub)
953        let mut unzipped_data =
954            Vec::with_capacity((bytes_data - bytes_cw).saturating_sub(bytes_lengths));
955
956        let mut current_offset = 0_u64;
957        let mut visible_item_count = 0_u64;
958        for databuf in data.into_iter() {
959            let mut databuf = databuf.as_ref();
960            while !databuf.is_empty() {
961                let data_start = unzipped_data.len();
962                let offset_start = offsets_data.len();
963                // We might have only-rep or only-def, neither, or both.  They move at the same
964                // speed though so we only need one index into it
965                let repdef_start = rep.len().max(def.len());
966                // TODO: Kind of inefficient we parse the control word twice here
967                let ctrl_desc = self.details.ctrl_word_parser.parse_desc(
968                    databuf,
969                    self.details.max_rep,
970                    self.details.max_visible_def,
971                );
972                self.details
973                    .ctrl_word_parser
974                    .parse(databuf, &mut rep, &mut def);
975                databuf = &databuf[self.details.ctrl_word_parser.bytes_per_word()..];
976
977                if ctrl_desc.is_new_row {
978                    self.repdef_starts.push(repdef_start);
979                    self.data_starts.push(data_start);
980                    self.offset_starts.push(offset_start);
981                    self.visible_item_counts.push(visible_item_count);
982                }
983                if ctrl_desc.is_visible {
984                    visible_item_count += 1;
985                    if ctrl_desc.is_valid_item {
986                        let length = Self::parse_length(databuf, in_bits_per_length)?;
987                        match out_bits_per_offset {
988                            32 => offsets_data
989                                .extend_from_slice(&(current_offset as u32).to_le_bytes()),
990                            64 => offsets_data.extend_from_slice(&current_offset.to_le_bytes()),
991                            _ => unreachable!(),
992                        };
993                        databuf = &databuf[bytes_per_offset..];
994                        if length as usize > databuf.len() {
995                            return Err(Error::corrupt_file_named(
996                                "variable_full_zip",
997                                format!(
998                                    "item length {} exceeds the {} byte(s) remaining in the \
999                                     page buffer",
1000                                    length,
1001                                    databuf.len()
1002                                ),
1003                            ));
1004                        }
1005                        unzipped_data.extend_from_slice(&databuf[..length as usize]);
1006                        databuf = &databuf[length as usize..];
1007                        current_offset += length;
1008                    } else {
1009                        // Null items still get an offset
1010                        match out_bits_per_offset {
1011                            32 => offsets_data
1012                                .extend_from_slice(&(current_offset as u32).to_le_bytes()),
1013                            64 => offsets_data.extend_from_slice(&current_offset.to_le_bytes()),
1014                            _ => unreachable!(),
1015                        }
1016                    }
1017                }
1018            }
1019        }
1020        self.repdef_starts.push(rep.len().max(def.len()));
1021        self.data_starts.push(unzipped_data.len());
1022        self.offset_starts.push(offsets_data.len());
1023        self.visible_item_counts.push(visible_item_count);
1024        match out_bits_per_offset {
1025            32 => offsets_data.extend_from_slice(&(current_offset as u32).to_le_bytes()),
1026            64 => offsets_data.extend_from_slice(&current_offset.to_le_bytes()),
1027            _ => unreachable!(),
1028        };
1029        self.rep = ScalarBuffer::from(rep);
1030        self.def = ScalarBuffer::from(def);
1031        self.data = LanceBuffer::from(unzipped_data);
1032        self.offsets = LanceBuffer::from(offsets_data);
1033        Ok(())
1034    }
1035}
1036
1037impl StructuralPageDecoder for VariableFullZipDecoder {
1038    fn drain(&mut self, num_rows: u64) -> Result<Box<dyn DecodePageTask>> {
1039        let start = self.current_idx;
1040        let end = start + num_rows as usize;
1041
1042        let offset_start = self.offset_starts[start];
1043        let offset_end = self.offset_starts[end] + (self.bits_per_offset as usize / 8);
1044        let offsets = self
1045            .offsets
1046            .slice_with_length(offset_start, offset_end - offset_start);
1047        // Keep each batch's variable data buffer bounded to the selected rows.
1048        let (data, offsets) =
1049            Self::slice_batch_data_and_rebase_offsets(&self.data, &offsets, self.bits_per_offset)?;
1050
1051        let repdef_start = self.repdef_starts[start];
1052        let repdef_end = self.repdef_starts[end];
1053        let rep = if self.rep.is_empty() {
1054            self.rep.clone()
1055        } else {
1056            self.rep.slice(repdef_start, repdef_end - repdef_start)
1057        };
1058        let def = if self.def.is_empty() {
1059            self.def.clone()
1060        } else {
1061            self.def.slice(repdef_start, repdef_end - repdef_start)
1062        };
1063
1064        let visible_item_counts_start = self.visible_item_counts[start];
1065        let visible_item_counts_end = self.visible_item_counts[end];
1066        let num_visible_items = visible_item_counts_end - visible_item_counts_start;
1067
1068        self.current_idx += num_rows as usize;
1069
1070        Ok(Box::new(VariableFullZipDecodeTask {
1071            details: self.details.clone(),
1072            decompressor: self.decompressor.clone(),
1073            data,
1074            offsets,
1075            bits_per_offset: self.bits_per_offset,
1076            num_visible_items,
1077            rep,
1078            def,
1079        }))
1080    }
1081
1082    fn num_rows(&self) -> u64 {
1083        self.num_rows
1084    }
1085}
1086
1087#[derive(Debug)]
1088struct VariableFullZipDecodeTask {
1089    details: Arc<FullZipDecodeDetails>,
1090    decompressor: Arc<dyn VariablePerValueDecompressor>,
1091    data: LanceBuffer,
1092    offsets: LanceBuffer,
1093    bits_per_offset: u8,
1094    num_visible_items: u64,
1095    rep: ScalarBuffer<u16>,
1096    def: ScalarBuffer<u16>,
1097}
1098
1099impl DecodePageTask for VariableFullZipDecodeTask {
1100    fn decode(self: Box<Self>) -> Result<DecodedPage> {
1101        let block = VariableWidthBlock {
1102            data: self.data,
1103            offsets: self.offsets,
1104            bits_per_offset: self.bits_per_offset,
1105            num_values: self.num_visible_items,
1106            block_info: BlockInfo::new(),
1107        };
1108        let decomopressed = self.decompressor.decompress(block)?;
1109        let rep = if self.rep.is_empty() {
1110            None
1111        } else {
1112            Some(self.rep.to_vec())
1113        };
1114        let def = if self.def.is_empty() {
1115            None
1116        } else {
1117            Some(self.def.to_vec())
1118        };
1119        let unraveler = RepDefUnraveler::new(
1120            rep,
1121            def,
1122            self.details.def_meaning.clone(),
1123            self.num_visible_items,
1124        );
1125        Ok(DecodedPage {
1126            data: decomopressed,
1127            repdef: unraveler,
1128        })
1129    }
1130}
1131
1132#[derive(Debug)]
1133struct FullZipDecodeTaskItem {
1134    data: PerValueDataBlock,
1135    rows_in_buf: u64,
1136}
1137
1138/// A task to unzip and decompress full-zip encoded data when that data
1139/// has a fixed-width.
1140#[derive(Debug)]
1141struct FixedFullZipDecodeTask {
1142    details: Arc<FullZipDecodeDetails>,
1143    data: Vec<FullZipDecodeTaskItem>,
1144    num_rows: usize,
1145    bytes_per_value: usize,
1146}
1147
1148impl DecodePageTask for FixedFullZipDecodeTask {
1149    fn decode(self: Box<Self>) -> Result<DecodedPage> {
1150        let estimated_size_bytes = if self.details.ctrl_word_parser.bytes_per_word() == 0 {
1151            let PerValueDecompressor::Fixed(decompressor) = &self.details.value_decompressor else {
1152                return Err(Error::internal(
1153                    "FixedFullZipDecodeTask requires a fixed-width decompressor",
1154                ));
1155            };
1156            decompressor
1157                .decoded_size_bytes(self.num_rows as u64)
1158                .unwrap_or_else(|| {
1159                    self.data
1160                        .iter()
1161                        .map(|task_item| task_item.data.data_size())
1162                        .sum::<u64>()
1163                        * 2
1164                })
1165        } else {
1166            // Rep/def levels can suppress values, so the exact output size is not known
1167            // until they are decoded. Keep the existing conservative estimate.
1168            self.data
1169                .iter()
1170                .map(|task_item| task_item.data.data_size())
1171                .sum::<u64>()
1172                * 2
1173        };
1174        let mut data_builder = DataBlockBuilder::with_capacity_estimate(estimated_size_bytes);
1175
1176        if self.details.ctrl_word_parser.bytes_per_word() == 0 {
1177            // Fast path, no need to unzip because there is no rep/def
1178            //
1179            // We decompress each buffer and add it to our output buffer
1180            for task_item in self.data.into_iter() {
1181                let PerValueDataBlock::Fixed(fixed_data) = task_item.data else {
1182                    unreachable!()
1183                };
1184                let PerValueDecompressor::Fixed(decompressor) = &self.details.value_decompressor
1185                else {
1186                    unreachable!()
1187                };
1188                debug_assert_eq!(fixed_data.num_values, task_item.rows_in_buf);
1189                let decompressed = decompressor.decompress(fixed_data, task_item.rows_in_buf)?;
1190                data_builder.append(&decompressed, 0..task_item.rows_in_buf)?;
1191            }
1192
1193            let unraveler = RepDefUnraveler::new(
1194                None,
1195                None,
1196                self.details.def_meaning.clone(),
1197                self.num_rows as u64,
1198            );
1199
1200            Ok(DecodedPage {
1201                data: data_builder.finish(),
1202                repdef: unraveler,
1203            })
1204        } else {
1205            // Slow path, unzipping needed
1206            let mut rep = Vec::with_capacity(self.num_rows);
1207            let mut def = Vec::with_capacity(self.num_rows);
1208            let mut num_visible_items: u64 = 0;
1209
1210            for task_item in self.data.into_iter() {
1211                let PerValueDataBlock::Fixed(fixed_data) = task_item.data else {
1212                    unreachable!()
1213                };
1214                let mut buf_slice = fixed_data.data.as_ref();
1215                let num_values = fixed_data.num_values as usize;
1216                // We will be unzipping repdef in to `rep` and `def` and the
1217                // values into `values` (which contains the compressed values)
1218                let mut values = Vec::with_capacity(
1219                    fixed_data.data.len()
1220                        - (self.details.ctrl_word_parser.bytes_per_word() * num_values),
1221                );
1222                let mut visible_items = 0;
1223                for _ in 0..num_values {
1224                    // Extract rep/def
1225                    self.details
1226                        .ctrl_word_parser
1227                        .parse(buf_slice, &mut rep, &mut def);
1228                    buf_slice = &buf_slice[self.details.ctrl_word_parser.bytes_per_word()..];
1229
1230                    let is_visible = def
1231                        .last()
1232                        .map(|d| *d <= self.details.max_visible_def)
1233                        .unwrap_or(true);
1234                    if is_visible {
1235                        // Extract value
1236                        values.extend_from_slice(buf_slice[..self.bytes_per_value].as_ref());
1237                        buf_slice = &buf_slice[self.bytes_per_value..];
1238                        visible_items += 1;
1239                    }
1240                }
1241
1242                // Finally, we decompress the values and add them to our output buffer
1243                let values_buf = LanceBuffer::from(values);
1244                let fixed_data = FixedWidthDataBlock {
1245                    bits_per_value: self.bytes_per_value as u64 * 8,
1246                    block_info: BlockInfo::new(),
1247                    data: values_buf,
1248                    num_values: visible_items,
1249                };
1250                let PerValueDecompressor::Fixed(decompressor) = &self.details.value_decompressor
1251                else {
1252                    unreachable!()
1253                };
1254                let decompressed = decompressor.decompress(fixed_data, visible_items)?;
1255                data_builder.append(&decompressed, 0..visible_items)?;
1256                num_visible_items += visible_items;
1257            }
1258
1259            let repetition = if rep.is_empty() { None } else { Some(rep) };
1260            let definition = if def.is_empty() { None } else { Some(def) };
1261
1262            // Items, not rows: a list row spans any number of items and a null or
1263            // empty list spans none.
1264            let unraveler = RepDefUnraveler::new(
1265                repetition,
1266                definition,
1267                self.details.def_meaning.clone(),
1268                num_visible_items,
1269            );
1270            let data = data_builder.finish();
1271
1272            Ok(DecodedPage {
1273                data,
1274                repdef: unraveler,
1275            })
1276        }
1277    }
1278}
1279
1280#[cfg(test)]
1281#[allow(clippy::single_range_in_vec_init)]
1282mod tests {
1283    use std::collections::HashMap;
1284    use std::sync::Arc;
1285
1286    use arrow_array::{Array, FixedSizeListArray, Float32Array, UInt8Array, make_array};
1287    use arrow_schema::{DataType, Field as ArrowField};
1288
1289    use crate::buffer::LanceBuffer;
1290    use crate::compression::FixedPerValueDecompressor;
1291    use crate::constants::{STRUCTURAL_ENCODING_META_KEY, STRUCTURAL_ENCODING_MINIBLOCK};
1292    use crate::data::{BlockInfo, DataBlock, FixedWidthDataBlock};
1293    use crate::decoder::DecodePageTask;
1294    use crate::encodings::logical::primitive::StructuralPageScheduler;
1295    use crate::encodings::logical::primitive::fullzip::{PerValueCompressor, PerValueDataBlock};
1296    use crate::encodings::physical::value::{ValueDecompressor, ValueEncoder};
1297    use crate::format::metadata::compressive_encoding::Compression;
1298    use crate::testing::{TestCases, check_round_trip_encoding_of_data};
1299
1300    use super::{
1301        FixedFullZipDecodeTask, FullZipCacheableState, FullZipDecodeDetails, FullZipDecodeTaskItem,
1302        FullZipReadSource, FullZipRepIndexDetails, FullZipScheduler, PerValueDecompressor,
1303        VariableFullZipDecoder,
1304    };
1305
1306    fn decode_fixed_fullzip_no_levels(
1307        decompressor: Arc<dyn FixedPerValueDecompressor>,
1308        data: Vec<FullZipDecodeTaskItem>,
1309        num_rows: usize,
1310        bytes_per_value: usize,
1311    ) -> DataBlock {
1312        Box::new(FixedFullZipDecodeTask {
1313            details: Arc::new(FullZipDecodeDetails {
1314                value_decompressor: PerValueDecompressor::Fixed(decompressor),
1315                def_meaning: Arc::from([]),
1316                ctrl_word_parser: crate::repdef::ControlWordParser::new(0, 0),
1317                max_rep: 0,
1318                max_visible_def: u16::MAX,
1319            }),
1320            data,
1321            num_rows,
1322            bytes_per_value,
1323        })
1324        .decode()
1325        .unwrap()
1326        .data
1327    }
1328
1329    #[test]
1330    fn test_fixed_fullzip_decode_preallocates_exact_output_size() {
1331        #[derive(Debug)]
1332        struct IdentityFixedDecompressor;
1333
1334        impl FixedPerValueDecompressor for IdentityFixedDecompressor {
1335            fn decompress(
1336                &self,
1337                data: FixedWidthDataBlock,
1338                num_rows: u64,
1339            ) -> crate::Result<DataBlock> {
1340                assert_eq!(data.num_values, num_rows);
1341                Ok(DataBlock::FixedWidth(data))
1342            }
1343
1344            fn bits_per_value(&self) -> u64 {
1345                32
1346            }
1347
1348            fn decoded_size_bytes(&self, num_values: u64) -> Option<u64> {
1349                num_values.checked_mul(4)
1350            }
1351        }
1352
1353        let make_item = |num_rows: u64| FullZipDecodeTaskItem {
1354            data: PerValueDataBlock::Fixed(FixedWidthDataBlock {
1355                data: LanceBuffer::from(vec![7_u8; num_rows as usize * 4]),
1356                bits_per_value: 32,
1357                num_values: num_rows,
1358                block_info: BlockInfo::new(),
1359            }),
1360            rows_in_buf: num_rows,
1361        };
1362
1363        let num_rows = 512;
1364        let decoded = decode_fixed_fullzip_no_levels(
1365            Arc::new(IdentityFixedDecompressor),
1366            vec![make_item(128), make_item(384)],
1367            num_rows,
1368            4,
1369        );
1370        let values = decoded.as_fixed_width_ref().unwrap();
1371        let expected_size = num_rows * 4;
1372        assert_eq!(values.data.len(), expected_size);
1373        assert_eq!(values.data.clone().into_buffer().capacity(), expected_size);
1374    }
1375
1376    #[test]
1377    fn test_fixed_fullzip_decode_falls_back_when_output_size_is_not_exact() {
1378        #[derive(Debug)]
1379        struct FallbackFixedDecompressor;
1380
1381        impl FixedPerValueDecompressor for FallbackFixedDecompressor {
1382            fn decompress(
1383                &self,
1384                data: FixedWidthDataBlock,
1385                num_rows: u64,
1386            ) -> crate::Result<DataBlock> {
1387                assert_eq!(data.num_values, num_rows);
1388                Ok(DataBlock::FixedWidth(FixedWidthDataBlock {
1389                    data: LanceBuffer::from(vec![7_u8; num_rows as usize * 4]),
1390                    bits_per_value: 32,
1391                    num_values: num_rows,
1392                    block_info: BlockInfo::new(),
1393                }))
1394            }
1395
1396            fn bits_per_value(&self) -> u64 {
1397                // This deliberately cannot be multiplied by num_rows. If FullZip treats
1398                // bits_per_value as an exact decoded-size estimate, decoding will fail.
1399                u64::MAX - 7
1400            }
1401        }
1402
1403        let num_rows = 2;
1404        let decoded = decode_fixed_fullzip_no_levels(
1405            Arc::new(FallbackFixedDecompressor),
1406            vec![FullZipDecodeTaskItem {
1407                data: PerValueDataBlock::Fixed(FixedWidthDataBlock {
1408                    data: LanceBuffer::from(vec![0_u8; num_rows * 4]),
1409                    bits_per_value: 32,
1410                    num_values: num_rows as u64,
1411                    block_info: BlockInfo::new(),
1412                }),
1413                rows_in_buf: num_rows as u64,
1414            }],
1415            num_rows,
1416            4,
1417        );
1418        let values = decoded.as_fixed_width_ref().unwrap();
1419        assert_eq!(values.num_values, num_rows as u64);
1420        assert_eq!(values.data.len(), num_rows * 4);
1421    }
1422
1423    #[test]
1424    fn test_fixed_fullzip_real_fsl_preallocates_exact_output_size() {
1425        let num_rows = 64;
1426        let dimension = 32;
1427        let items = Arc::new(Float32Array::from_iter_values(
1428            (0..num_rows * dimension).map(|value| value as f32),
1429        ));
1430        let item_field = Arc::new(ArrowField::new("item", DataType::Float32, false));
1431        let sample = FixedSizeListArray::new(item_field, dimension as i32, items, None);
1432
1433        let (data, compression) = PerValueCompressor::compress(
1434            &ValueEncoder::default(),
1435            DataBlock::from_array(sample.clone()),
1436        )
1437        .unwrap();
1438        let Compression::FixedSizeList(fsl) = compression.compression.unwrap() else {
1439            panic!("expected fixed-size-list compression");
1440        };
1441        let decompressor = ValueDecompressor::from_fsl(fsl.as_ref()).unwrap();
1442        let expected_size = num_rows * dimension * size_of::<f32>();
1443        assert_eq!(
1444            FixedPerValueDecompressor::decoded_size_bytes(&decompressor, num_rows as u64),
1445            Some(expected_size as u64)
1446        );
1447
1448        let decoded = decode_fixed_fullzip_no_levels(
1449            Arc::new(decompressor),
1450            vec![FullZipDecodeTaskItem {
1451                data,
1452                rows_in_buf: num_rows as u64,
1453            }],
1454            num_rows,
1455            dimension * size_of::<f32>(),
1456        );
1457        let fsl = decoded.as_fixed_size_list_ref().unwrap();
1458        let values = fsl.child.as_fixed_width_ref().unwrap();
1459        assert_eq!(values.data.len(), expected_size);
1460        assert_eq!(values.data.clone().into_buffer().capacity(), expected_size);
1461
1462        let decoded_array = make_array(
1463            decoded
1464                .into_arrow(sample.data_type().clone(), true)
1465                .unwrap(),
1466        );
1467        assert_eq!(decoded_array.as_ref(), &sample);
1468    }
1469
1470    #[test]
1471    fn test_fixed_fullzip_nullable_fsl_uses_fallback_end_to_end() {
1472        #[derive(Debug)]
1473        struct NullableFslDecompressor {
1474            inner: ValueDecompressor,
1475        }
1476
1477        impl FixedPerValueDecompressor for NullableFslDecompressor {
1478            fn decompress(
1479                &self,
1480                data: FixedWidthDataBlock,
1481                num_rows: u64,
1482            ) -> crate::Result<DataBlock> {
1483                FixedPerValueDecompressor::decompress(&self.inner, data, num_rows)
1484            }
1485
1486            fn bits_per_value(&self) -> u64 {
1487                // FullZip must not use this physical row width as an exact decoded-size
1488                // estimate for the nullable, multi-buffer Arrow output.
1489                u64::MAX - 7
1490            }
1491
1492            fn decoded_size_bytes(&self, num_values: u64) -> Option<u64> {
1493                FixedPerValueDecompressor::decoded_size_bytes(&self.inner, num_values)
1494            }
1495        }
1496
1497        let num_rows = 64;
1498        let items = Arc::new(UInt8Array::from_iter(
1499            (0..num_rows).map(|value| (value % 3 != 0).then_some(value as u8)),
1500        ));
1501        let item_field = Arc::new(ArrowField::new("item", DataType::UInt8, true));
1502        let sample = FixedSizeListArray::new(item_field, 1, items, None);
1503
1504        let (data, compression) = PerValueCompressor::compress(
1505            &ValueEncoder::default(),
1506            DataBlock::from_array(sample.clone()),
1507        )
1508        .unwrap();
1509        let Compression::FixedSizeList(fsl) = compression.compression.unwrap() else {
1510            panic!("expected fixed-size-list compression");
1511        };
1512        let decompressor = NullableFslDecompressor {
1513            inner: ValueDecompressor::from_fsl(fsl.as_ref()).unwrap(),
1514        };
1515        assert_eq!(
1516            FixedPerValueDecompressor::decoded_size_bytes(&decompressor, num_rows as u64),
1517            None
1518        );
1519
1520        let decoded = decode_fixed_fullzip_no_levels(
1521            Arc::new(decompressor),
1522            vec![FullZipDecodeTaskItem {
1523                data,
1524                rows_in_buf: num_rows as u64,
1525            }],
1526            num_rows,
1527            2,
1528        );
1529        let decoded_array = make_array(
1530            decoded
1531                .into_arrow(sample.data_type().clone(), true)
1532                .unwrap(),
1533        );
1534        assert_eq!(decoded_array.as_ref(), &sample);
1535    }
1536
1537    #[test]
1538    fn test_slice_batch_data_and_rebase_offsets_u32() {
1539        let data = LanceBuffer::copy_slice(b"0123456789abcdefghij");
1540        let offsets = LanceBuffer::reinterpret_vec(vec![6_u32, 8_u32, 8_u32, 12_u32]);
1541
1542        let (sliced_data, normalized_offsets) =
1543            VariableFullZipDecoder::slice_batch_data_and_rebase_offsets(&data, &offsets, 32)
1544                .unwrap();
1545
1546        assert_eq!(sliced_data.as_ref(), b"6789ab");
1547        let normalized = normalized_offsets.borrow_to_typed_slice::<u32>();
1548        assert_eq!(normalized.as_ref(), &[0, 2, 2, 6]);
1549    }
1550
1551    #[test]
1552    fn test_slice_batch_data_and_rebase_offsets_u64() {
1553        let data = LanceBuffer::copy_slice(b"abcdefghijklmnopqrstuvwxyz");
1554        let offsets = LanceBuffer::reinterpret_vec(vec![10_u64, 12_u64, 16_u64, 20_u64]);
1555
1556        let (sliced_data, normalized_offsets) =
1557            VariableFullZipDecoder::slice_batch_data_and_rebase_offsets(&data, &offsets, 64)
1558                .unwrap();
1559
1560        assert_eq!(sliced_data.as_ref(), b"klmnopqrst");
1561        let normalized = normalized_offsets.borrow_to_typed_slice::<u64>();
1562        assert_eq!(normalized.as_ref(), &[0, 2, 6, 10]);
1563    }
1564
1565    #[test]
1566    fn test_slice_batch_data_and_rebase_offsets_rejects_invalid_offsets() {
1567        let data = LanceBuffer::copy_slice(b"abcd");
1568        let offsets = LanceBuffer::reinterpret_vec(vec![3_u32, 2_u32]);
1569
1570        let err = VariableFullZipDecoder::slice_batch_data_and_rebase_offsets(&data, &offsets, 32)
1571            .expect_err("offset end before start should error");
1572        assert!(err.to_string().contains("less than base"));
1573    }
1574
1575    #[tokio::test]
1576    async fn test_fullzip_initialize_is_lazy() {
1577        use futures::{FutureExt, future::BoxFuture};
1578        use std::ops::Range;
1579        use std::sync::Mutex;
1580
1581        #[derive(Debug, Clone)]
1582        struct RecordingScheduler {
1583            data: bytes::Bytes,
1584            requests: Arc<Mutex<Vec<Vec<Range<u64>>>>>,
1585        }
1586
1587        impl RecordingScheduler {
1588            fn new(data: bytes::Bytes) -> Self {
1589                Self {
1590                    data,
1591                    requests: Arc::new(Mutex::new(Vec::new())),
1592                }
1593            }
1594
1595            fn requests(&self) -> Vec<Vec<Range<u64>>> {
1596                self.requests.lock().unwrap().clone()
1597            }
1598        }
1599
1600        impl crate::EncodingsIo for RecordingScheduler {
1601            fn submit_request(
1602                &self,
1603                ranges: Vec<Range<u64>>,
1604                _priority: u64,
1605            ) -> BoxFuture<'static, crate::Result<Vec<bytes::Bytes>>> {
1606                self.requests.lock().unwrap().push(ranges.clone());
1607                let data = ranges
1608                    .into_iter()
1609                    .map(|range| self.data.slice(range.start as usize..range.end as usize))
1610                    .collect::<Vec<_>>();
1611                std::future::ready(Ok(data)).boxed()
1612            }
1613        }
1614
1615        #[derive(Debug)]
1616        struct TestFixedDecompressor;
1617
1618        impl FixedPerValueDecompressor for TestFixedDecompressor {
1619            fn decompress(
1620                &self,
1621                _data: FixedWidthDataBlock,
1622                _num_rows: u64,
1623            ) -> crate::Result<DataBlock> {
1624                unimplemented!("Test decompressor")
1625            }
1626
1627            fn bits_per_value(&self) -> u64 {
1628                32
1629            }
1630        }
1631
1632        let io = Arc::new(RecordingScheduler::new(bytes::Bytes::from(vec![
1633            0;
1634            16 * 1024
1635        ])));
1636        let mut scheduler = FullZipScheduler {
1637            data_buf_position: 0,
1638            data_buf_size: 4096,
1639            rep_index: Some(FullZipRepIndexDetails {
1640                buf_position: 1000,
1641                bytes_per_value: 4,
1642            }),
1643            priority: 0,
1644            rows_in_page: 100,
1645            bits_per_offset: 32,
1646            details: Arc::new(FullZipDecodeDetails {
1647                value_decompressor: PerValueDecompressor::Fixed(Arc::new(TestFixedDecompressor)),
1648                def_meaning: Arc::new([crate::repdef::DefinitionInterpretation::NullableItem]),
1649                ctrl_word_parser: crate::repdef::ControlWordParser::new(0, 1),
1650                max_rep: 0,
1651                max_visible_def: 0,
1652            }),
1653            cached_state: None,
1654            enable_cache: false,
1655        };
1656
1657        let io_dyn: Arc<dyn crate::EncodingsIo> = io.clone();
1658        let initialization = scheduler.init_layout().unwrap();
1659        assert!(
1660            initialization.required_ranges().next().is_none(),
1661            "FullZip should not request any ranges when caching is disabled"
1662        );
1663        let buffers = initialization.with_buffers(vec![]).unwrap();
1664        let cached_data = scheduler.init_from_buffers(buffers, &io_dyn).await.unwrap();
1665
1666        assert!(
1667            cached_data
1668                .as_arc_any()
1669                .downcast_ref::<super::NoCachedPageData>()
1670                .is_some(),
1671            "FullZip initialize should not eagerly load repetition index data"
1672        );
1673        assert!(scheduler.cached_state.is_none());
1674        assert!(
1675            io.requests().is_empty(),
1676            "FullZip initialize should not issue any I/O"
1677        );
1678    }
1679
1680    #[tokio::test]
1681    async fn test_fullzip_read_source_slices_prefetched_page() {
1682        let page_start = 200_u64;
1683        let page_data = LanceBuffer::copy_slice(&[0, 1, 2, 3, 4, 5, 6, 7]);
1684        let source = FullZipReadSource::PrefetchedPage {
1685            base_offset: page_start,
1686            data: page_data,
1687        };
1688        let ranges = vec![
1689            page_start..(page_start + 3),
1690            (page_start + 4)..(page_start + 8),
1691        ];
1692        let mut data = source.fetch(&ranges, 0).await.unwrap();
1693        assert_eq!(data.pop_front().unwrap().as_ref(), &[0, 1, 2]);
1694        assert_eq!(data.pop_front().unwrap().as_ref(), &[4, 5, 6, 7]);
1695    }
1696
1697    #[tokio::test]
1698    async fn test_fullzip_initialize_caches_rep_index_when_enabled() {
1699        use futures::{FutureExt, future::BoxFuture};
1700        use std::ops::Range;
1701        use std::sync::Mutex;
1702
1703        #[derive(Debug, Clone)]
1704        struct RecordingScheduler {
1705            data: bytes::Bytes,
1706            requests: Arc<Mutex<Vec<Vec<Range<u64>>>>>,
1707        }
1708
1709        impl RecordingScheduler {
1710            fn new(data: bytes::Bytes) -> Self {
1711                Self {
1712                    data,
1713                    requests: Arc::new(Mutex::new(Vec::new())),
1714                }
1715            }
1716
1717            fn requests(&self) -> Vec<Vec<Range<u64>>> {
1718                self.requests.lock().unwrap().clone()
1719            }
1720        }
1721
1722        impl crate::EncodingsIo for RecordingScheduler {
1723            fn submit_request(
1724                &self,
1725                ranges: Vec<Range<u64>>,
1726                _priority: u64,
1727            ) -> BoxFuture<'static, crate::Result<Vec<bytes::Bytes>>> {
1728                self.requests.lock().unwrap().push(ranges.clone());
1729                let data = ranges
1730                    .into_iter()
1731                    .map(|range| self.data.slice(range.start as usize..range.end as usize))
1732                    .collect::<Vec<_>>();
1733                std::future::ready(Ok(data)).boxed()
1734            }
1735        }
1736
1737        #[derive(Debug)]
1738        struct TestFixedDecompressor;
1739
1740        impl FixedPerValueDecompressor for TestFixedDecompressor {
1741            fn decompress(
1742                &self,
1743                _data: FixedWidthDataBlock,
1744                _num_rows: u64,
1745            ) -> crate::Result<DataBlock> {
1746                unimplemented!("Test decompressor")
1747            }
1748
1749            fn bits_per_value(&self) -> u64 {
1750                32
1751            }
1752        }
1753
1754        let rows_in_page = 100_u64;
1755        let bytes_per_value = 4_u64;
1756        let rep_start = 1000_u64;
1757        let rep_size = ((rows_in_page + 1) * bytes_per_value) as usize;
1758        let mut data = vec![0_u8; 16 * 1024];
1759        data[rep_start as usize..rep_start as usize + rep_size].fill(7);
1760        let io = Arc::new(RecordingScheduler::new(bytes::Bytes::from(data)));
1761
1762        let mut scheduler = FullZipScheduler {
1763            data_buf_position: 0,
1764            data_buf_size: 4096,
1765            rep_index: Some(FullZipRepIndexDetails {
1766                buf_position: rep_start,
1767                bytes_per_value,
1768            }),
1769            priority: 0,
1770            rows_in_page,
1771            bits_per_offset: 32,
1772            details: Arc::new(FullZipDecodeDetails {
1773                value_decompressor: PerValueDecompressor::Fixed(Arc::new(TestFixedDecompressor)),
1774                def_meaning: Arc::new([crate::repdef::DefinitionInterpretation::NullableItem]),
1775                ctrl_word_parser: crate::repdef::ControlWordParser::new(0, 1),
1776                max_rep: 0,
1777                max_visible_def: 0,
1778            }),
1779            cached_state: None,
1780            enable_cache: true,
1781        };
1782
1783        let io_dyn: Arc<dyn crate::EncodingsIo> = io.clone();
1784        let initialization = scheduler.init_layout().unwrap();
1785        let ranges = initialization
1786            .required_ranges()
1787            .cloned()
1788            .collect::<Vec<_>>();
1789        assert_eq!(
1790            ranges,
1791            vec![rep_start..(rep_start + (rows_in_page + 1) * bytes_per_value)]
1792        );
1793        // The field scheduler fetches the declared ranges (coalesced across all
1794        // pages) and hands the bytes back to the page to finish initialization.
1795        let buffers = io_dyn.submit_request(ranges, 0).await.unwrap();
1796        let buffers = initialization.with_buffers(buffers).unwrap();
1797        let cached_data = scheduler.init_from_buffers(buffers, &io_dyn).await.unwrap();
1798        assert!(
1799            cached_data
1800                .clone()
1801                .as_arc_any()
1802                .downcast_ref::<FullZipCacheableState>()
1803                .is_some()
1804        );
1805        assert!(scheduler.cached_state.is_none());
1806        scheduler.try_load(&cached_data).unwrap();
1807        assert!(scheduler.cached_state.is_some());
1808        assert_eq!(
1809            io.requests(),
1810            vec![vec![
1811                rep_start..(rep_start + (rows_in_page + 1) * bytes_per_value)
1812            ]]
1813        );
1814    }
1815
1816    #[tokio::test]
1817    async fn test_fullzip_full_page_bypasses_rep_index_io() {
1818        use futures::{FutureExt, future::BoxFuture};
1819        use std::ops::Range;
1820        use std::sync::Mutex;
1821
1822        #[derive(Debug, Clone)]
1823        struct RecordingScheduler {
1824            data: bytes::Bytes,
1825            requests: Arc<Mutex<Vec<Vec<Range<u64>>>>>,
1826        }
1827
1828        impl RecordingScheduler {
1829            fn new(data: bytes::Bytes) -> Self {
1830                Self {
1831                    data,
1832                    requests: Arc::new(Mutex::new(Vec::new())),
1833                }
1834            }
1835
1836            fn requests(&self) -> Vec<Vec<Range<u64>>> {
1837                self.requests.lock().unwrap().clone()
1838            }
1839        }
1840
1841        impl crate::EncodingsIo for RecordingScheduler {
1842            fn submit_request(
1843                &self,
1844                ranges: Vec<Range<u64>>,
1845                _priority: u64,
1846            ) -> BoxFuture<'static, crate::Result<Vec<bytes::Bytes>>> {
1847                self.requests.lock().unwrap().push(ranges.clone());
1848                let data = ranges
1849                    .into_iter()
1850                    .map(|range| self.data.slice(range.start as usize..range.end as usize))
1851                    .collect::<Vec<_>>();
1852                std::future::ready(Ok(data)).boxed()
1853            }
1854        }
1855
1856        #[derive(Debug)]
1857        struct TestFixedDecompressor;
1858
1859        impl FixedPerValueDecompressor for TestFixedDecompressor {
1860            fn decompress(
1861                &self,
1862                _data: FixedWidthDataBlock,
1863                _num_rows: u64,
1864            ) -> crate::Result<DataBlock> {
1865                unimplemented!("Test decompressor")
1866            }
1867
1868            fn bits_per_value(&self) -> u64 {
1869                32
1870            }
1871        }
1872
1873        let rows_in_page = 100_u64;
1874        let data_start = 256_u64;
1875        let data_size = 500_u64;
1876        let rep_start = 4096_u64;
1877        let bytes_per_value = 4_u64;
1878
1879        let mut bytes = vec![0_u8; 16 * 1024];
1880        for i in 0..=rows_in_page {
1881            let offset = (i * 5) as u32;
1882            let pos = rep_start as usize + (i * bytes_per_value) as usize;
1883            bytes[pos..pos + 4].copy_from_slice(&offset.to_le_bytes());
1884        }
1885        let io = Arc::new(RecordingScheduler::new(bytes::Bytes::from(bytes)));
1886
1887        let scheduler = FullZipScheduler {
1888            data_buf_position: data_start,
1889            data_buf_size: data_size,
1890            rep_index: Some(FullZipRepIndexDetails {
1891                buf_position: rep_start,
1892                bytes_per_value,
1893            }),
1894            priority: 0,
1895            rows_in_page,
1896            bits_per_offset: 32,
1897            details: Arc::new(FullZipDecodeDetails {
1898                value_decompressor: PerValueDecompressor::Fixed(Arc::new(TestFixedDecompressor)),
1899                def_meaning: Arc::new([crate::repdef::DefinitionInterpretation::NullableItem]),
1900                ctrl_word_parser: crate::repdef::ControlWordParser::new(0, 1),
1901                max_rep: 0,
1902                max_visible_def: 0,
1903            }),
1904            cached_state: None,
1905            enable_cache: false,
1906        };
1907
1908        let io_dyn: Arc<dyn crate::EncodingsIo> = io.clone();
1909        let tasks = scheduler
1910            .schedule_ranges_rep(
1911                &[0..rows_in_page],
1912                &io_dyn,
1913                FullZipRepIndexDetails {
1914                    buf_position: rep_start,
1915                    bytes_per_value,
1916                },
1917            )
1918            .unwrap();
1919
1920        let requests = io.requests();
1921        assert_eq!(requests.len(), 1);
1922        assert_eq!(requests[0], vec![data_start..(data_start + data_size)]);
1923
1924        let _ = tasks.into_iter().next().unwrap().decoder_fut.await.unwrap();
1925        let requests_after_await = io.requests();
1926        assert_eq!(
1927            requests_after_await.len(),
1928            1,
1929            "full page path should not issue rep-index I/O"
1930        );
1931    }
1932
1933    /// This test is used to reproduce fuzz test https://github.com/lancedb/lance/issues/4492
1934    #[tokio::test]
1935    async fn test_fuzz_issue_4492_empty_rep_values() {
1936        use lance_datagen::{RowCount, Seed, array, gen_batch};
1937
1938        let seed = 1823859942947654717u64;
1939        let num_rows = 2741usize;
1940
1941        // Generate the exact same data that caused the failure
1942        let batch_gen = gen_batch().with_seed(Seed::from(seed));
1943        let base_generator = array::rand_type(&DataType::FixedSizeBinary(32));
1944        let list_generator = array::rand_list_any(base_generator, false);
1945
1946        let batch = batch_gen
1947            .anon_col(list_generator)
1948            .into_batch_rows(RowCount::from(num_rows as u64))
1949            .unwrap();
1950
1951        let list_array = batch.column(0).clone();
1952
1953        // Force miniblock encoding
1954        let mut metadata = HashMap::new();
1955        metadata.insert(
1956            STRUCTURAL_ENCODING_META_KEY.to_string(),
1957            STRUCTURAL_ENCODING_MINIBLOCK.to_string(),
1958        );
1959
1960        let test_cases = TestCases::default()
1961            .with_structural_encodings()
1962            .with_batch_size(100)
1963            .with_range(0..num_rows.min(500) as u64)
1964            .with_indices(vec![0, num_rows as u64 / 2, (num_rows - 1) as u64]);
1965
1966        check_round_trip_encoding_of_data(vec![list_array], &test_cases, metadata).await
1967    }
1968
1969    /// One list of vectors per entry: `None` is a null list and `Some(items)` holds one
1970    /// vector per item, where `None` is a null vector.  The vectors are wide enough that
1971    /// the leaf is full-zip encoded.
1972    fn list_of_vectors(lists: &[Option<Vec<Option<f32>>>]) -> arrow_array::ArrayRef {
1973        use arrow_buffer::{NullBuffer, OffsetBuffer, ScalarBuffer};
1974
1975        const DIMENSION: i32 = 128;
1976        let mut values = Vec::new();
1977        let mut vector_validity = Vec::new();
1978        let mut offsets = vec![0_i32];
1979        let mut list_validity = Vec::new();
1980        for list in lists {
1981            if let Some(items) = list {
1982                for item in items {
1983                    values.extend(std::iter::repeat_n(item.unwrap_or(0.0), DIMENSION as usize));
1984                    vector_validity.push(item.is_some());
1985                }
1986            }
1987            list_validity.push(list.is_some());
1988            offsets.push(vector_validity.len() as i32);
1989        }
1990        let vectors = FixedSizeListArray::new(
1991            Arc::new(ArrowField::new("item", DataType::Float32, true)),
1992            DIMENSION,
1993            Arc::new(Float32Array::from(values)),
1994            vector_validity
1995                .contains(&false)
1996                .then(|| NullBuffer::from(vector_validity)),
1997        );
1998        Arc::new(arrow_array::ListArray::new(
1999            Arc::new(ArrowField::new("item", vectors.data_type().clone(), true)),
2000            OffsetBuffer::new(ScalarBuffer::from(offsets)),
2001            Arc::new(vectors),
2002            Some(NullBuffer::from(list_validity)),
2003        ))
2004    }
2005
2006    /// A read that spans a page whose vectors are all valid and a page holding a null
2007    /// vector must restore one validity bit per vector, not one per list.
2008    #[tokio::test]
2009    async fn test_fullzip_list_read_across_pages_with_mixed_item_validity() {
2010        let all_valid_page = list_of_vectors(&[
2011            Some(vec![Some(1.0), Some(2.0), Some(3.0)]),
2012            None,
2013            Some(vec![Some(4.0), Some(5.0)]),
2014        ]);
2015        let null_vector_page = list_of_vectors(&[
2016            Some(vec![Some(6.0), None, Some(7.0)]),
2017            Some(vec![Some(8.0)]),
2018        ]);
2019        check_round_trip_encoding_of_data(
2020            vec![all_valid_page, null_vector_page],
2021            &TestCases::default()
2022                .with_structural_encodings()
2023                // One page per input array
2024                .with_page_sizes(vec![1])
2025                .with_indices(vec![2, 3, 4]),
2026            HashMap::new(),
2027        )
2028        .await;
2029    }
2030
2031    fn truncated_tail_details() -> std::sync::Arc<super::FullZipDecodeDetails> {
2032        use crate::compression::VariablePerValueDecompressor;
2033        use crate::encodings::physical::binary::VariableDecoder;
2034        use crate::repdef::{ControlWordParser, DefinitionInterpretation};
2035        use std::sync::Arc;
2036        Arc::new(super::FullZipDecodeDetails {
2037            value_decompressor: super::PerValueDecompressor::Variable(Arc::new(
2038                VariableDecoder::default(),
2039            )
2040                as Arc<dyn VariablePerValueDecompressor>),
2041            def_meaning: vec![DefinitionInterpretation::NullableItem].into(),
2042            ctrl_word_parser: ControlWordParser::new(0, 0),
2043            max_rep: 0,
2044            max_visible_def: 0,
2045        })
2046    }
2047
2048    fn decode_variable_full_zip(
2049        buf: Vec<u8>,
2050        bits_per_offset: u8,
2051    ) -> lance_core::Result<super::VariableFullZipDecoder> {
2052        use std::collections::VecDeque;
2053        let mut data = VecDeque::new();
2054        data.push_back(crate::buffer::LanceBuffer::from(buf));
2055        super::VariableFullZipDecoder::new(
2056            truncated_tail_details(),
2057            data,
2058            1,
2059            bits_per_offset,
2060            bits_per_offset,
2061        )
2062    }
2063
2064    /// A well-formed length prefix decodes without incident, for both widths.
2065    #[test]
2066    fn variable_full_zip_wellformed_length_prefix() {
2067        assert!(decode_variable_full_zip(0u32.to_le_bytes().to_vec(), 32).is_ok());
2068        assert!(decode_variable_full_zip(0u64.to_le_bytes().to_vec(), 64).is_ok());
2069    }
2070
2071    /// A page whose item walk ends with a partial length prefix must surface a
2072    /// corrupt-file error rather than read past the end of the buffer.
2073    ///
2074    /// This asserts the error variant and message rather than merely expecting a
2075    /// panic: before the length prefix was bounds checked, the read was
2076    /// `get_unchecked` behind a `debug_assert!`, so a debug build panicked here
2077    /// (which a `#[should_panic]` test would have accepted as a pass) while a
2078    /// release build read up to 8 bytes out of a 4 byte allocation.
2079    #[test]
2080    fn variable_full_zip_truncated_length_prefix_is_corrupt_file() {
2081        use lance_core::Error;
2082
2083        for (bits, buf_len) in [(32u8, 3usize), (64u8, 4usize)] {
2084            let err = decode_variable_full_zip(vec![0xAA; buf_len], bits)
2085                .expect_err("a truncated length prefix must not decode");
2086            assert!(
2087                matches!(err, Error::CorruptFile { .. }),
2088                "expected CorruptFile for a {}-bit prefix with {} byte(s), got: {:?}",
2089                bits,
2090                buf_len,
2091                err
2092            );
2093            let msg = err.to_string();
2094            assert!(
2095                msg.contains("truncated length prefix"),
2096                "error should say what is wrong, got: {msg}"
2097            );
2098        }
2099    }
2100
2101    /// Drains `num_rows` from a single-buffer fixed full-zip page built from `buf`.
2102    ///
2103    /// `bits_rep` picks the control word width (1 -> one byte, 9 -> two bytes) and each
2104    /// visible item is a one-word control plus a 4 byte value.
2105    fn drain_fixed_full_zip(buf: Vec<u8>, bits_rep: u8, num_rows: u64) -> lance_core::Result<()> {
2106        use crate::compression::FixedPerValueDecompressor;
2107        use crate::decoder::StructuralPageDecoder;
2108        use crate::repdef::{ControlWordParser, DefinitionInterpretation};
2109        use std::collections::VecDeque;
2110        use std::sync::Arc;
2111
2112        #[derive(Debug)]
2113        struct UnusedFixedDecompressor;
2114
2115        impl FixedPerValueDecompressor for UnusedFixedDecompressor {
2116            fn decompress(&self, _: FixedWidthDataBlock, _: u64) -> crate::Result<DataBlock> {
2117                unreachable!("these cases fail while slicing, before any value is decoded")
2118            }
2119
2120            fn bits_per_value(&self) -> u64 {
2121                32
2122            }
2123        }
2124
2125        let details = Arc::new(super::FullZipDecodeDetails {
2126            value_decompressor: super::PerValueDecompressor::Fixed(Arc::new(
2127                UnusedFixedDecompressor,
2128            )),
2129            def_meaning: vec![DefinitionInterpretation::AllValidList].into(),
2130            ctrl_word_parser: ControlWordParser::new(bits_rep, 0),
2131            max_rep: 1,
2132            max_visible_def: 0,
2133        });
2134        let bytes_per_word = bits_rep.div_ceil(8) as usize;
2135
2136        let mut data = VecDeque::new();
2137        data.push_back(crate::buffer::LanceBuffer::from(buf));
2138        let mut decoder = super::FixedFullZipDecoder {
2139            details,
2140            data,
2141            offset_in_current: 0,
2142            bytes_per_value: 4,
2143            total_bytes_per_value: 4 + bytes_per_word,
2144            num_rows,
2145        };
2146        decoder.drain(num_rows).map(|_| ())
2147    }
2148
2149    /// Two one-item rows, each a control word plus a 4 byte value, slice cleanly.
2150    #[test]
2151    fn fixed_full_zip_wellformed_page_slices() {
2152        assert!(drain_fixed_full_zip(vec![1, 0, 0, 0, 0, 1, 0, 0, 0, 0], 1, 2).is_ok());
2153    }
2154
2155    /// The item walk is driven by the page's own control words, so a page that disagrees
2156    /// with its layout can run off the end of the buffer.  Each way of doing so must
2157    /// surface a corrupt-file error.
2158    ///
2159    /// These assert the error variant and message rather than merely expecting a panic:
2160    /// before the walk was bounds checked these aborted the process inside
2161    /// `LanceBuffer::slice_with_length`, which a `#[should_panic]` test would have
2162    /// accepted as a pass.
2163    #[rstest::rstest]
2164    // Second value is one byte short of a full 4 byte value
2165    #[case::truncated_item(vec![1, 0, 0, 0, 0, 1, 0, 0, 0], 1, 2, "truncated item")]
2166    // Two byte control words, with a lone trailing byte after the first item
2167    #[case::truncated_control_word(
2168        vec![1, 0, 0, 0, 0, 0, 1],
2169        9,
2170        2,
2171        "truncated control word"
2172    )]
2173    // A well-formed two row page cannot satisfy a request for three rows
2174    #[case::page_exhausted(vec![1, 0, 0, 0, 0, 1, 0, 0, 0, 0], 1, 3, "page data ran out")]
2175    fn fixed_full_zip_malformed_page_is_corrupt_file(
2176        #[case] buf: Vec<u8>,
2177        #[case] bits_rep: u8,
2178        #[case] num_rows: u64,
2179        #[case] expected_msg: &str,
2180    ) {
2181        use lance_core::Error;
2182
2183        let err = drain_fixed_full_zip(buf, bits_rep, num_rows)
2184            .expect_err("a malformed page must not slice");
2185        assert!(
2186            matches!(err, Error::CorruptFile { .. }),
2187            "expected CorruptFile, got: {err:?}"
2188        );
2189        let msg = err.to_string();
2190        assert!(
2191            msg.contains(expected_msg),
2192            "error should say what is wrong, got: {msg}"
2193        );
2194    }
2195
2196    /// A page whose item length prefix decodes to more bytes than remain in the
2197    /// buffer must surface a corrupt-file error rather than panic slicing the
2198    /// payload. Such a length means the item walk is reading bytes that are not
2199    /// a well-formed full-zip stream (a corrupt read or a misaligned walk), and
2200    /// the panic aborts the process when it crosses an FFI boundary.
2201    #[test]
2202    fn variable_full_zip_oversized_item_length_is_corrupt_file() {
2203        use lance_core::Error;
2204
2205        for bits in [32u8, 64u8] {
2206            let prefix_width = bits as usize / 8;
2207            // Length prefix decodes to u32/u64::MAX with a single payload byte behind it.
2208            let mut buf = vec![0xFF; prefix_width];
2209            buf.push(0xAA);
2210            let err = decode_variable_full_zip(buf, bits)
2211                .expect_err("an oversized item length must not decode");
2212            assert!(
2213                matches!(err, Error::CorruptFile { .. }),
2214                "expected CorruptFile for an oversized {}-bit length, got: {:?}",
2215                bits,
2216                err
2217            );
2218            let msg = err.to_string();
2219            assert!(
2220                msg.contains("exceeds") && msg.contains("remaining"),
2221                "error should say what is wrong, got: {msg}"
2222            );
2223        }
2224    }
2225}