Skip to main content

lance_encoding/encodings/logical/
primitive.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::{
5    any::Any,
6    collections::{HashMap, VecDeque},
7    env,
8    fmt::Debug,
9    iter,
10    ops::Range,
11    sync::Arc,
12    vec,
13};
14
15use crate::{
16    constants::{
17        STRUCTURAL_ENCODING_FULLZIP, STRUCTURAL_ENCODING_META_KEY, STRUCTURAL_ENCODING_MINIBLOCK,
18    },
19    data::DictionaryDataBlock,
20    encodings::logical::primitive::blob::{BlobDescriptionPageScheduler, BlobPageScheduler},
21    format::{
22        ProtobufUtils21,
23        pb21::{self, CompressiveEncoding, PageLayout, compressive_encoding::Compression},
24    },
25};
26use arrow_array::{Array, ArrayRef, PrimitiveArray, cast::AsArray, make_array, types::UInt64Type};
27use arrow_buffer::{BooleanBuffer, BooleanBufferBuilder, NullBuffer, ScalarBuffer};
28use arrow_schema::{DataType, Field as ArrowField};
29use bytes::Bytes;
30use futures::{FutureExt, TryStreamExt, future::BoxFuture, stream::FuturesOrdered};
31use itertools::Itertools;
32use lance_arrow::DataTypeExt;
33use lance_arrow::deepcopy::deep_copy_nulls;
34use lance_core::{
35    cache::{CacheKey, Context, DeepSizeOf},
36    error::{Error, LanceOptionExt},
37    utils::bit::pad_bytes,
38};
39use log::trace;
40
41use crate::{
42    compression::{
43        BlockDecompressor, CompressionStrategy, DecompressionStrategy, MiniBlockDecompressor,
44    },
45    data::{AllNullDataBlock, DataBlock, VariableWidthBlock},
46    utils::bytepack::BytepackedIntegerEncoder,
47};
48use crate::{
49    compression::{FixedPerValueDecompressor, VariablePerValueDecompressor},
50    encodings::logical::primitive::fullzip::PerValueDataBlock,
51};
52use crate::{
53    encodings::logical::primitive::miniblock::MiniBlockChunk, utils::bytepack::ByteUnpacker,
54};
55use crate::{
56    encodings::logical::primitive::miniblock::MiniBlockCompressed,
57    statistics::{ComputeStat, GetStat, Stat},
58};
59use crate::{
60    repdef::{
61        CompositeRepDefUnraveler, ControlWordIterator, ControlWordParser, DefinitionInterpretation,
62        RepDefSlicer, build_control_word_iterator,
63    },
64    utils::accumulation::AccumulationQueue,
65};
66use lance_core::{Result, datatypes::Field, utils::tokio::spawn_cpu};
67
68use crate::constants::{
69    COMPRESSION_LEVEL_META_KEY, COMPRESSION_META_KEY, DICT_DIVISOR_META_KEY,
70    DICT_SIZE_RATIO_META_KEY, DICT_VALUES_COMPRESSION_ENV_VAR,
71    DICT_VALUES_COMPRESSION_LEVEL_ENV_VAR, DICT_VALUES_COMPRESSION_LEVEL_META_KEY,
72    DICT_VALUES_COMPRESSION_META_KEY,
73};
74use crate::version::LanceFileVersion;
75use crate::{
76    EncodingsIo,
77    buffer::LanceBuffer,
78    data::{BlockInfo, DataBlockBuilder, FixedWidthDataBlock},
79    decoder::{
80        ColumnInfo, DecodePageTask, DecodedArray, DecodedPage, FilterExpression, LoadedPageShard,
81        MessageType, PageEncoding, PageInfo, ScheduledScanLine, SchedulerContext,
82        StructuralDecodeArrayTask, StructuralFieldDecoder, StructuralFieldScheduler,
83        StructuralPageDecoder, StructuralSchedulingJob, UnloadedPageShard,
84    },
85    encoder::{
86        EncodeTask, EncodedColumn, EncodedPage, EncodingOptions, FieldEncoder, OutOfLineBuffers,
87    },
88    repdef::{LevelBuffer, RepDefBuilder, RepDefUnraveler},
89};
90
91pub mod blob;
92pub mod constant;
93pub mod dict;
94pub mod fullzip;
95pub mod miniblock;
96
97const FILL_BYTE: u8 = 0xFE;
98const DEFAULT_DICT_DIVISOR: u64 = 2;
99const DEFAULT_DICT_MAX_CARDINALITY: u64 = 100_000;
100const DEFAULT_DICT_SIZE_RATIO: f64 = 0.8;
101const DEFAULT_DICT_VALUES_COMPRESSION: &str = "lz4";
102
103struct PageLoadTask {
104    decoder_fut: BoxFuture<'static, Result<Box<dyn StructuralPageDecoder>>>,
105    num_rows: u64,
106}
107
108/// A trait for figuring out how to schedule the data within
109/// a single page.
110trait StructuralPageScheduler: std::fmt::Debug + Send {
111    /// Fetches any metadata required for the page
112    fn initialize<'a>(
113        &'a mut self,
114        io: &Arc<dyn EncodingsIo>,
115    ) -> BoxFuture<'a, Result<Arc<dyn CachedPageData>>>;
116    /// Loads metadata from a previous initialize call
117    fn load(&mut self, data: &Arc<dyn CachedPageData>);
118    /// Schedules the read of the given ranges in the page
119    ///
120    /// The read may be split into multiple "shards" if the page is extremely large.
121    /// Each shard maps to one or more rows and can be decoded independently.
122    ///
123    /// Note: this sharding is for splitting up very large pages into smaller reads to
124    /// avoid buffering too much data in memory.  It is not related to the batch size or
125    /// compute units in any way.
126    fn schedule_ranges(
127        &self,
128        ranges: &[Range<u64>],
129        io: &Arc<dyn EncodingsIo>,
130    ) -> Result<Vec<PageLoadTask>>;
131}
132
133/// Metadata describing the decoded size of a mini-block
134#[derive(Debug)]
135struct ChunkMeta {
136    num_values: u64,
137    chunk_size_bytes: u64,
138    offset_bytes: u64,
139}
140
141/// A mini-block chunk that has been decoded and decompressed
142#[derive(Debug, Clone)]
143struct DecodedMiniBlockChunk {
144    rep: Option<ScalarBuffer<u16>>,
145    def: Option<ScalarBuffer<u16>>,
146    values: DataBlock,
147}
148
149/// A task to decode a one or more mini-blocks of data into an output batch
150///
151/// Note: Two batches might share the same mini-block of data.  When this happens
152/// then each batch gets a copy of the block and each batch decodes the block independently.
153///
154/// This means we have duplicated work but it is necessary to avoid having to synchronize
155/// the decoding of the block. (TODO: test this theory)
156#[derive(Debug)]
157struct DecodeMiniBlockTask {
158    rep_decompressor: Option<Arc<dyn BlockDecompressor>>,
159    def_decompressor: Option<Arc<dyn BlockDecompressor>>,
160    value_decompressor: Arc<dyn MiniBlockDecompressor>,
161    dictionary_data: Option<Arc<DataBlock>>,
162    def_meaning: Arc<[DefinitionInterpretation]>,
163    num_buffers: u64,
164    max_visible_level: u16,
165    instructions: Vec<(ChunkDrainInstructions, LoadedChunk)>,
166    has_large_chunk: bool,
167}
168
169impl DecodeMiniBlockTask {
170    fn decode_levels(
171        rep_decompressor: &dyn BlockDecompressor,
172        levels: LanceBuffer,
173        num_levels: u16,
174    ) -> Result<ScalarBuffer<u16>> {
175        let rep = rep_decompressor.decompress(levels, num_levels as u64)?;
176        let rep = rep.as_fixed_width().unwrap();
177        debug_assert_eq!(rep.num_values, num_levels as u64);
178        debug_assert_eq!(rep.bits_per_value, 16);
179        Ok(rep.data.borrow_to_typed_slice::<u16>())
180    }
181
182    // We are building a LevelBuffer (levels) and want to copy into it `total_len`
183    // values from `level_buf` starting at `offset`.
184    //
185    // We need to handle both the case where `levels` is None (no nulls encountered
186    // yet) and the case where `level_buf` is None (the input we are copying from has
187    // no nulls)
188    fn extend_levels(
189        range: Range<u64>,
190        levels: &mut Option<LevelBuffer>,
191        level_buf: &Option<impl AsRef<[u16]>>,
192        dest_offset: usize,
193    ) {
194        if let Some(level_buf) = level_buf {
195            if levels.is_none() {
196                // This is the first non-empty def buf we've hit, fill in the past
197                // with 0 (valid)
198                let mut new_levels_vec =
199                    LevelBuffer::with_capacity(dest_offset + (range.end - range.start) as usize);
200                new_levels_vec.extend(iter::repeat_n(0, dest_offset));
201                *levels = Some(new_levels_vec);
202            }
203            levels.as_mut().unwrap().extend(
204                level_buf.as_ref()[range.start as usize..range.end as usize]
205                    .iter()
206                    .copied(),
207            );
208        } else if let Some(levels) = levels {
209            let num_values = (range.end - range.start) as usize;
210            // This is an all-valid level_buf but we had nulls earlier and so we
211            // need to materialize it
212            levels.extend(iter::repeat_n(0, num_values));
213        }
214    }
215
216    /// Maps a range of rows to a range of items and a range of levels
217    ///
218    /// If there is no repetition information this just returns the range as-is.
219    ///
220    /// If there is repetition information then we need to do some work to figure out what
221    /// range of items corresponds to the requested range of rows.
222    ///
223    /// For example, if the data is [[1, 2, 3], [4, 5], [6, 7]] and the range is 1..2 (i.e. just row
224    /// 1) then the user actually wants items 3..5.  In the above case the rep levels would be:
225    ///
226    /// Idx: 0 1 2 3 4 5 6
227    /// Rep: 1 0 0 1 0 1 0
228    ///
229    /// So the start (1) maps to the second 1 (idx=3) and the end (2) maps to the third 1 (idx=5)
230    ///
231    /// If there are invisible items then we don't count them when calculating the range of items we
232    /// are interested in but we do count them when calculating the range of levels we are interested
233    /// in.  As a result we have to return both the item range (first return value) and the level range
234    /// (second return value).
235    ///
236    /// For example, if the data is [[1, 2, 3], [4, 5], NULL, [6, 7, 8]] and the range is 2..4 then the
237    /// user wants items 5..8 but they want levels 5..9.  In the above case the rep/def levels would be:
238    ///
239    /// Idx: 0 1 2 3 4 5 6 7 8
240    /// Rep: 1 0 0 1 0 1 1 0 0
241    /// Def: 0 0 0 0 0 1 0 0 0
242    /// Itm: 1 2 3 4 5 6 7 8
243    ///
244    /// Finally, we have to contend with the fact that chunks may or may not start with a "preamble" of
245    /// trailing values that finish up a list from the previous chunk.  In this case the first item does
246    /// not start at max_rep because it is a continuation of the previous chunk.  For our purposes we do
247    /// not consider this a "row" and so the range 0..1 will refer to the first row AFTER the preamble.
248    ///
249    /// We have a separate parameter (`preamble_action`) to control whether we want the preamble or not.
250    ///
251    /// Note that the "trailer" is considered a "row" and if we want it we should include it in the range.
252    fn map_range(
253        range: Range<u64>,
254        rep: Option<&impl AsRef<[u16]>>,
255        def: Option<&impl AsRef<[u16]>>,
256        max_rep: u16,
257        max_visible_def: u16,
258        // The total number of items (not rows) in the chunk.  This is not quite the same as
259        // rep.len() / def.len() because it doesn't count invisible items
260        total_items: u64,
261        preamble_action: PreambleAction,
262    ) -> (Range<u64>, Range<u64>) {
263        if let Some(rep) = rep {
264            let mut rep = rep.as_ref();
265            // If there is a preamble and we need to skip it then do that first.  The work is the same
266            // whether there is def information or not
267            let mut items_in_preamble = 0_u64;
268            let first_row_start = match preamble_action {
269                PreambleAction::Skip | PreambleAction::Take => {
270                    let first_row_start = if let Some(def) = def.as_ref() {
271                        let mut first_row_start = None;
272                        for (idx, (rep, def)) in rep.iter().zip(def.as_ref()).enumerate() {
273                            if *rep == max_rep {
274                                first_row_start = Some(idx as u64);
275                                break;
276                            }
277                            if *def <= max_visible_def {
278                                items_in_preamble += 1;
279                            }
280                        }
281                        first_row_start
282                    } else {
283                        let first_row_start =
284                            rep.iter().position(|&r| r == max_rep).map(|r| r as u64);
285                        items_in_preamble = first_row_start.unwrap_or(rep.len() as u64);
286                        first_row_start
287                    };
288                    // It is possible for a chunk to be entirely partial values but if it is then it
289                    // should never show up as a preamble to skip
290                    if first_row_start.is_none() {
291                        assert!(preamble_action == PreambleAction::Take);
292                        return (0..total_items, 0..rep.len() as u64);
293                    }
294                    let first_row_start = first_row_start.unwrap();
295                    rep = &rep[first_row_start as usize..];
296                    first_row_start
297                }
298                PreambleAction::Absent => {
299                    debug_assert!(rep[0] == max_rep);
300                    0
301                }
302            };
303
304            // We hit this case when all we needed was the preamble
305            if range.start == range.end {
306                debug_assert!(preamble_action == PreambleAction::Take);
307                debug_assert!(items_in_preamble <= total_items);
308                return (0..items_in_preamble, 0..first_row_start);
309            }
310            assert!(range.start < range.end);
311
312            let mut rows_seen = 0;
313            let mut new_start = 0;
314            let mut new_levels_start = 0;
315
316            if let Some(def) = def {
317                let def = &def.as_ref()[first_row_start as usize..];
318
319                // range.start == 0 always maps to 0 (even with invis items), otherwise we need to walk
320                let mut lead_invis_seen = 0;
321
322                if range.start > 0 {
323                    if def[0] > max_visible_def {
324                        lead_invis_seen += 1;
325                    }
326                    for (idx, (rep, def)) in rep.iter().zip(def).skip(1).enumerate() {
327                        if *rep == max_rep {
328                            rows_seen += 1;
329                            if rows_seen == range.start {
330                                new_start = idx as u64 + 1 - lead_invis_seen;
331                                new_levels_start = idx as u64 + 1;
332                                break;
333                            }
334                        }
335                        if *def > max_visible_def {
336                            lead_invis_seen += 1;
337                        }
338                    }
339                }
340
341                rows_seen += 1;
342
343                let mut new_end = u64::MAX;
344                let mut new_levels_end = rep.len() as u64;
345                let new_start_is_visible = def[new_levels_start as usize] <= max_visible_def;
346                let mut tail_invis_seen = if new_start_is_visible { 0 } else { 1 };
347                for (idx, (rep, def)) in rep[(new_levels_start + 1) as usize..]
348                    .iter()
349                    .zip(&def[(new_levels_start + 1) as usize..])
350                    .enumerate()
351                {
352                    if *rep == max_rep {
353                        rows_seen += 1;
354                        if rows_seen == range.end + 1 {
355                            new_end = idx as u64 + new_start + 1 - tail_invis_seen;
356                            new_levels_end = idx as u64 + new_levels_start + 1;
357                            break;
358                        }
359                    }
360                    if *def > max_visible_def {
361                        tail_invis_seen += 1;
362                    }
363                }
364
365                if new_end == u64::MAX {
366                    new_levels_end = rep.len() as u64;
367                    let total_invis_seen = lead_invis_seen + tail_invis_seen;
368                    new_end = rep.len() as u64 - total_invis_seen;
369                }
370
371                assert_ne!(new_end, u64::MAX);
372
373                // Adjust for any skipped preamble
374                if preamble_action == PreambleAction::Skip {
375                    new_start += items_in_preamble;
376                    new_end += items_in_preamble;
377                    new_levels_start += first_row_start;
378                    new_levels_end += first_row_start;
379                } else if preamble_action == PreambleAction::Take {
380                    debug_assert_eq!(new_start, 0);
381                    debug_assert_eq!(new_levels_start, 0);
382                    new_end += items_in_preamble;
383                    new_levels_end += first_row_start;
384                }
385
386                debug_assert!(new_end <= total_items);
387                (new_start..new_end, new_levels_start..new_levels_end)
388            } else {
389                // Easy case, there are no invisible items, so we don't need to check for them
390                // The items range and levels range will be the same.  We do still need to walk
391                // the rep levels to find the row boundaries
392
393                // range.start == 0 always maps to 0, otherwise we need to walk
394                if range.start > 0 {
395                    for (idx, rep) in rep.iter().skip(1).enumerate() {
396                        if *rep == max_rep {
397                            rows_seen += 1;
398                            if rows_seen == range.start {
399                                new_start = idx as u64 + 1;
400                                break;
401                            }
402                        }
403                    }
404                }
405                let mut new_end = rep.len() as u64;
406                // range.end == max_items always maps to rep.len(), otherwise we need to walk
407                if range.end < total_items {
408                    for (idx, rep) in rep[(new_start + 1) as usize..].iter().enumerate() {
409                        if *rep == max_rep {
410                            rows_seen += 1;
411                            if rows_seen == range.end {
412                                new_end = idx as u64 + new_start + 1;
413                                break;
414                            }
415                        }
416                    }
417                }
418
419                // Adjust for any skipped preamble
420                if preamble_action == PreambleAction::Skip {
421                    new_start += first_row_start;
422                    new_end += first_row_start;
423                } else if preamble_action == PreambleAction::Take {
424                    debug_assert_eq!(new_start, 0);
425                    new_end += first_row_start;
426                }
427
428                debug_assert!(new_end <= total_items);
429                (new_start..new_end, new_start..new_end)
430            }
431        } else {
432            // No repetition info, easy case, just use the range as-is and the item
433            // and level ranges are the same
434            (range.clone(), range)
435        }
436    }
437
438    // read `num_buffers` buffer sizes from `buf` starting at `offset`
439    fn read_buffer_sizes<const LARGE: bool>(
440        buf: &[u8],
441        offset: &mut usize,
442        num_buffers: u64,
443    ) -> Vec<u32> {
444        let read_size = if LARGE { 4 } else { 2 };
445        (0..num_buffers)
446            .map(|_| {
447                let bytes = &buf[*offset..*offset + read_size];
448                let size = if LARGE {
449                    u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
450                } else {
451                    // the buffer size is read from u16 but is stored as u32 after decoding for consistency
452                    u16::from_le_bytes([bytes[0], bytes[1]]) as u32
453                };
454                *offset += read_size;
455                size
456            })
457            .collect()
458    }
459
460    // Unserialize a miniblock into a collection of vectors
461    fn decode_miniblock_chunk(
462        &self,
463        buf: &LanceBuffer,
464        items_in_chunk: u64,
465    ) -> Result<DecodedMiniBlockChunk> {
466        let mut offset = 0;
467        let num_levels = u16::from_le_bytes([buf[offset], buf[offset + 1]]);
468        offset += 2;
469
470        let rep_size = if self.rep_decompressor.is_some() {
471            let rep_size = u16::from_le_bytes([buf[offset], buf[offset + 1]]);
472            offset += 2;
473            Some(rep_size)
474        } else {
475            None
476        };
477        let def_size = if self.def_decompressor.is_some() {
478            let def_size = u16::from_le_bytes([buf[offset], buf[offset + 1]]);
479            offset += 2;
480            Some(def_size)
481        } else {
482            None
483        };
484
485        let buffer_sizes = if self.has_large_chunk {
486            Self::read_buffer_sizes::<true>(buf, &mut offset, self.num_buffers)
487        } else {
488            Self::read_buffer_sizes::<false>(buf, &mut offset, self.num_buffers)
489        };
490
491        offset += pad_bytes::<MINIBLOCK_ALIGNMENT>(offset);
492
493        let rep = rep_size.map(|rep_size| {
494            let rep = buf.slice_with_length(offset, rep_size as usize);
495            offset += rep_size as usize;
496            offset += pad_bytes::<MINIBLOCK_ALIGNMENT>(offset);
497            rep
498        });
499
500        let def = def_size.map(|def_size| {
501            let def = buf.slice_with_length(offset, def_size as usize);
502            offset += def_size as usize;
503            offset += pad_bytes::<MINIBLOCK_ALIGNMENT>(offset);
504            def
505        });
506
507        let buffers = buffer_sizes
508            .into_iter()
509            .map(|buf_size| {
510                let buf = buf.slice_with_length(offset, buf_size as usize);
511                offset += buf_size as usize;
512                offset += pad_bytes::<MINIBLOCK_ALIGNMENT>(offset);
513                buf
514            })
515            .collect::<Vec<_>>();
516
517        let values = self
518            .value_decompressor
519            .decompress(buffers, items_in_chunk)?;
520
521        let rep = rep
522            .map(|rep| {
523                Self::decode_levels(
524                    self.rep_decompressor.as_ref().unwrap().as_ref(),
525                    rep,
526                    num_levels,
527                )
528            })
529            .transpose()?;
530        let def = def
531            .map(|def| {
532                Self::decode_levels(
533                    self.def_decompressor.as_ref().unwrap().as_ref(),
534                    def,
535                    num_levels,
536                )
537            })
538            .transpose()?;
539
540        Ok(DecodedMiniBlockChunk { rep, def, values })
541    }
542}
543
544impl DecodePageTask for DecodeMiniBlockTask {
545    fn decode(self: Box<Self>) -> Result<DecodedPage> {
546        // First, we create output buffers for the rep and def and data
547        let mut repbuf: Option<LevelBuffer> = None;
548        let mut defbuf: Option<LevelBuffer> = None;
549
550        let max_rep = self.def_meaning.iter().filter(|l| l.is_list()).count() as u16;
551
552        // This is probably an over-estimate but it's quick and easy to calculate
553        let estimated_size_bytes = self
554            .instructions
555            .iter()
556            .map(|(_, chunk)| chunk.data.len())
557            .sum::<usize>()
558            * 2;
559        let mut data_builder =
560            DataBlockBuilder::with_capacity_estimate(estimated_size_bytes as u64);
561
562        // We need to keep track of the offset into repbuf/defbuf that we are building up
563        let mut level_offset = 0;
564
565        // Pre-compute caching needs for each chunk by checking if the next chunk is the same
566        let needs_caching: Vec<bool> = self
567            .instructions
568            .windows(2)
569            .map(|w| w[0].1.chunk_idx == w[1].1.chunk_idx)
570            .chain(std::iter::once(false)) // the last one never needs caching
571            .collect();
572
573        // Cache for storing decoded chunks when beneficial
574        let mut chunk_cache: Option<(usize, DecodedMiniBlockChunk)> = None;
575
576        // Now we iterate through each instruction and process it
577        for (idx, (instructions, chunk)) in self.instructions.iter().enumerate() {
578            let should_cache_this_chunk = needs_caching[idx];
579
580            let decoded_chunk = match &chunk_cache {
581                Some((cached_chunk_idx, cached_chunk)) if *cached_chunk_idx == chunk.chunk_idx => {
582                    // Clone only when we have a cache hit (much cheaper than decoding)
583                    cached_chunk.clone()
584                }
585                _ => {
586                    // Cache miss, need to decode
587                    let decoded = self.decode_miniblock_chunk(&chunk.data, chunk.items_in_chunk)?;
588
589                    // Only update cache if this chunk will benefit the next access
590                    if should_cache_this_chunk {
591                        chunk_cache = Some((chunk.chunk_idx, decoded.clone()));
592                    }
593                    decoded
594                }
595            };
596
597            let DecodedMiniBlockChunk { rep, def, values } = decoded_chunk;
598
599            // Our instructions tell us which rows we want to take from this chunk
600            let row_range_start =
601                instructions.rows_to_skip + instructions.chunk_instructions.rows_to_skip;
602            let row_range_end = row_range_start + instructions.rows_to_take;
603
604            // We use the rep info to map the row range to an item range / levels range
605            let (item_range, level_range) = Self::map_range(
606                row_range_start..row_range_end,
607                rep.as_ref(),
608                def.as_ref(),
609                max_rep,
610                self.max_visible_level,
611                chunk.items_in_chunk,
612                instructions.preamble_action,
613            );
614            if item_range.end - item_range.start > chunk.items_in_chunk {
615                return Err(lance_core::Error::internal(format!(
616                    "Item range {:?} is greater than chunk items in chunk {:?}",
617                    item_range, chunk.items_in_chunk
618                )));
619            }
620
621            // Now we append the data to the output buffers
622            Self::extend_levels(level_range.clone(), &mut repbuf, &rep, level_offset);
623            Self::extend_levels(level_range.clone(), &mut defbuf, &def, level_offset);
624            level_offset += (level_range.end - level_range.start) as usize;
625            data_builder.append(&values, item_range);
626        }
627
628        let mut data = data_builder.finish();
629
630        let unraveler =
631            RepDefUnraveler::new(repbuf, defbuf, self.def_meaning.clone(), data.num_values());
632
633        if let Some(dictionary) = &self.dictionary_data {
634            // Don't decode here, that happens later (if needed)
635            let DataBlock::FixedWidth(indices) = data else {
636                return Err(lance_core::Error::internal(format!(
637                    "Expected FixedWidth DataBlock for dictionary indices, got {:?}",
638                    data
639                )));
640            };
641            data = DataBlock::Dictionary(DictionaryDataBlock::from_parts(
642                indices,
643                dictionary.as_ref().clone(),
644            ));
645        }
646
647        Ok(DecodedPage {
648            data,
649            repdef: unraveler,
650        })
651    }
652}
653
654/// A chunk that has been loaded by the miniblock scheduler (but not
655/// yet decoded)
656#[derive(Debug)]
657struct LoadedChunk {
658    data: LanceBuffer,
659    items_in_chunk: u64,
660    byte_range: Range<u64>,
661    chunk_idx: usize,
662}
663
664impl Clone for LoadedChunk {
665    fn clone(&self) -> Self {
666        Self {
667            // Safe as we always create borrowed buffers here
668            data: self.data.clone(),
669            items_in_chunk: self.items_in_chunk,
670            byte_range: self.byte_range.clone(),
671            chunk_idx: self.chunk_idx,
672        }
673    }
674}
675
676/// Decodes mini-block formatted data.  See [`PrimitiveStructuralEncoder`] for more
677/// details on the different layouts.
678#[derive(Debug)]
679struct MiniBlockDecoder {
680    rep_decompressor: Option<Arc<dyn BlockDecompressor>>,
681    def_decompressor: Option<Arc<dyn BlockDecompressor>>,
682    value_decompressor: Arc<dyn MiniBlockDecompressor>,
683    def_meaning: Arc<[DefinitionInterpretation]>,
684    loaded_chunks: VecDeque<LoadedChunk>,
685    instructions: VecDeque<ChunkInstructions>,
686    offset_in_current_chunk: u64,
687    num_rows: u64,
688    num_buffers: u64,
689    dictionary: Option<Arc<DataBlock>>,
690    has_large_chunk: bool,
691}
692
693/// See [`MiniBlockScheduler`] for more details on the scheduling and decoding
694/// process for miniblock encoded data.
695impl StructuralPageDecoder for MiniBlockDecoder {
696    fn drain(&mut self, num_rows: u64) -> Result<Box<dyn DecodePageTask>> {
697        let mut items_desired = num_rows;
698        let mut need_preamble = false;
699        let mut skip_in_chunk = self.offset_in_current_chunk;
700        let mut drain_instructions = Vec::new();
701        while items_desired > 0 || need_preamble {
702            let (instructions, consumed) = self
703                .instructions
704                .front()
705                .unwrap()
706                .drain_from_instruction(&mut items_desired, &mut need_preamble, &mut skip_in_chunk);
707
708            while self.loaded_chunks.front().unwrap().chunk_idx
709                != instructions.chunk_instructions.chunk_idx
710            {
711                self.loaded_chunks.pop_front();
712            }
713            drain_instructions.push((instructions, self.loaded_chunks.front().unwrap().clone()));
714            if consumed {
715                self.instructions.pop_front();
716            }
717        }
718        // We can throw away need_preamble here because it must be false.  If it were true it would mean
719        // we were still in the middle of loading rows.  We do need to latch skip_in_chunk though.
720        self.offset_in_current_chunk = skip_in_chunk;
721
722        let max_visible_level = self
723            .def_meaning
724            .iter()
725            .take_while(|l| !l.is_list())
726            .map(|l| l.num_def_levels())
727            .sum::<u16>();
728
729        Ok(Box::new(DecodeMiniBlockTask {
730            instructions: drain_instructions,
731            def_decompressor: self.def_decompressor.clone(),
732            rep_decompressor: self.rep_decompressor.clone(),
733            value_decompressor: self.value_decompressor.clone(),
734            dictionary_data: self.dictionary.clone(),
735            def_meaning: self.def_meaning.clone(),
736            num_buffers: self.num_buffers,
737            max_visible_level,
738            has_large_chunk: self.has_large_chunk,
739        }))
740    }
741
742    fn num_rows(&self) -> u64 {
743        self.num_rows
744    }
745}
746
747#[derive(Debug)]
748struct CachedComplexAllNullState {
749    rep: Option<ScalarBuffer<u16>>,
750    def: Option<ScalarBuffer<u16>>,
751}
752
753impl DeepSizeOf for CachedComplexAllNullState {
754    fn deep_size_of_children(&self, _ctx: &mut Context) -> usize {
755        self.rep.as_ref().map(|buf| buf.len() * 2).unwrap_or(0)
756            + self.def.as_ref().map(|buf| buf.len() * 2).unwrap_or(0)
757    }
758}
759
760impl CachedPageData for CachedComplexAllNullState {
761    fn as_arc_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync + 'static> {
762        self
763    }
764}
765
766/// A scheduler for all-null data that has repetition and definition levels
767///
768/// We still need to do some I/O in this case because we need to figure out what kind of null we
769/// are dealing with (null list, null struct, what level null struct, etc.)
770///
771/// TODO: Right now we just load the entire rep/def at initialization time and cache it.  This is a touch
772/// RAM aggressive and maybe we want something more lazy in the future.  On the other hand, it's simple
773/// and fast so...maybe not :)
774#[derive(Debug)]
775pub struct ComplexAllNullScheduler {
776    // Set from protobuf
777    buffer_offsets_and_sizes: Arc<[(u64, u64)]>,
778    def_meaning: Arc<[DefinitionInterpretation]>,
779    repdef: Option<Arc<CachedComplexAllNullState>>,
780    max_visible_level: u16,
781    rep_decompressor: Option<Arc<dyn BlockDecompressor>>,
782    def_decompressor: Option<Arc<dyn BlockDecompressor>>,
783    num_rep_values: u64,
784    num_def_values: u64,
785}
786
787impl ComplexAllNullScheduler {
788    pub fn new(
789        buffer_offsets_and_sizes: Arc<[(u64, u64)]>,
790        def_meaning: Arc<[DefinitionInterpretation]>,
791        rep_decompressor: Option<Arc<dyn BlockDecompressor>>,
792        def_decompressor: Option<Arc<dyn BlockDecompressor>>,
793        num_rep_values: u64,
794        num_def_values: u64,
795    ) -> Self {
796        let max_visible_level = def_meaning
797            .iter()
798            .take_while(|l| !l.is_list())
799            .map(|l| l.num_def_levels())
800            .sum::<u16>();
801        Self {
802            buffer_offsets_and_sizes,
803            def_meaning,
804            repdef: None,
805            max_visible_level,
806            rep_decompressor,
807            def_decompressor,
808            num_rep_values,
809            num_def_values,
810        }
811    }
812}
813
814impl StructuralPageScheduler for ComplexAllNullScheduler {
815    fn initialize<'a>(
816        &'a mut self,
817        io: &Arc<dyn EncodingsIo>,
818    ) -> BoxFuture<'a, Result<Arc<dyn CachedPageData>>> {
819        // Fully load the rep & def buffers, as needed
820        let (rep_pos, rep_size) = self.buffer_offsets_and_sizes[0];
821        let (def_pos, def_size) = self.buffer_offsets_and_sizes[1];
822        let has_rep = rep_size > 0;
823        let has_def = def_size > 0;
824
825        let mut reads = Vec::with_capacity(2);
826        if has_rep {
827            reads.push(rep_pos..rep_pos + rep_size);
828        }
829        if has_def {
830            reads.push(def_pos..def_pos + def_size);
831        }
832
833        let data = io.submit_request(reads, 0);
834        let rep_decompressor = self.rep_decompressor.clone();
835        let def_decompressor = self.def_decompressor.clone();
836        let num_rep_values = self.num_rep_values;
837        let num_def_values = self.num_def_values;
838
839        async move {
840            let data = data.await?;
841            let mut data_iter = data.into_iter();
842
843            let decompress_levels = |compressed_bytes: Bytes,
844                                     decompressor: &Arc<dyn BlockDecompressor>,
845                                     num_values: u64,
846                                     level_type: &str|
847             -> Result<ScalarBuffer<u16>> {
848                let compressed_buffer = LanceBuffer::from_bytes(compressed_bytes, 1);
849                let decompressed = decompressor.decompress(compressed_buffer, num_values)?;
850                match decompressed {
851                    DataBlock::FixedWidth(block) => {
852                        if block.num_values != num_values {
853                            return Err(Error::invalid_input_source(format!(
854                                "Unexpected {} level count after decompression: expected {}, got {}",
855                                level_type, num_values, block.num_values
856                            )
857                            .into()));
858                        }
859                        if block.bits_per_value != 16 {
860                            return Err(Error::invalid_input_source(format!(
861                                "Unexpected {} level bit width after decompression: expected 16, got {}",
862                                level_type, block.bits_per_value
863                            )
864                            .into()));
865                        }
866                        Ok(block.data.borrow_to_typed_slice::<u16>())
867                    }
868                    _ => Err(Error::invalid_input_source(format!(
869                        "Expected fixed-width data block for {} levels",
870                        level_type
871                    )
872                    .into())),
873                }
874            };
875
876            let rep = if has_rep {
877                let rep = data_iter.next().unwrap();
878                if let Some(rep_decompressor) = rep_decompressor.as_ref() {
879                    Some(decompress_levels(
880                        rep,
881                        rep_decompressor,
882                        num_rep_values,
883                        "repetition",
884                    )?)
885                } else {
886                    let rep = LanceBuffer::from_bytes(rep, 2);
887                    let rep = rep.borrow_to_typed_slice::<u16>();
888                    Some(rep)
889                }
890            } else {
891                None
892            };
893
894            let def = if has_def {
895                let def = data_iter.next().unwrap();
896                if let Some(def_decompressor) = def_decompressor.as_ref() {
897                    Some(decompress_levels(
898                        def,
899                        def_decompressor,
900                        num_def_values,
901                        "definition",
902                    )?)
903                } else {
904                    let def = LanceBuffer::from_bytes(def, 2);
905                    let def = def.borrow_to_typed_slice::<u16>();
906                    Some(def)
907                }
908            } else {
909                None
910            };
911
912            let repdef = Arc::new(CachedComplexAllNullState { rep, def });
913
914            self.repdef = Some(repdef.clone());
915
916            Ok(repdef as Arc<dyn CachedPageData>)
917        }
918        .boxed()
919    }
920
921    fn load(&mut self, data: &Arc<dyn CachedPageData>) {
922        self.repdef = Some(
923            data.clone()
924                .as_arc_any()
925                .downcast::<CachedComplexAllNullState>()
926                .unwrap(),
927        );
928    }
929
930    fn schedule_ranges(
931        &self,
932        ranges: &[Range<u64>],
933        _io: &Arc<dyn EncodingsIo>,
934    ) -> Result<Vec<PageLoadTask>> {
935        let ranges = VecDeque::from_iter(ranges.iter().cloned());
936        let num_rows = ranges.iter().map(|r| r.end - r.start).sum::<u64>();
937        let decoder = Box::new(ComplexAllNullPageDecoder {
938            ranges,
939            rep: self.repdef.as_ref().unwrap().rep.clone(),
940            def: self.repdef.as_ref().unwrap().def.clone(),
941            num_rows,
942            def_meaning: self.def_meaning.clone(),
943            max_visible_level: self.max_visible_level,
944        }) as Box<dyn StructuralPageDecoder>;
945        let page_load_task = PageLoadTask {
946            decoder_fut: std::future::ready(Ok(decoder)).boxed(),
947            num_rows,
948        };
949        Ok(vec![page_load_task])
950    }
951}
952
953#[derive(Debug)]
954pub struct ComplexAllNullPageDecoder {
955    ranges: VecDeque<Range<u64>>,
956    rep: Option<ScalarBuffer<u16>>,
957    def: Option<ScalarBuffer<u16>>,
958    num_rows: u64,
959    def_meaning: Arc<[DefinitionInterpretation]>,
960    max_visible_level: u16,
961}
962
963impl ComplexAllNullPageDecoder {
964    fn drain_ranges(&mut self, num_rows: u64) -> Vec<Range<u64>> {
965        let mut rows_desired = num_rows;
966        let mut ranges = Vec::with_capacity(self.ranges.len());
967        while rows_desired > 0 {
968            let front = self.ranges.front_mut().unwrap();
969            let avail = front.end - front.start;
970            if avail > rows_desired {
971                ranges.push(front.start..front.start + rows_desired);
972                front.start += rows_desired;
973                rows_desired = 0;
974            } else {
975                ranges.push(self.ranges.pop_front().unwrap());
976                rows_desired -= avail;
977            }
978        }
979        ranges
980    }
981}
982
983impl StructuralPageDecoder for ComplexAllNullPageDecoder {
984    fn drain(&mut self, num_rows: u64) -> Result<Box<dyn DecodePageTask>> {
985        let drained_ranges = self.drain_ranges(num_rows);
986        Ok(Box::new(DecodeComplexAllNullTask {
987            ranges: drained_ranges,
988            rep: self.rep.clone(),
989            def: self.def.clone(),
990            def_meaning: self.def_meaning.clone(),
991            max_visible_level: self.max_visible_level,
992        }))
993    }
994
995    fn num_rows(&self) -> u64 {
996        self.num_rows
997    }
998}
999
1000/// We use `ranges` to slice into `rep` and `def` and create rep/def buffers
1001/// for the null data.
1002#[derive(Debug)]
1003pub struct DecodeComplexAllNullTask {
1004    ranges: Vec<Range<u64>>,
1005    rep: Option<ScalarBuffer<u16>>,
1006    def: Option<ScalarBuffer<u16>>,
1007    def_meaning: Arc<[DefinitionInterpretation]>,
1008    max_visible_level: u16,
1009}
1010
1011impl DecodeComplexAllNullTask {
1012    fn decode_level(
1013        &self,
1014        levels: &Option<ScalarBuffer<u16>>,
1015        num_values: u64,
1016    ) -> Option<Vec<u16>> {
1017        levels.as_ref().map(|levels| {
1018            let mut referenced_levels = Vec::with_capacity(num_values as usize);
1019            for range in &self.ranges {
1020                referenced_levels.extend(
1021                    levels[range.start as usize..range.end as usize]
1022                        .iter()
1023                        .copied(),
1024                );
1025            }
1026            referenced_levels
1027        })
1028    }
1029}
1030
1031impl DecodePageTask for DecodeComplexAllNullTask {
1032    fn decode(self: Box<Self>) -> Result<DecodedPage> {
1033        let num_values = self.ranges.iter().map(|r| r.end - r.start).sum::<u64>();
1034        let rep = self.decode_level(&self.rep, num_values);
1035        let def = self.decode_level(&self.def, num_values);
1036
1037        // If there are definition levels there may be empty / null lists which are not visible
1038        // in the items array.  We need to account for that here to figure out how many values
1039        // should be in the items array.
1040        let num_values = if let Some(def) = &def {
1041            def.iter().filter(|&d| *d <= self.max_visible_level).count() as u64
1042        } else {
1043            num_values
1044        };
1045
1046        let data = DataBlock::AllNull(AllNullDataBlock { num_values });
1047        let unraveler = RepDefUnraveler::new(rep, def, self.def_meaning, num_values);
1048        Ok(DecodedPage {
1049            data,
1050            repdef: unraveler,
1051        })
1052    }
1053}
1054
1055/// A scheduler for simple all-null data
1056///
1057/// "simple" all-null data is data that is all null and only has a single level of definition and
1058/// no repetition.  We don't need to read any data at all in this case.
1059#[derive(Debug, Default)]
1060pub struct SimpleAllNullScheduler {}
1061
1062impl StructuralPageScheduler for SimpleAllNullScheduler {
1063    fn initialize<'a>(
1064        &'a mut self,
1065        _io: &Arc<dyn EncodingsIo>,
1066    ) -> BoxFuture<'a, Result<Arc<dyn CachedPageData>>> {
1067        std::future::ready(Ok(Arc::new(NoCachedPageData) as Arc<dyn CachedPageData>)).boxed()
1068    }
1069
1070    fn load(&mut self, _cache: &Arc<dyn CachedPageData>) {}
1071
1072    fn schedule_ranges(
1073        &self,
1074        ranges: &[Range<u64>],
1075        _io: &Arc<dyn EncodingsIo>,
1076    ) -> Result<Vec<PageLoadTask>> {
1077        let num_rows = ranges.iter().map(|r| r.end - r.start).sum::<u64>();
1078        let decoder =
1079            Box::new(SimpleAllNullPageDecoder { num_rows }) as Box<dyn StructuralPageDecoder>;
1080        let page_load_task = PageLoadTask {
1081            decoder_fut: std::future::ready(Ok(decoder)).boxed(),
1082            num_rows,
1083        };
1084        Ok(vec![page_load_task])
1085    }
1086}
1087
1088/// A page decode task for all-null data without any
1089/// repetition and only a single level of definition
1090#[derive(Debug)]
1091struct SimpleAllNullDecodePageTask {
1092    num_values: u64,
1093}
1094impl DecodePageTask for SimpleAllNullDecodePageTask {
1095    fn decode(self: Box<Self>) -> Result<DecodedPage> {
1096        let unraveler = RepDefUnraveler::new(
1097            None,
1098            Some(vec![1; self.num_values as usize]),
1099            Arc::new([DefinitionInterpretation::NullableItem]),
1100            self.num_values,
1101        );
1102        Ok(DecodedPage {
1103            data: DataBlock::AllNull(AllNullDataBlock {
1104                num_values: self.num_values,
1105            }),
1106            repdef: unraveler,
1107        })
1108    }
1109}
1110
1111#[derive(Debug)]
1112pub struct SimpleAllNullPageDecoder {
1113    num_rows: u64,
1114}
1115
1116impl StructuralPageDecoder for SimpleAllNullPageDecoder {
1117    fn drain(&mut self, num_rows: u64) -> Result<Box<dyn DecodePageTask>> {
1118        Ok(Box::new(SimpleAllNullDecodePageTask {
1119            num_values: num_rows,
1120        }))
1121    }
1122
1123    fn num_rows(&self) -> u64 {
1124        self.num_rows
1125    }
1126}
1127
1128#[derive(Debug, Clone)]
1129struct MiniBlockSchedulerDictionary {
1130    // These come from the protobuf
1131    dictionary_decompressor: Arc<dyn BlockDecompressor>,
1132    dictionary_buf_position_and_size: (u64, u64),
1133    dictionary_data_alignment: u64,
1134    num_dictionary_items: u64,
1135}
1136
1137/// Individual block metadata within a MiniBlock repetition index.
1138#[derive(Debug)]
1139struct MiniBlockRepIndexBlock {
1140    // The index of the first row that starts after the beginning of this block.  If the block
1141    // has a preamble this will be the row after the preamble.  If the block is entirely preamble
1142    // then this will be a row that starts in some future block.
1143    first_row: u64,
1144    // The number of rows in the block, including the trailer but not the preamble.
1145    // Can be 0 if the block is entirely preamble
1146    starts_including_trailer: u64,
1147    // Whether the block has a preamble
1148    has_preamble: bool,
1149    // Whether the block has a trailer
1150    has_trailer: bool,
1151}
1152
1153impl DeepSizeOf for MiniBlockRepIndexBlock {
1154    fn deep_size_of_children(&self, _context: &mut Context) -> usize {
1155        0
1156    }
1157}
1158
1159/// Repetition index for MiniBlock encoding.
1160///
1161/// Stores block-level offset information to enable efficient random
1162/// access to nested data structures within mini-blocks.
1163#[derive(Debug)]
1164struct MiniBlockRepIndex {
1165    blocks: Vec<MiniBlockRepIndexBlock>,
1166}
1167
1168impl DeepSizeOf for MiniBlockRepIndex {
1169    fn deep_size_of_children(&self, context: &mut Context) -> usize {
1170        self.blocks.deep_size_of_children(context)
1171    }
1172}
1173
1174impl MiniBlockRepIndex {
1175    /// Decode repetition index from chunk metadata using default values.
1176    ///
1177    /// This creates a repetition index where each chunk has no partial values
1178    /// and no trailers, suitable for simple sequential data layouts.
1179    pub fn default_from_chunks(chunks: &[ChunkMeta]) -> Self {
1180        let mut blocks = Vec::with_capacity(chunks.len());
1181        let mut offset: u64 = 0;
1182
1183        for c in chunks {
1184            blocks.push(MiniBlockRepIndexBlock {
1185                first_row: offset,
1186                starts_including_trailer: c.num_values,
1187                has_preamble: false,
1188                has_trailer: false,
1189            });
1190
1191            offset += c.num_values;
1192        }
1193
1194        Self { blocks }
1195    }
1196
1197    /// Decode repetition index from raw bytes in little-endian format.
1198    ///
1199    /// The bytes should contain u64 values arranged in groups of `stride` elements,
1200    /// where the first two values of each group represent ends_count and partial_count.
1201    /// Returns an empty index if no bytes are provided.
1202    pub fn decode_from_bytes(rep_bytes: &[u8], stride: usize) -> Self {
1203        // Convert bytes to u64 slice, handling alignment automatically
1204        let buffer = crate::buffer::LanceBuffer::from(rep_bytes.to_vec());
1205        let u64_slice = buffer.borrow_to_typed_slice::<u64>();
1206        let n = u64_slice.len() / stride;
1207
1208        let mut blocks = Vec::with_capacity(n);
1209        let mut chunk_has_preamble = false;
1210        let mut offset: u64 = 0;
1211
1212        // Extract first two values from each block: ends_count and partial_count
1213        for i in 0..n {
1214            let base_idx = i * stride;
1215            let ends = u64_slice[base_idx];
1216            let partial = u64_slice[base_idx + 1];
1217
1218            let has_trailer = partial > 0;
1219            // Convert branches to arithmetic for better compiler optimization
1220            let starts_including_trailer =
1221                ends + (has_trailer as u64) - (chunk_has_preamble as u64);
1222
1223            blocks.push(MiniBlockRepIndexBlock {
1224                first_row: offset,
1225                starts_including_trailer,
1226                has_preamble: chunk_has_preamble,
1227                has_trailer,
1228            });
1229
1230            chunk_has_preamble = has_trailer;
1231            offset += starts_including_trailer;
1232        }
1233
1234        Self { blocks }
1235    }
1236}
1237
1238/// State that is loaded once and cached for future lookups
1239#[derive(Debug)]
1240struct MiniBlockCacheableState {
1241    /// Metadata that describes each chunk in the page
1242    chunk_meta: Vec<ChunkMeta>,
1243    /// The decoded repetition index
1244    rep_index: MiniBlockRepIndex,
1245    /// The dictionary for the page, if any
1246    dictionary: Option<Arc<DataBlock>>,
1247}
1248
1249impl DeepSizeOf for MiniBlockCacheableState {
1250    fn deep_size_of_children(&self, context: &mut Context) -> usize {
1251        self.rep_index.deep_size_of_children(context)
1252            + self
1253                .dictionary
1254                .as_ref()
1255                .map(|dict| dict.data_size() as usize)
1256                .unwrap_or(0)
1257    }
1258}
1259
1260impl CachedPageData for MiniBlockCacheableState {
1261    fn as_arc_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync + 'static> {
1262        self
1263    }
1264}
1265
1266/// A scheduler for a page that has been encoded with the mini-block layout
1267///
1268/// Scheduling mini-block encoded data is simple in concept and somewhat complex
1269/// in practice.
1270///
1271/// First, during initialization, we load the chunk metadata, the repetition index,
1272/// and the dictionary (these last two may not be present)
1273///
1274/// Then, during scheduling, we use the user's requested row ranges and the repetition
1275/// index to determine which chunks we need and which rows we need from those chunks.
1276///
1277/// For example, if the repetition index is: [50, 3], [50, 0], [10, 0] and the range
1278/// from the user is 40..60 then we need to:
1279///
1280///  - Read the first chunk and skip the first 40 rows, then read 10 full rows, and
1281///    then read 3 items for the 11th row of our range.
1282///  - Read the second chunk and read the remaining items in our 11th row and then read
1283///    the remaining 9 full rows.
1284///
1285/// Then, if we are going to decode that in batches of 5, we need to make decode tasks.
1286/// The first two decode tasks will just need the first chunk.  The third decode task will
1287/// need the first chunk (for the trailer which has the 11th row in our range) and the second
1288/// chunk.  The final decode task will just need the second chunk.
1289///
1290/// The above prose descriptions are what are represented by `ChunkInstructions` and
1291/// `ChunkDrainInstructions`.
1292#[derive(Debug)]
1293pub struct MiniBlockScheduler {
1294    // These come from the protobuf
1295    buffer_offsets_and_sizes: Vec<(u64, u64)>,
1296    priority: u64,
1297    items_in_page: u64,
1298    repetition_index_depth: u16,
1299    num_buffers: u64,
1300    rep_decompressor: Option<Arc<dyn BlockDecompressor>>,
1301    def_decompressor: Option<Arc<dyn BlockDecompressor>>,
1302    value_decompressor: Arc<dyn MiniBlockDecompressor>,
1303    def_meaning: Arc<[DefinitionInterpretation]>,
1304    dictionary: Option<MiniBlockSchedulerDictionary>,
1305    // This is set after initialization
1306    page_meta: Option<Arc<MiniBlockCacheableState>>,
1307    has_large_chunk: bool,
1308}
1309
1310impl MiniBlockScheduler {
1311    fn try_new(
1312        buffer_offsets_and_sizes: &[(u64, u64)],
1313        priority: u64,
1314        items_in_page: u64,
1315        layout: &pb21::MiniBlockLayout,
1316        decompressors: &dyn DecompressionStrategy,
1317    ) -> Result<Self> {
1318        let rep_decompressor = layout
1319            .rep_compression
1320            .as_ref()
1321            .map(|rep_compression| {
1322                decompressors
1323                    .create_block_decompressor(rep_compression)
1324                    .map(Arc::from)
1325            })
1326            .transpose()?;
1327        let def_decompressor = layout
1328            .def_compression
1329            .as_ref()
1330            .map(|def_compression| {
1331                decompressors
1332                    .create_block_decompressor(def_compression)
1333                    .map(Arc::from)
1334            })
1335            .transpose()?;
1336        let def_meaning = layout
1337            .layers
1338            .iter()
1339            .map(|l| ProtobufUtils21::repdef_layer_to_def_interp(*l))
1340            .collect::<Vec<_>>();
1341        let value_decompressor = decompressors.create_miniblock_decompressor(
1342            layout.value_compression.as_ref().unwrap(),
1343            decompressors,
1344        )?;
1345
1346        let dictionary = if let Some(dictionary_encoding) = layout.dictionary.as_ref() {
1347            let num_dictionary_items = layout.num_dictionary_items;
1348            let dictionary_decompressor = decompressors
1349                .create_block_decompressor(dictionary_encoding)?
1350                .into();
1351            let dictionary_data_alignment = match dictionary_encoding.compression.as_ref().unwrap()
1352            {
1353                Compression::Variable(_) => 4,
1354                Compression::Flat(_) => 16,
1355                Compression::General(_) => 1,
1356                Compression::InlineBitpacking(_) | Compression::OutOfLineBitpacking(_) => {
1357                    crate::encoder::MIN_PAGE_BUFFER_ALIGNMENT
1358                }
1359                _ => {
1360                    return Err(Error::invalid_input_source(
1361                        format!(
1362                            "Unsupported mini-block dictionary encoding: {:?}",
1363                            dictionary_encoding.compression.as_ref().unwrap()
1364                        )
1365                        .into(),
1366                    ));
1367                }
1368            };
1369            Some(MiniBlockSchedulerDictionary {
1370                dictionary_decompressor,
1371                dictionary_buf_position_and_size: buffer_offsets_and_sizes[2],
1372                dictionary_data_alignment,
1373                num_dictionary_items,
1374            })
1375        } else {
1376            None
1377        };
1378
1379        Ok(Self {
1380            buffer_offsets_and_sizes: buffer_offsets_and_sizes.to_vec(),
1381            rep_decompressor,
1382            def_decompressor,
1383            value_decompressor: value_decompressor.into(),
1384            repetition_index_depth: layout.repetition_index_depth as u16,
1385            num_buffers: layout.num_buffers,
1386            priority,
1387            items_in_page,
1388            dictionary,
1389            def_meaning: def_meaning.into(),
1390            page_meta: None,
1391            has_large_chunk: layout.has_large_chunk,
1392        })
1393    }
1394
1395    fn lookup_chunks(&self, chunk_indices: &[usize]) -> Vec<LoadedChunk> {
1396        let page_meta = self.page_meta.as_ref().unwrap();
1397        chunk_indices
1398            .iter()
1399            .map(|&chunk_idx| {
1400                let chunk_meta = &page_meta.chunk_meta[chunk_idx];
1401                let bytes_start = chunk_meta.offset_bytes;
1402                let bytes_end = bytes_start + chunk_meta.chunk_size_bytes;
1403                LoadedChunk {
1404                    byte_range: bytes_start..bytes_end,
1405                    items_in_chunk: chunk_meta.num_values,
1406                    chunk_idx,
1407                    data: LanceBuffer::empty(),
1408                }
1409            })
1410            .collect()
1411    }
1412}
1413
1414#[derive(Debug, PartialEq, Eq, Clone, Copy)]
1415enum PreambleAction {
1416    Take,
1417    Skip,
1418    Absent,
1419}
1420
1421// When we schedule a chunk we use the repetition index (or, if none exists, just the # of items
1422// in each chunk) to map a user requested range into a set of ChunkInstruction objects which tell
1423// us how exactly to read from the chunk.
1424//
1425// Examples:
1426//
1427// | Chunk 0     | Chunk 1   | Chunk 2   | Chunk 3 |
1428// | xxxxyyyyzzz | zzzzzzzzz | zzzzzzzzz | aaabbcc |
1429//
1430// Full read (0..6)
1431//
1432// Chunk 0: (several rows, ends with trailer)
1433//   preamble: absent
1434//   rows_to_skip: 0
1435//   rows_to_take: 3 (x, y, z)
1436//   take_trailer: true
1437//
1438// Chunk 1: (all preamble, ends with trailer)
1439//   preamble: take
1440//   rows_to_skip: 0
1441//   rows_to_take: 0
1442//   take_trailer: true
1443//
1444// Chunk 2: (all preamble, no trailer)
1445//   preamble: take
1446//   rows_to_skip: 0
1447//   rows_to_take: 0
1448//   take_trailer: false
1449//
1450// Chunk 3: (several rows, no trailer or preamble)
1451//   preamble: absent
1452//   rows_to_skip: 0
1453//   rows_to_take: 3 (a, b, c)
1454//   take_trailer: false
1455#[derive(Clone, Debug, PartialEq, Eq)]
1456struct ChunkInstructions {
1457    // The index of the chunk to read
1458    chunk_idx: usize,
1459    // A "preamble" is when a chunk begins with a continuation of the previous chunk's list.  If there
1460    // is no repetition index there is never a preamble.
1461    //
1462    // It's possible for a chunk to be entirely premable.  For example, if there is a really large list
1463    // that spans several chunks.
1464    preamble: PreambleAction,
1465    // How many complete rows (not including the preamble or trailer) to skip
1466    //
1467    // If this is non-zero then premable must not be Take
1468    rows_to_skip: u64,
1469    // How many rows to take.  If a row splits across chunks then we will count the row in the first
1470    // chunk that contains the row.
1471    rows_to_take: u64,
1472    // A "trailer" is when a chunk ends with a partial list.  If there is no repetition index there is
1473    // never a trailer.
1474    //
1475    // A chunk that is all preamble may or may not have a trailer.
1476    //
1477    // If this is true then we want to include the trailer
1478    take_trailer: bool,
1479}
1480
1481// First, we schedule a bunch of [`ChunkInstructions`] based on the users ranges.  Then we
1482// start decoding them, based on a batch size, which might not align with what we scheduled.
1483//
1484// This results in `ChunkDrainInstructions` which targets a contiguous slice of a `ChunkInstructions`
1485//
1486// So if `ChunkInstructions` is "skip preamble, skip 10, take 50, take trailer" and we are decoding in
1487// batches of size 10 we might have a `ChunkDrainInstructions` that targets that chunk and has its own
1488// skip of 17 and take of 10.  This would mean we decode the chunk, skip the preamble and 27 rows, and
1489// then take 10 rows.
1490//
1491// One very confusing bit is that `rows_to_take` includes the trailer.  So if we have two chunks:
1492//  -no preamble, skip 5, take 10, take trailer
1493//  -take preamble, skip 0, take 50, no trailer
1494//
1495// and we are draining 20 rows then the drain instructions for the first batch will be:
1496//  - no preamble, skip 0 (from chunk 0), take 11 (from chunk 0)
1497//  - take preamble (from chunk 1), skip 0 (from chunk 1), take 9 (from chunk 1)
1498#[derive(Debug, PartialEq, Eq)]
1499struct ChunkDrainInstructions {
1500    chunk_instructions: ChunkInstructions,
1501    rows_to_skip: u64,
1502    rows_to_take: u64,
1503    preamble_action: PreambleAction,
1504}
1505
1506impl ChunkInstructions {
1507    // Given a repetition index and a set of user ranges we need to figure out how to read from the chunks
1508    //
1509    // We assume that `user_ranges` are in sorted order and non-overlapping
1510    //
1511    // The output will be a set of `ChunkInstructions` which tell us how to read from the chunks
1512    fn schedule_instructions(
1513        rep_index: &MiniBlockRepIndex,
1514        user_ranges: &[Range<u64>],
1515    ) -> Vec<Self> {
1516        // This is an in-exact capacity guess but pretty good.  The actual capacity can be
1517        // smaller if instructions are merged.  It can be larger if there are multiple instructions
1518        // per row which can happen with lists.
1519        let mut chunk_instructions = Vec::with_capacity(user_ranges.len());
1520
1521        for user_range in user_ranges {
1522            let mut rows_needed = user_range.end - user_range.start;
1523            let mut need_preamble = false;
1524
1525            // Need to find the first chunk with a first row >= user_range.start.  If there are
1526            // multiple chunks with the same first row we need to take the first one.
1527            let mut block_index = match rep_index
1528                .blocks
1529                .binary_search_by_key(&user_range.start, |block| block.first_row)
1530            {
1531                Ok(idx) => {
1532                    // Slightly tricky case, we may need to walk backwards a bit to make sure we
1533                    // are grabbing first eligible chunk
1534                    let mut idx = idx;
1535                    while idx > 0 && rep_index.blocks[idx - 1].first_row == user_range.start {
1536                        idx -= 1;
1537                    }
1538                    idx
1539                }
1540                // Easy case.  idx is greater, and idx - 1 is smaller, so idx - 1 contains the start
1541                Err(idx) => idx - 1,
1542            };
1543
1544            let mut to_skip = user_range.start - rep_index.blocks[block_index].first_row;
1545
1546            while rows_needed > 0 || need_preamble {
1547                // Check if we've gone past the last block (should not happen)
1548                if block_index >= rep_index.blocks.len() {
1549                    log::warn!(
1550                        "schedule_instructions inconsistency: block_index >= rep_index.blocks.len(), exiting early"
1551                    );
1552                    break;
1553                }
1554
1555                let chunk = &rep_index.blocks[block_index];
1556                let rows_avail = chunk.starts_including_trailer.saturating_sub(to_skip);
1557
1558                // Handle blocks that are entirely preamble (rows_avail = 0)
1559                // These blocks have no rows to take but may have a preamble we need
1560                // We only look for preamble if to_skip == 0 (we're not skipping rows)
1561                if rows_avail == 0 && to_skip == 0 {
1562                    // Only process if this chunk has a preamble we need
1563                    if chunk.has_preamble && need_preamble {
1564                        chunk_instructions.push(Self {
1565                            chunk_idx: block_index,
1566                            preamble: PreambleAction::Take,
1567                            rows_to_skip: 0,
1568                            rows_to_take: 0,
1569                            // We still need to look at has_trailer to distinguish between "all preamble
1570                            // and row ends at end of chunk" and "all preamble and row bleeds into next
1571                            // chunk".  Both cases will have 0 rows available.
1572                            take_trailer: chunk.has_trailer,
1573                        });
1574                        // Only set need_preamble = false if the chunk has at least one row,
1575                        // Or we are reaching the last block,
1576                        // Otherwise, the chunk is entirely preamble and we need the next chunk's preamble too
1577                        if chunk.starts_including_trailer > 0
1578                            || block_index == rep_index.blocks.len() - 1
1579                        {
1580                            need_preamble = false;
1581                        }
1582                    }
1583                    // Move to next block
1584                    block_index += 1;
1585                    continue;
1586                }
1587
1588                // Edge case: if rows_avail == 0 but to_skip > 0
1589                // This theoretically shouldn't happen (binary search should avoid it)
1590                // but handle it for safety
1591                if rows_avail == 0 && to_skip > 0 {
1592                    // This block doesn't have enough rows to skip, move to next block
1593                    // Adjust to_skip by the number of rows in this block
1594                    to_skip -= chunk.starts_including_trailer;
1595                    block_index += 1;
1596                    continue;
1597                }
1598
1599                let rows_to_take = rows_avail.min(rows_needed);
1600                rows_needed -= rows_to_take;
1601
1602                let mut take_trailer = false;
1603                let preamble = if chunk.has_preamble {
1604                    if need_preamble {
1605                        PreambleAction::Take
1606                    } else {
1607                        PreambleAction::Skip
1608                    }
1609                } else {
1610                    PreambleAction::Absent
1611                };
1612
1613                // Are we taking the trailer?  If so, make sure we mark that we need the preamble
1614                if rows_to_take == rows_avail && chunk.has_trailer {
1615                    take_trailer = true;
1616                    need_preamble = true;
1617                } else {
1618                    need_preamble = false;
1619                };
1620
1621                chunk_instructions.push(Self {
1622                    preamble,
1623                    chunk_idx: block_index,
1624                    rows_to_skip: to_skip,
1625                    rows_to_take,
1626                    take_trailer,
1627                });
1628
1629                to_skip = 0;
1630                block_index += 1;
1631            }
1632        }
1633
1634        // If there were multiple ranges we may have multiple instructions for a single chunk.  Merge them now if they
1635        // are _adjacent_ (i.e. don't merge "take first row of chunk 0" and "take third row of chunk 0" into "take 2
1636        // rows of chunk 0 starting at 0")
1637        if user_ranges.len() > 1 {
1638            // TODO: Could probably optimize this allocation away
1639            let mut merged_instructions = Vec::with_capacity(chunk_instructions.len());
1640            let mut instructions_iter = chunk_instructions.into_iter();
1641            merged_instructions.push(instructions_iter.next().unwrap());
1642            for instruction in instructions_iter {
1643                let last = merged_instructions.last_mut().unwrap();
1644                if last.chunk_idx == instruction.chunk_idx
1645                    && last.rows_to_take + last.rows_to_skip == instruction.rows_to_skip
1646                {
1647                    last.rows_to_take += instruction.rows_to_take;
1648                    last.take_trailer |= instruction.take_trailer;
1649                } else {
1650                    merged_instructions.push(instruction);
1651                }
1652            }
1653            merged_instructions
1654        } else {
1655            chunk_instructions
1656        }
1657    }
1658
1659    fn drain_from_instruction(
1660        &self,
1661        rows_desired: &mut u64,
1662        need_preamble: &mut bool,
1663        skip_in_chunk: &mut u64,
1664    ) -> (ChunkDrainInstructions, bool) {
1665        // If we need the premable then we shouldn't be skipping anything
1666        debug_assert!(!*need_preamble || *skip_in_chunk == 0);
1667        let rows_avail = self.rows_to_take - *skip_in_chunk;
1668        let has_preamble = self.preamble != PreambleAction::Absent;
1669        let preamble_action = match (*need_preamble, has_preamble) {
1670            (true, true) => PreambleAction::Take,
1671            (true, false) => panic!("Need preamble but there isn't one"),
1672            (false, true) => PreambleAction::Skip,
1673            (false, false) => PreambleAction::Absent,
1674        };
1675
1676        // How many rows are we actually taking in this take step (including the preamble
1677        // and trailer both as individual rows)
1678        let rows_taking = if *rows_desired >= rows_avail {
1679            // We want all the rows.  If there is a trailer we are grabbing it and will need
1680            // the preamble of the next chunk
1681            // If there is a trailer and we are taking all the rows then we need the preamble
1682            // of the next chunk.
1683            //
1684            // Also, if this chunk is entirely preamble (rows_avail == 0 && !take_trailer) then we
1685            // need the preamble of the next chunk.
1686            *need_preamble = self.take_trailer;
1687            rows_avail
1688        } else {
1689            // We aren't taking all the rows.  Even if there is a trailer we aren't taking
1690            // it so we will not need the preamble
1691            *need_preamble = false;
1692            *rows_desired
1693        };
1694        let rows_skipped = *skip_in_chunk;
1695
1696        // Update the state for the next iteration
1697        let consumed_chunk = if *rows_desired >= rows_avail {
1698            *rows_desired -= rows_avail;
1699            *skip_in_chunk = 0;
1700            true
1701        } else {
1702            *skip_in_chunk += *rows_desired;
1703            *rows_desired = 0;
1704            false
1705        };
1706
1707        (
1708            ChunkDrainInstructions {
1709                chunk_instructions: self.clone(),
1710                rows_to_skip: rows_skipped,
1711                rows_to_take: rows_taking,
1712                preamble_action,
1713            },
1714            consumed_chunk,
1715        )
1716    }
1717}
1718
1719enum Words {
1720    U16(ScalarBuffer<u16>),
1721    U32(ScalarBuffer<u32>),
1722}
1723
1724struct WordsIter<'a> {
1725    iter: Box<dyn Iterator<Item = u32> + 'a>,
1726}
1727
1728impl Words {
1729    pub fn len(&self) -> usize {
1730        match self {
1731            Self::U16(b) => b.len(),
1732            Self::U32(b) => b.len(),
1733        }
1734    }
1735
1736    pub fn iter(&self) -> WordsIter<'_> {
1737        match self {
1738            Self::U16(buf) => WordsIter {
1739                iter: Box::new(buf.iter().map(|&x| x as u32)),
1740            },
1741            Self::U32(buf) => WordsIter {
1742                iter: Box::new(buf.iter().copied()),
1743            },
1744        }
1745    }
1746
1747    pub fn from_bytes(bytes: Bytes, has_large_chunk: bool) -> Result<Self> {
1748        let bytes_per_value = if has_large_chunk { 4 } else { 2 };
1749        assert_eq!(bytes.len() % bytes_per_value, 0);
1750        let buffer = LanceBuffer::from_bytes(bytes, bytes_per_value as u64);
1751        if has_large_chunk {
1752            Ok(Self::U32(buffer.borrow_to_typed_slice::<u32>()))
1753        } else {
1754            Ok(Self::U16(buffer.borrow_to_typed_slice::<u16>()))
1755        }
1756    }
1757}
1758
1759impl<'a> Iterator for WordsIter<'a> {
1760    type Item = u32;
1761
1762    fn next(&mut self) -> Option<Self::Item> {
1763        self.iter.next()
1764    }
1765}
1766
1767impl StructuralPageScheduler for MiniBlockScheduler {
1768    fn initialize<'a>(
1769        &'a mut self,
1770        io: &Arc<dyn EncodingsIo>,
1771    ) -> BoxFuture<'a, Result<Arc<dyn CachedPageData>>> {
1772        // We always need to fetch chunk metadata.  We may also need to fetch a dictionary and
1773        // we may also need to fetch the repetition index.  Here, we gather what buffers we
1774        // need.
1775        let (meta_buf_position, meta_buf_size) = self.buffer_offsets_and_sizes[0];
1776        let value_buf_position = self.buffer_offsets_and_sizes[1].0;
1777        let mut bufs_needed = 1;
1778        if self.dictionary.is_some() {
1779            bufs_needed += 1;
1780        }
1781        if self.repetition_index_depth > 0 {
1782            bufs_needed += 1;
1783        }
1784        let mut required_ranges = Vec::with_capacity(bufs_needed);
1785        required_ranges.push(meta_buf_position..meta_buf_position + meta_buf_size);
1786        if let Some(ref dictionary) = self.dictionary {
1787            required_ranges.push(
1788                dictionary.dictionary_buf_position_and_size.0
1789                    ..dictionary.dictionary_buf_position_and_size.0
1790                        + dictionary.dictionary_buf_position_and_size.1,
1791            );
1792        }
1793        if self.repetition_index_depth > 0 {
1794            let (rep_index_pos, rep_index_size) = self.buffer_offsets_and_sizes.last().unwrap();
1795            required_ranges.push(*rep_index_pos..*rep_index_pos + *rep_index_size);
1796        }
1797        let io_req = io.submit_request(required_ranges, 0);
1798
1799        async move {
1800            let mut buffers = io_req.await?.into_iter().fuse();
1801            let meta_bytes = buffers.next().unwrap();
1802            let dictionary_bytes = self.dictionary.as_ref().and_then(|_| buffers.next());
1803            let rep_index_bytes = buffers.next();
1804
1805            // Parse the metadata and build the chunk meta
1806            let words = Words::from_bytes(meta_bytes, self.has_large_chunk)?;
1807            let mut chunk_meta = Vec::with_capacity(words.len());
1808
1809            let mut rows_counter = 0;
1810            let mut offset_bytes = value_buf_position;
1811            for (word_idx, word) in words.iter().enumerate() {
1812                let log_num_values = word & 0x0F;
1813                let divided_bytes = word >> 4;
1814                let num_bytes = (divided_bytes as usize + 1) * MINIBLOCK_ALIGNMENT;
1815                debug_assert!(num_bytes > 0);
1816                let num_values = if word_idx < words.len() - 1 {
1817                    debug_assert!(log_num_values > 0);
1818                    1 << log_num_values
1819                } else {
1820                    debug_assert!(
1821                        log_num_values == 0
1822                            || (1 << log_num_values) == (self.items_in_page - rows_counter)
1823                    );
1824                    self.items_in_page - rows_counter
1825                };
1826                rows_counter += num_values;
1827
1828                chunk_meta.push(ChunkMeta {
1829                    num_values,
1830                    chunk_size_bytes: num_bytes as u64,
1831                    offset_bytes,
1832                });
1833                offset_bytes += num_bytes as u64;
1834            }
1835
1836            // Build the repetition index
1837            let rep_index = if let Some(rep_index_data) = rep_index_bytes {
1838                assert!(rep_index_data.len() % 8 == 0);
1839                let stride = self.repetition_index_depth as usize + 1;
1840                MiniBlockRepIndex::decode_from_bytes(&rep_index_data, stride)
1841            } else {
1842                MiniBlockRepIndex::default_from_chunks(&chunk_meta)
1843            };
1844
1845            let mut page_meta = MiniBlockCacheableState {
1846                chunk_meta,
1847                rep_index,
1848                dictionary: None,
1849            };
1850
1851            // decode dictionary
1852            if let Some(ref mut dictionary) = self.dictionary {
1853                let dictionary_data = dictionary_bytes.unwrap();
1854                page_meta.dictionary =
1855                    Some(Arc::new(dictionary.dictionary_decompressor.decompress(
1856                        LanceBuffer::from_bytes(
1857                            dictionary_data,
1858                            dictionary.dictionary_data_alignment,
1859                        ),
1860                        dictionary.num_dictionary_items,
1861                    )?));
1862            };
1863            let page_meta = Arc::new(page_meta);
1864            self.page_meta = Some(page_meta.clone());
1865            Ok(page_meta as Arc<dyn CachedPageData>)
1866        }
1867        .boxed()
1868    }
1869
1870    fn load(&mut self, data: &Arc<dyn CachedPageData>) {
1871        self.page_meta = Some(
1872            data.clone()
1873                .as_arc_any()
1874                .downcast::<MiniBlockCacheableState>()
1875                .unwrap(),
1876        );
1877    }
1878
1879    fn schedule_ranges(
1880        &self,
1881        ranges: &[Range<u64>],
1882        io: &Arc<dyn EncodingsIo>,
1883    ) -> Result<Vec<PageLoadTask>> {
1884        let num_rows = ranges.iter().map(|r| r.end - r.start).sum();
1885
1886        let page_meta = self.page_meta.as_ref().unwrap();
1887
1888        let chunk_instructions =
1889            ChunkInstructions::schedule_instructions(&page_meta.rep_index, ranges);
1890
1891        debug_assert_eq!(
1892            num_rows,
1893            chunk_instructions
1894                .iter()
1895                .map(|ci| ci.rows_to_take)
1896                .sum::<u64>()
1897        );
1898
1899        let chunks_needed = chunk_instructions
1900            .iter()
1901            .map(|ci| ci.chunk_idx)
1902            .unique()
1903            .collect::<Vec<_>>();
1904
1905        let mut loaded_chunks = self.lookup_chunks(&chunks_needed);
1906        let chunk_ranges = loaded_chunks
1907            .iter()
1908            .map(|c| c.byte_range.clone())
1909            .collect::<Vec<_>>();
1910        let loaded_chunk_data = io.submit_request(chunk_ranges, self.priority);
1911
1912        let rep_decompressor = self.rep_decompressor.clone();
1913        let def_decompressor = self.def_decompressor.clone();
1914        let value_decompressor = self.value_decompressor.clone();
1915        let num_buffers = self.num_buffers;
1916        let has_large_chunk = self.has_large_chunk;
1917        let dictionary = page_meta
1918            .dictionary
1919            .as_ref()
1920            .map(|dictionary| dictionary.clone());
1921        let def_meaning = self.def_meaning.clone();
1922
1923        let res = async move {
1924            let loaded_chunk_data = loaded_chunk_data.await?;
1925            for (loaded_chunk, chunk_data) in loaded_chunks.iter_mut().zip(loaded_chunk_data) {
1926                loaded_chunk.data = LanceBuffer::from_bytes(chunk_data, 1);
1927            }
1928
1929            Ok(Box::new(MiniBlockDecoder {
1930                rep_decompressor,
1931                def_decompressor,
1932                value_decompressor,
1933                def_meaning,
1934                loaded_chunks: VecDeque::from_iter(loaded_chunks),
1935                instructions: VecDeque::from(chunk_instructions),
1936                offset_in_current_chunk: 0,
1937                dictionary,
1938                num_rows,
1939                num_buffers,
1940                has_large_chunk,
1941            }) as Box<dyn StructuralPageDecoder>)
1942        }
1943        .boxed();
1944        let page_load_task = PageLoadTask {
1945            decoder_fut: res,
1946            num_rows,
1947        };
1948        Ok(vec![page_load_task])
1949    }
1950}
1951
1952#[derive(Debug, Clone, Copy)]
1953struct FullZipRepIndexDetails {
1954    buf_position: u64,
1955    bytes_per_value: u64, // Will be 1, 2, 4, or 8
1956}
1957
1958#[derive(Debug)]
1959enum PerValueDecompressor {
1960    Fixed(Arc<dyn FixedPerValueDecompressor>),
1961    Variable(Arc<dyn VariablePerValueDecompressor>),
1962}
1963
1964#[derive(Debug)]
1965struct FullZipDecodeDetails {
1966    value_decompressor: PerValueDecompressor,
1967    def_meaning: Arc<[DefinitionInterpretation]>,
1968    ctrl_word_parser: ControlWordParser,
1969    max_rep: u16,
1970    max_visible_def: u16,
1971}
1972
1973/// Describes where FullZip byte ranges should be read from.
1974///
1975/// FullZip decoding always needs a list of byte ranges, but those bytes can come
1976/// from two different places:
1977/// - Remote I/O (normal path): ranges are fetched from the underlying `EncodingsIo`.
1978/// - A prefetched full page (full scan fast path): the entire page has already been
1979///   loaded once and ranges should be sliced from memory.
1980///
1981/// This abstraction keeps scheduling code focused on "which ranges are needed"
1982/// instead of "how bytes are fetched", and it lets full-page scans avoid the
1983/// two-stage rep-index -> data I/O pipeline.
1984#[derive(Debug, Clone)]
1985enum FullZipReadSource {
1986    /// Fetch ranges from the storage backend through the encoding I/O interface.
1987    Remote(Arc<dyn EncodingsIo>),
1988    /// Slice ranges from an already-loaded FullZip page buffer.
1989    PrefetchedPage { base_offset: u64, data: LanceBuffer },
1990}
1991
1992impl FullZipReadSource {
1993    /// Materialize the requested ranges as decode-ready `LanceBuffer`s.
1994    ///
1995    /// The returned buffers preserve the input range order.
1996    fn fetch(
1997        &self,
1998        ranges: &[Range<u64>],
1999        priority: u64,
2000    ) -> BoxFuture<'static, Result<VecDeque<LanceBuffer>>> {
2001        match self {
2002            Self::Remote(io) => {
2003                let io = io.clone();
2004                let ranges = ranges.to_vec();
2005                async move {
2006                    let data = io.submit_request(ranges, priority).await?;
2007                    Ok(data
2008                        .into_iter()
2009                        .map(|bytes| LanceBuffer::from_bytes(bytes, 1))
2010                        .collect::<VecDeque<_>>())
2011                }
2012                .boxed()
2013            }
2014            Self::PrefetchedPage { base_offset, data } => {
2015                let base_offset = *base_offset;
2016                let data = data.clone();
2017                let page_end = base_offset + data.len() as u64;
2018                std::future::ready(
2019                    ranges
2020                        .iter()
2021                        .map(|range| {
2022                            if range.start > range.end
2023                                || range.start < base_offset
2024                                || range.end > page_end
2025                            {
2026                                return Err(Error::internal(format!(
2027                                    "Requested range {:?} is outside page range {}..{}",
2028                                    range, base_offset, page_end
2029                                )));
2030                            }
2031                            let start = (range.start - base_offset) as usize;
2032                            let len = (range.end - range.start) as usize;
2033                            Ok(data.slice_with_length(start, len))
2034                        })
2035                        .collect::<Result<VecDeque<_>>>(),
2036                )
2037                .boxed()
2038            }
2039        }
2040    }
2041}
2042
2043/// A scheduler for full-zip encoded data
2044///
2045/// When the data type has a fixed-width then we simply need to map from
2046/// row ranges to byte ranges using the fixed-width of the data type.
2047///
2048/// When the data type is variable-width or has any repetition then a
2049/// repetition index is required.
2050#[derive(Debug)]
2051pub struct FullZipScheduler {
2052    data_buf_position: u64,
2053    data_buf_size: u64,
2054    rep_index: Option<FullZipRepIndexDetails>,
2055    priority: u64,
2056    rows_in_page: u64,
2057    bits_per_offset: u8,
2058    details: Arc<FullZipDecodeDetails>,
2059    /// Cached state containing the decoded repetition index
2060    cached_state: Option<Arc<FullZipCacheableState>>,
2061    /// Whether repetition index metadata should be cached during initialize.
2062    enable_cache: bool,
2063}
2064
2065impl FullZipScheduler {
2066    fn try_new(
2067        buffer_offsets_and_sizes: &[(u64, u64)],
2068        priority: u64,
2069        rows_in_page: u64,
2070        layout: &pb21::FullZipLayout,
2071        decompressors: &dyn DecompressionStrategy,
2072    ) -> Result<Self> {
2073        let (data_buf_position, data_buf_size) = buffer_offsets_and_sizes[0];
2074        let rep_index = buffer_offsets_and_sizes.get(1).map(|(pos, len)| {
2075            let num_reps = rows_in_page + 1;
2076            let bytes_per_rep = len / num_reps;
2077            debug_assert_eq!(len % num_reps, 0);
2078            debug_assert!(
2079                bytes_per_rep == 1
2080                    || bytes_per_rep == 2
2081                    || bytes_per_rep == 4
2082                    || bytes_per_rep == 8
2083            );
2084            FullZipRepIndexDetails {
2085                buf_position: *pos,
2086                bytes_per_value: bytes_per_rep,
2087            }
2088        });
2089
2090        let value_decompressor = match layout.details {
2091            Some(pb21::full_zip_layout::Details::BitsPerValue(_)) => {
2092                let decompressor = decompressors.create_fixed_per_value_decompressor(
2093                    layout.value_compression.as_ref().unwrap(),
2094                )?;
2095                PerValueDecompressor::Fixed(decompressor.into())
2096            }
2097            Some(pb21::full_zip_layout::Details::BitsPerOffset(_)) => {
2098                let decompressor = decompressors.create_variable_per_value_decompressor(
2099                    layout.value_compression.as_ref().unwrap(),
2100                )?;
2101                PerValueDecompressor::Variable(decompressor.into())
2102            }
2103            None => {
2104                panic!("Full-zip layout must have a `details` field");
2105            }
2106        };
2107        let ctrl_word_parser = ControlWordParser::new(
2108            layout.bits_rep.try_into().unwrap(),
2109            layout.bits_def.try_into().unwrap(),
2110        );
2111        let def_meaning = layout
2112            .layers
2113            .iter()
2114            .map(|l| ProtobufUtils21::repdef_layer_to_def_interp(*l))
2115            .collect::<Vec<_>>();
2116
2117        let max_rep = def_meaning.iter().filter(|d| d.is_list()).count() as u16;
2118        let max_visible_def = def_meaning
2119            .iter()
2120            .filter(|d| !d.is_list())
2121            .map(|d| d.num_def_levels())
2122            .sum();
2123
2124        let bits_per_offset = match layout.details {
2125            Some(pb21::full_zip_layout::Details::BitsPerValue(_)) => 32,
2126            Some(pb21::full_zip_layout::Details::BitsPerOffset(bits_per_offset)) => {
2127                bits_per_offset as u8
2128            }
2129            None => panic!("Full-zip layout must have a `details` field"),
2130        };
2131
2132        let details = Arc::new(FullZipDecodeDetails {
2133            value_decompressor,
2134            def_meaning: def_meaning.into(),
2135            ctrl_word_parser,
2136            max_rep,
2137            max_visible_def,
2138        });
2139        Ok(Self {
2140            data_buf_position,
2141            data_buf_size,
2142            rep_index,
2143            details,
2144            priority,
2145            rows_in_page,
2146            bits_per_offset,
2147            cached_state: None,
2148            enable_cache: false,
2149        })
2150    }
2151
2152    fn covers_entire_page(ranges: &[Range<u64>], rows_in_page: u64) -> bool {
2153        if ranges.is_empty() {
2154            return false;
2155        }
2156        let mut expected_start = 0;
2157        for range in ranges {
2158            if range.start != expected_start || range.end > rows_in_page || range.end < range.start
2159            {
2160                return false;
2161            }
2162            expected_start = range.end;
2163        }
2164        expected_start == rows_in_page
2165    }
2166
2167    fn create_page_load_task(
2168        read_source: FullZipReadSource,
2169        byte_ranges: Vec<Range<u64>>,
2170        priority: u64,
2171        num_rows: u64,
2172        details: Arc<FullZipDecodeDetails>,
2173        bits_per_offset: u8,
2174    ) -> PageLoadTask {
2175        let load_task = async move {
2176            let data = read_source.fetch(&byte_ranges, priority).await?;
2177            Self::create_decoder(details, data, num_rows, bits_per_offset)
2178        }
2179        .boxed();
2180        PageLoadTask {
2181            decoder_fut: load_task,
2182            num_rows,
2183        }
2184    }
2185
2186    /// Creates a decoder from the loaded data
2187    fn create_decoder(
2188        details: Arc<FullZipDecodeDetails>,
2189        data: VecDeque<LanceBuffer>,
2190        num_rows: u64,
2191        bits_per_offset: u8,
2192    ) -> Result<Box<dyn StructuralPageDecoder>> {
2193        match &details.value_decompressor {
2194            PerValueDecompressor::Fixed(decompressor) => {
2195                let bits_per_value = decompressor.bits_per_value();
2196                if bits_per_value % 8 != 0 {
2197                    return Err(lance_core::Error::not_supported_source("Bit-packed full-zip encoding (non-byte-aligned values) is not yet implemented".into()));
2198                }
2199                let bytes_per_value = bits_per_value / 8;
2200                let total_bytes_per_value =
2201                    bytes_per_value as usize + details.ctrl_word_parser.bytes_per_word();
2202                if total_bytes_per_value == 0 {
2203                    return Err(lance_core::Error::internal(
2204                        "Invalid encoding: per-row byte width must be greater than 0",
2205                    ));
2206                }
2207                Ok(Box::new(FixedFullZipDecoder {
2208                    details,
2209                    data,
2210                    num_rows,
2211                    offset_in_current: 0,
2212                    bytes_per_value: bytes_per_value as usize,
2213                    total_bytes_per_value,
2214                }) as Box<dyn StructuralPageDecoder>)
2215            }
2216            PerValueDecompressor::Variable(_decompressor) => {
2217                Ok(Box::new(VariableFullZipDecoder::new(
2218                    details,
2219                    data,
2220                    num_rows,
2221                    bits_per_offset,
2222                    bits_per_offset,
2223                )?))
2224            }
2225        }
2226    }
2227
2228    /// Extracts byte ranges from a repetition index buffer
2229    /// The buffer contains pairs of (start, end) values for each range
2230    fn extract_byte_ranges_from_pairs(
2231        buffer: LanceBuffer,
2232        bytes_per_value: u64,
2233        data_buf_position: u64,
2234    ) -> Vec<Range<u64>> {
2235        ByteUnpacker::new(buffer, bytes_per_value as usize)
2236            .chunks(2)
2237            .into_iter()
2238            .map(|mut c| {
2239                let start = c.next().unwrap() + data_buf_position;
2240                let end = c.next().unwrap() + data_buf_position;
2241                start..end
2242            })
2243            .collect::<Vec<_>>()
2244    }
2245
2246    /// Extracts byte ranges from a cached repetition index buffer
2247    /// The buffer contains all values and we need to extract specific ranges
2248    fn extract_byte_ranges_from_cached(
2249        buffer: &LanceBuffer,
2250        ranges: &[Range<u64>],
2251        bytes_per_value: u64,
2252        data_buf_position: u64,
2253    ) -> Vec<Range<u64>> {
2254        ranges
2255            .iter()
2256            .map(|r| {
2257                let start_offset = (r.start * bytes_per_value) as usize;
2258                let end_offset = (r.end * bytes_per_value) as usize;
2259
2260                let start_slice = &buffer[start_offset..start_offset + bytes_per_value as usize];
2261                let start_val =
2262                    ByteUnpacker::new(start_slice.iter().copied(), bytes_per_value as usize)
2263                        .next()
2264                        .unwrap();
2265
2266                let end_slice = &buffer[end_offset..end_offset + bytes_per_value as usize];
2267                let end_val =
2268                    ByteUnpacker::new(end_slice.iter().copied(), bytes_per_value as usize)
2269                        .next()
2270                        .unwrap();
2271
2272                (data_buf_position + start_val)..(data_buf_position + end_val)
2273            })
2274            .collect()
2275    }
2276
2277    /// Computes the ranges in the repetition index that need to be loaded
2278    fn compute_rep_index_ranges(
2279        ranges: &[Range<u64>],
2280        rep_index: &FullZipRepIndexDetails,
2281    ) -> Vec<Range<u64>> {
2282        ranges
2283            .iter()
2284            .flat_map(|r| {
2285                let first_val_start =
2286                    rep_index.buf_position + (r.start * rep_index.bytes_per_value);
2287                let first_val_end = first_val_start + rep_index.bytes_per_value;
2288                let last_val_start = rep_index.buf_position + (r.end * rep_index.bytes_per_value);
2289                let last_val_end = last_val_start + rep_index.bytes_per_value;
2290                [first_val_start..first_val_end, last_val_start..last_val_end]
2291            })
2292            .collect()
2293    }
2294
2295    /// Schedules ranges in the presence of a repetition index
2296    fn schedule_ranges_rep(
2297        &self,
2298        ranges: &[Range<u64>],
2299        io: &Arc<dyn EncodingsIo>,
2300        rep_index: FullZipRepIndexDetails,
2301    ) -> Result<Vec<PageLoadTask>> {
2302        let num_rows = ranges.iter().map(|r| r.end - r.start).sum();
2303        let data_buf_position = self.data_buf_position;
2304        let priority = self.priority;
2305        let details = self.details.clone();
2306        let bits_per_offset = self.bits_per_offset;
2307
2308        if Self::covers_entire_page(ranges, self.rows_in_page) {
2309            let full_range = self.data_buf_position..(self.data_buf_position + self.data_buf_size);
2310            let page_data = io.submit_single(full_range.clone(), priority);
2311            let load_task = async move {
2312                let page_data = page_data.await?;
2313                let source = FullZipReadSource::PrefetchedPage {
2314                    base_offset: full_range.start,
2315                    data: LanceBuffer::from_bytes(page_data, 1),
2316                };
2317                let read_ranges = vec![full_range];
2318                let data = source.fetch(&read_ranges, priority).await?;
2319                Self::create_decoder(details, data, num_rows, bits_per_offset)
2320            }
2321            .boxed();
2322            let page_load_task = PageLoadTask {
2323                decoder_fut: load_task,
2324                num_rows,
2325            };
2326            return Ok(vec![page_load_task]);
2327        }
2328
2329        if let Some(cached_state) = &self.cached_state {
2330            let byte_ranges = Self::extract_byte_ranges_from_cached(
2331                &cached_state.rep_index_buffer,
2332                ranges,
2333                rep_index.bytes_per_value,
2334                data_buf_position,
2335            );
2336            let page_load_task = Self::create_page_load_task(
2337                FullZipReadSource::Remote(io.clone()),
2338                byte_ranges,
2339                priority,
2340                num_rows,
2341                details,
2342                bits_per_offset,
2343            );
2344            return Ok(vec![page_load_task]);
2345        }
2346
2347        let rep_ranges = Self::compute_rep_index_ranges(ranges, &rep_index);
2348        let rep_data = io.submit_request(rep_ranges, priority);
2349        let io_clone = io.clone();
2350        let load_task = async move {
2351            let rep_data = rep_data.await?;
2352            let rep_buffer = LanceBuffer::concat(
2353                &rep_data
2354                    .into_iter()
2355                    .map(|d| LanceBuffer::from_bytes(d, 1))
2356                    .collect::<Vec<_>>(),
2357            );
2358            let byte_ranges = Self::extract_byte_ranges_from_pairs(
2359                rep_buffer,
2360                rep_index.bytes_per_value,
2361                data_buf_position,
2362            );
2363            let source = FullZipReadSource::Remote(io_clone);
2364            let data = source.fetch(&byte_ranges, priority).await?;
2365            Self::create_decoder(details, data, num_rows, bits_per_offset)
2366        }
2367        .boxed();
2368        let page_load_task = PageLoadTask {
2369            decoder_fut: load_task,
2370            num_rows,
2371        };
2372        Ok(vec![page_load_task])
2373    }
2374
2375    // In the simple case there is no repetition and we just have large fixed-width
2376    // rows of data.  We can just map row ranges to byte ranges directly using the
2377    // fixed-width of the data type.
2378    fn schedule_ranges_simple(
2379        &self,
2380        ranges: &[Range<u64>],
2381        io: &Arc<dyn EncodingsIo>,
2382    ) -> Result<Vec<PageLoadTask>> {
2383        // Convert row ranges to item ranges (i.e. multiply by items per row)
2384        let num_rows = ranges.iter().map(|r| r.end - r.start).sum();
2385
2386        let PerValueDecompressor::Fixed(decompressor) = &self.details.value_decompressor else {
2387            unreachable!()
2388        };
2389
2390        // Convert item ranges to byte ranges (i.e. multiply by bytes per item)
2391        let bits_per_value = decompressor.bits_per_value();
2392        assert_eq!(bits_per_value % 8, 0);
2393        let bytes_per_value = bits_per_value / 8;
2394        let bytes_per_cw = self.details.ctrl_word_parser.bytes_per_word();
2395        let total_bytes_per_value = bytes_per_value + bytes_per_cw as u64;
2396        let byte_ranges = ranges
2397            .iter()
2398            .map(|r| {
2399                debug_assert!(r.end <= self.rows_in_page);
2400                let start = self.data_buf_position + r.start * total_bytes_per_value;
2401                let end = self.data_buf_position + r.end * total_bytes_per_value;
2402                start..end
2403            })
2404            .collect::<Vec<_>>();
2405
2406        let page_load_task = Self::create_page_load_task(
2407            FullZipReadSource::Remote(io.clone()),
2408            byte_ranges,
2409            self.priority,
2410            num_rows,
2411            self.details.clone(),
2412            self.bits_per_offset,
2413        );
2414        Ok(vec![page_load_task])
2415    }
2416}
2417
2418/// Cacheable state for FullZip encoding, storing the decoded repetition index
2419#[derive(Debug)]
2420struct FullZipCacheableState {
2421    /// The raw repetition index buffer for future decoding
2422    rep_index_buffer: LanceBuffer,
2423}
2424
2425impl DeepSizeOf for FullZipCacheableState {
2426    fn deep_size_of_children(&self, _context: &mut Context) -> usize {
2427        self.rep_index_buffer.len()
2428    }
2429}
2430
2431impl CachedPageData for FullZipCacheableState {
2432    fn as_arc_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync + 'static> {
2433        self
2434    }
2435}
2436
2437impl StructuralPageScheduler for FullZipScheduler {
2438    fn initialize<'a>(
2439        &'a mut self,
2440        io: &Arc<dyn EncodingsIo>,
2441    ) -> BoxFuture<'a, Result<Arc<dyn CachedPageData>>> {
2442        if self.enable_cache
2443            && let Some(rep_index) = self.rep_index
2444        {
2445            let total_size = (self.rows_in_page + 1) * rep_index.bytes_per_value;
2446            let rep_index_range = rep_index.buf_position..(rep_index.buf_position + total_size);
2447            let io_clone = io.clone();
2448            return async move {
2449                let rep_index_data = io_clone.submit_request(vec![rep_index_range], 0).await?;
2450                let state = Arc::new(FullZipCacheableState {
2451                    rep_index_buffer: LanceBuffer::from_bytes(rep_index_data[0].clone(), 1),
2452                });
2453                self.cached_state = Some(state.clone());
2454                Ok(state as Arc<dyn CachedPageData>)
2455            }
2456            .boxed();
2457        }
2458        std::future::ready(Ok(Arc::new(NoCachedPageData) as Arc<dyn CachedPageData>)).boxed()
2459    }
2460
2461    /// Loads previously cached repetition index data from the cache system.
2462    /// This method is called when a scheduler instance needs to use cached data
2463    /// that was initialized by another instance or in a previous operation.
2464    fn load(&mut self, cache: &Arc<dyn CachedPageData>) {
2465        // Try to downcast to our specific cache type
2466        if let Ok(cached_state) = cache
2467            .clone()
2468            .as_arc_any()
2469            .downcast::<FullZipCacheableState>()
2470        {
2471            // Store the cached state for use in schedule_ranges
2472            self.cached_state = Some(cached_state);
2473        }
2474    }
2475
2476    fn schedule_ranges(
2477        &self,
2478        ranges: &[Range<u64>],
2479        io: &Arc<dyn EncodingsIo>,
2480    ) -> Result<Vec<PageLoadTask>> {
2481        if let Some(rep_index) = self.rep_index {
2482            self.schedule_ranges_rep(ranges, io, rep_index)
2483        } else {
2484            self.schedule_ranges_simple(ranges, io)
2485        }
2486    }
2487}
2488
2489/// A decoder for full-zip encoded data when the data has a fixed-width
2490///
2491/// Here we need to unzip the control words from the values themselves and
2492/// then decompress the requested values.
2493///
2494/// We use a PerValueDecompressor because we will only be decompressing the
2495/// requested data.  This decoder / scheduler does not do any read amplification.
2496#[derive(Debug)]
2497struct FixedFullZipDecoder {
2498    details: Arc<FullZipDecodeDetails>,
2499    data: VecDeque<LanceBuffer>,
2500    offset_in_current: usize,
2501    bytes_per_value: usize,
2502    total_bytes_per_value: usize,
2503    num_rows: u64,
2504}
2505
2506impl FixedFullZipDecoder {
2507    fn slice_next_task(&mut self, num_rows: u64) -> FullZipDecodeTaskItem {
2508        debug_assert!(num_rows > 0);
2509        let cur_buf = self.data.front_mut().unwrap();
2510        let start = self.offset_in_current;
2511        if self.details.ctrl_word_parser.has_rep() {
2512            // This is a slightly slower path.  In order to figure out where to split we need to
2513            // examine the rep index so we can convert num_lists to num_rows
2514            let mut rows_started = 0;
2515            // We always need at least one value.  Now loop through until we have passed num_rows
2516            // values
2517            let mut num_items = 0;
2518            while self.offset_in_current < cur_buf.len() {
2519                let control = self.details.ctrl_word_parser.parse_desc(
2520                    &cur_buf[self.offset_in_current..],
2521                    self.details.max_rep,
2522                    self.details.max_visible_def,
2523                );
2524                if control.is_new_row {
2525                    if rows_started == num_rows {
2526                        break;
2527                    }
2528                    rows_started += 1;
2529                }
2530                num_items += 1;
2531                if control.is_visible {
2532                    self.offset_in_current += self.total_bytes_per_value;
2533                } else {
2534                    self.offset_in_current += self.details.ctrl_word_parser.bytes_per_word();
2535                }
2536            }
2537
2538            let task_slice = cur_buf.slice_with_length(start, self.offset_in_current - start);
2539            if self.offset_in_current == cur_buf.len() {
2540                self.data.pop_front();
2541                self.offset_in_current = 0;
2542            }
2543
2544            FullZipDecodeTaskItem {
2545                data: PerValueDataBlock::Fixed(FixedWidthDataBlock {
2546                    data: task_slice,
2547                    bits_per_value: self.bytes_per_value as u64 * 8,
2548                    num_values: num_items,
2549                    block_info: BlockInfo::new(),
2550                }),
2551                rows_in_buf: rows_started,
2552            }
2553        } else {
2554            // If there's no repetition we can calculate the slicing point by just multiplying
2555            // the number of rows by the total bytes per value
2556            let cur_buf = self.data.front_mut().unwrap();
2557            let bytes_avail = cur_buf.len() - self.offset_in_current;
2558            let offset_in_cur = self.offset_in_current;
2559
2560            let bytes_needed = num_rows as usize * self.total_bytes_per_value;
2561            let mut rows_taken = num_rows;
2562            let task_slice = if bytes_needed >= bytes_avail {
2563                self.offset_in_current = 0;
2564                rows_taken = bytes_avail as u64 / self.total_bytes_per_value as u64;
2565                self.data
2566                    .pop_front()
2567                    .unwrap()
2568                    .slice_with_length(offset_in_cur, bytes_avail)
2569            } else {
2570                self.offset_in_current += bytes_needed;
2571                cur_buf.slice_with_length(offset_in_cur, bytes_needed)
2572            };
2573            FullZipDecodeTaskItem {
2574                data: PerValueDataBlock::Fixed(FixedWidthDataBlock {
2575                    data: task_slice,
2576                    bits_per_value: self.bytes_per_value as u64 * 8,
2577                    num_values: rows_taken,
2578                    block_info: BlockInfo::new(),
2579                }),
2580                rows_in_buf: rows_taken,
2581            }
2582        }
2583    }
2584}
2585
2586impl StructuralPageDecoder for FixedFullZipDecoder {
2587    fn drain(&mut self, num_rows: u64) -> Result<Box<dyn DecodePageTask>> {
2588        let mut task_data = Vec::with_capacity(self.data.len());
2589        let mut remaining = num_rows;
2590        while remaining > 0 {
2591            let task_item = self.slice_next_task(remaining);
2592            remaining -= task_item.rows_in_buf;
2593            task_data.push(task_item);
2594        }
2595        Ok(Box::new(FixedFullZipDecodeTask {
2596            details: self.details.clone(),
2597            data: task_data,
2598            bytes_per_value: self.bytes_per_value,
2599            num_rows: num_rows as usize,
2600        }))
2601    }
2602
2603    fn num_rows(&self) -> u64 {
2604        self.num_rows
2605    }
2606}
2607
2608/// A decoder for full-zip encoded data when the data has a variable-width
2609///
2610/// Here we need to unzip the control words AND lengths from the values and
2611/// then decompress the requested values.
2612#[derive(Debug)]
2613struct VariableFullZipDecoder {
2614    details: Arc<FullZipDecodeDetails>,
2615    decompressor: Arc<dyn VariablePerValueDecompressor>,
2616    data: LanceBuffer,
2617    offsets: LanceBuffer,
2618    rep: ScalarBuffer<u16>,
2619    def: ScalarBuffer<u16>,
2620    repdef_starts: Vec<usize>,
2621    data_starts: Vec<usize>,
2622    offset_starts: Vec<usize>,
2623    visible_item_counts: Vec<u64>,
2624    bits_per_offset: u8,
2625    current_idx: usize,
2626    num_rows: u64,
2627}
2628
2629fn corrupt_file_named(name: &str, message: impl Into<String>) -> Error {
2630    Error::corrupt_file(name.into(), message)
2631}
2632
2633impl VariableFullZipDecoder {
2634    fn new(
2635        details: Arc<FullZipDecodeDetails>,
2636        data: VecDeque<LanceBuffer>,
2637        num_rows: u64,
2638        in_bits_per_length: u8,
2639        out_bits_per_offset: u8,
2640    ) -> Result<Self> {
2641        let decompressor = match details.value_decompressor {
2642            PerValueDecompressor::Variable(ref d) => d.clone(),
2643            _ => unreachable!(),
2644        };
2645
2646        assert_eq!(in_bits_per_length % 8, 0);
2647        assert!(out_bits_per_offset == 32 || out_bits_per_offset == 64);
2648
2649        let mut decoder = Self {
2650            details,
2651            decompressor,
2652            data: LanceBuffer::empty(),
2653            offsets: LanceBuffer::empty(),
2654            rep: LanceBuffer::empty().borrow_to_typed_slice(),
2655            def: LanceBuffer::empty().borrow_to_typed_slice(),
2656            bits_per_offset: out_bits_per_offset,
2657            repdef_starts: Vec::with_capacity(num_rows as usize + 1),
2658            data_starts: Vec::with_capacity(num_rows as usize + 1),
2659            offset_starts: Vec::with_capacity(num_rows as usize + 1),
2660            visible_item_counts: Vec::with_capacity(num_rows as usize + 1),
2661            current_idx: 0,
2662            num_rows,
2663        };
2664
2665        // There's no great time to do this and this is the least worst time.  If we don't unzip then
2666        // we can't slice the data during the decode phase.  This is because we need the offsets to be
2667        // unpacked to know where the values start and end.
2668        //
2669        // We don't want to unzip on the decode thread because that is a single-threaded path
2670        // We don't want to unzip on the scheduling thread because that is a single-threaded path
2671        //
2672        // Fortunately, we know variable length data will always be read indirectly and so we can do it
2673        // here, which should be on the indirect thread.  The primary disadvantage to doing it here is that
2674        // we load all the data into memory and then throw it away only to load it all into memory again during
2675        // the decode.
2676        //
2677        // There are some alternatives to investigate:
2678        //   - Instead of just reading the beginning and end of the rep index we could read the entire
2679        //     range in between.  This will give us the break points that we need for slicing and won't increase
2680        //     the number of IOPs but it will mean we are doing more total I/O and we need to load the rep index
2681        //     even when doing a full scan.
2682        //   - We could force each decode task to do a full unzip of all the data.  Each decode task now
2683        //     has to do more work but the work is all fused.
2684        //   - We could just try doing this work on the decode thread and see if it is a problem.
2685        decoder.unzip(data, in_bits_per_length, out_bits_per_offset, num_rows)?;
2686
2687        Ok(decoder)
2688    }
2689
2690    fn slice_batch_data_and_rebase_offsets_typed<T>(
2691        data: &LanceBuffer,
2692        offsets: &LanceBuffer,
2693    ) -> Result<(LanceBuffer, LanceBuffer)>
2694    where
2695        T: arrow_buffer::ArrowNativeType
2696            + Copy
2697            + PartialOrd
2698            + std::ops::Sub<Output = T>
2699            + std::fmt::Display
2700            + TryInto<usize>,
2701    {
2702        let offsets_slice = offsets.borrow_to_typed_slice::<T>();
2703        let offsets_slice = offsets_slice.as_ref();
2704        if offsets_slice.is_empty() {
2705            return Err(Error::internal(
2706                "Variable offsets cannot be empty".to_string(),
2707            ));
2708        }
2709
2710        let base = offsets_slice[0];
2711        let end = *offsets_slice.last().unwrap();
2712        if end < base {
2713            return Err(Error::internal(format!(
2714                "Invalid variable offsets: end ({end}) is less than base ({base})"
2715            )));
2716        }
2717
2718        let data_start = base.try_into().map_err(|_| {
2719            Error::internal(format!("Variable offset ({base}) does not fit into usize"))
2720        })?;
2721        let data_end = end.try_into().map_err(|_| {
2722            Error::internal(format!("Variable offset ({end}) does not fit into usize"))
2723        })?;
2724        if data_end > data.len() {
2725            return Err(Error::internal(format!(
2726                "Invalid variable offsets: end ({data_end}) exceeds data len ({})",
2727                data.len()
2728            )));
2729        }
2730
2731        let mut rebased_offsets = Vec::with_capacity(offsets_slice.len());
2732        for &offset in offsets_slice {
2733            if offset < base {
2734                return Err(Error::internal(format!(
2735                    "Invalid variable offsets: offset ({offset}) is less than base ({base})"
2736                )));
2737            }
2738            rebased_offsets.push(offset - base);
2739        }
2740
2741        let sliced_data = data.slice_with_length(data_start, data_end - data_start);
2742        // Copy into a compact buffer so each output batch owns only what it references.
2743        let sliced_data = LanceBuffer::copy_slice(&sliced_data);
2744        let rebased_offsets = LanceBuffer::reinterpret_vec(rebased_offsets);
2745        Ok((sliced_data, rebased_offsets))
2746    }
2747
2748    fn slice_batch_data_and_rebase_offsets(
2749        data: &LanceBuffer,
2750        offsets: &LanceBuffer,
2751        bits_per_offset: u8,
2752    ) -> Result<(LanceBuffer, LanceBuffer)> {
2753        match bits_per_offset {
2754            32 => Self::slice_batch_data_and_rebase_offsets_typed::<u32>(data, offsets),
2755            64 => Self::slice_batch_data_and_rebase_offsets_typed::<u64>(data, offsets),
2756            _ => Err(Error::internal(format!(
2757                "Unsupported bits_per_offset={bits_per_offset}"
2758            ))),
2759        }
2760    }
2761
2762    /// Reads a single length prefix from the front of `data`.
2763    ///
2764    /// The bytes come from the file. A page whose item walk ends with a partial
2765    /// trailing item leaves fewer than `bits_per_offset / 8` bytes here, so this
2766    /// is bounds checked and reports a corrupt file rather than reading past the
2767    /// end of the buffer.
2768    fn parse_length(data: &[u8], bits_per_offset: u8) -> Result<u64> {
2769        let width = bits_per_offset as usize / 8;
2770        if data.len() < width {
2771            return Err(corrupt_file_named(
2772                "variable_full_zip",
2773                format!(
2774                    "truncated length prefix: {} byte(s) remain in the page buffer but a \
2775                     {}-bit length prefix requires {}",
2776                    data.len(),
2777                    bits_per_offset,
2778                    width
2779                ),
2780            ));
2781        }
2782        Ok(match bits_per_offset {
2783            8 => data[0] as u64,
2784            16 => u16::from_le_bytes(data[..2].try_into().unwrap()) as u64,
2785            32 => u32::from_le_bytes(data[..4].try_into().unwrap()) as u64,
2786            64 => u64::from_le_bytes(data[..8].try_into().unwrap()),
2787            _ => unreachable!(),
2788        })
2789    }
2790
2791    fn unzip(
2792        &mut self,
2793        data: VecDeque<LanceBuffer>,
2794        in_bits_per_length: u8,
2795        out_bits_per_offset: u8,
2796        num_rows: u64,
2797    ) -> Result<()> {
2798        // This undercounts if there are lists but, at this point, we don't really know how many items we have
2799        let mut rep = Vec::with_capacity(num_rows as usize);
2800        let mut def = Vec::with_capacity(num_rows as usize);
2801        let bytes_cw = self.details.ctrl_word_parser.bytes_per_word() * num_rows as usize;
2802
2803        // This undercounts if there are lists
2804        // It can also overcount if there are invisible items
2805        let bytes_per_offset = out_bits_per_offset as usize / 8;
2806        let bytes_offsets = bytes_per_offset * (num_rows as usize + 1);
2807        let mut offsets_data = Vec::with_capacity(bytes_offsets);
2808
2809        let bytes_per_length = in_bits_per_length as usize / 8;
2810        let bytes_lengths = bytes_per_length * num_rows as usize;
2811
2812        let bytes_data = data.iter().map(|d| d.len()).sum::<usize>();
2813        // This overcounts since bytes_lengths and bytes_cw are undercounts
2814        // It can also undercount if there are invisible items (hence the saturating_sub)
2815        let mut unzipped_data =
2816            Vec::with_capacity((bytes_data - bytes_cw).saturating_sub(bytes_lengths));
2817
2818        let mut current_offset = 0_u64;
2819        let mut visible_item_count = 0_u64;
2820        for databuf in data.into_iter() {
2821            let mut databuf = databuf.as_ref();
2822            while !databuf.is_empty() {
2823                let data_start = unzipped_data.len();
2824                let offset_start = offsets_data.len();
2825                // We might have only-rep or only-def, neither, or both.  They move at the same
2826                // speed though so we only need one index into it
2827                let repdef_start = rep.len().max(def.len());
2828                // TODO: Kind of inefficient we parse the control word twice here
2829                let ctrl_desc = self.details.ctrl_word_parser.parse_desc(
2830                    databuf,
2831                    self.details.max_rep,
2832                    self.details.max_visible_def,
2833                );
2834                self.details
2835                    .ctrl_word_parser
2836                    .parse(databuf, &mut rep, &mut def);
2837                databuf = &databuf[self.details.ctrl_word_parser.bytes_per_word()..];
2838
2839                if ctrl_desc.is_new_row {
2840                    self.repdef_starts.push(repdef_start);
2841                    self.data_starts.push(data_start);
2842                    self.offset_starts.push(offset_start);
2843                    self.visible_item_counts.push(visible_item_count);
2844                }
2845                if ctrl_desc.is_visible {
2846                    visible_item_count += 1;
2847                    if ctrl_desc.is_valid_item {
2848                        let length = Self::parse_length(databuf, in_bits_per_length)?;
2849                        match out_bits_per_offset {
2850                            32 => offsets_data
2851                                .extend_from_slice(&(current_offset as u32).to_le_bytes()),
2852                            64 => offsets_data.extend_from_slice(&current_offset.to_le_bytes()),
2853                            _ => unreachable!(),
2854                        };
2855                        databuf = &databuf[bytes_per_offset..];
2856                        unzipped_data.extend_from_slice(&databuf[..length as usize]);
2857                        databuf = &databuf[length as usize..];
2858                        current_offset += length;
2859                    } else {
2860                        // Null items still get an offset
2861                        match out_bits_per_offset {
2862                            32 => offsets_data
2863                                .extend_from_slice(&(current_offset as u32).to_le_bytes()),
2864                            64 => offsets_data.extend_from_slice(&current_offset.to_le_bytes()),
2865                            _ => unreachable!(),
2866                        }
2867                    }
2868                }
2869            }
2870        }
2871        self.repdef_starts.push(rep.len().max(def.len()));
2872        self.data_starts.push(unzipped_data.len());
2873        self.offset_starts.push(offsets_data.len());
2874        self.visible_item_counts.push(visible_item_count);
2875        match out_bits_per_offset {
2876            32 => offsets_data.extend_from_slice(&(current_offset as u32).to_le_bytes()),
2877            64 => offsets_data.extend_from_slice(&current_offset.to_le_bytes()),
2878            _ => unreachable!(),
2879        };
2880        self.rep = ScalarBuffer::from(rep);
2881        self.def = ScalarBuffer::from(def);
2882        self.data = LanceBuffer::from(unzipped_data);
2883        self.offsets = LanceBuffer::from(offsets_data);
2884        Ok(())
2885    }
2886}
2887
2888impl StructuralPageDecoder for VariableFullZipDecoder {
2889    fn drain(&mut self, num_rows: u64) -> Result<Box<dyn DecodePageTask>> {
2890        let start = self.current_idx;
2891        let end = start + num_rows as usize;
2892
2893        let offset_start = self.offset_starts[start];
2894        let offset_end = self.offset_starts[end] + (self.bits_per_offset as usize / 8);
2895        let offsets = self
2896            .offsets
2897            .slice_with_length(offset_start, offset_end - offset_start);
2898        // Keep each batch's variable data buffer bounded to the selected rows.
2899        let (data, offsets) =
2900            Self::slice_batch_data_and_rebase_offsets(&self.data, &offsets, self.bits_per_offset)?;
2901
2902        let repdef_start = self.repdef_starts[start];
2903        let repdef_end = self.repdef_starts[end];
2904        let rep = if self.rep.is_empty() {
2905            self.rep.clone()
2906        } else {
2907            self.rep.slice(repdef_start, repdef_end - repdef_start)
2908        };
2909        let def = if self.def.is_empty() {
2910            self.def.clone()
2911        } else {
2912            self.def.slice(repdef_start, repdef_end - repdef_start)
2913        };
2914
2915        let visible_item_counts_start = self.visible_item_counts[start];
2916        let visible_item_counts_end = self.visible_item_counts[end];
2917        let num_visible_items = visible_item_counts_end - visible_item_counts_start;
2918
2919        self.current_idx += num_rows as usize;
2920
2921        Ok(Box::new(VariableFullZipDecodeTask {
2922            details: self.details.clone(),
2923            decompressor: self.decompressor.clone(),
2924            data,
2925            offsets,
2926            bits_per_offset: self.bits_per_offset,
2927            num_visible_items,
2928            rep,
2929            def,
2930        }))
2931    }
2932
2933    fn num_rows(&self) -> u64 {
2934        self.num_rows
2935    }
2936}
2937
2938#[derive(Debug)]
2939struct VariableFullZipDecodeTask {
2940    details: Arc<FullZipDecodeDetails>,
2941    decompressor: Arc<dyn VariablePerValueDecompressor>,
2942    data: LanceBuffer,
2943    offsets: LanceBuffer,
2944    bits_per_offset: u8,
2945    num_visible_items: u64,
2946    rep: ScalarBuffer<u16>,
2947    def: ScalarBuffer<u16>,
2948}
2949
2950impl DecodePageTask for VariableFullZipDecodeTask {
2951    fn decode(self: Box<Self>) -> Result<DecodedPage> {
2952        let block = VariableWidthBlock {
2953            data: self.data,
2954            offsets: self.offsets,
2955            bits_per_offset: self.bits_per_offset,
2956            num_values: self.num_visible_items,
2957            block_info: BlockInfo::new(),
2958        };
2959        let decomopressed = self.decompressor.decompress(block)?;
2960        let rep = if self.rep.is_empty() {
2961            None
2962        } else {
2963            Some(self.rep.to_vec())
2964        };
2965        let def = if self.def.is_empty() {
2966            None
2967        } else {
2968            Some(self.def.to_vec())
2969        };
2970        let unraveler = RepDefUnraveler::new(
2971            rep,
2972            def,
2973            self.details.def_meaning.clone(),
2974            self.num_visible_items,
2975        );
2976        Ok(DecodedPage {
2977            data: decomopressed,
2978            repdef: unraveler,
2979        })
2980    }
2981}
2982
2983#[derive(Debug)]
2984struct FullZipDecodeTaskItem {
2985    data: PerValueDataBlock,
2986    rows_in_buf: u64,
2987}
2988
2989/// A task to unzip and decompress full-zip encoded data when that data
2990/// has a fixed-width.
2991#[derive(Debug)]
2992struct FixedFullZipDecodeTask {
2993    details: Arc<FullZipDecodeDetails>,
2994    data: Vec<FullZipDecodeTaskItem>,
2995    num_rows: usize,
2996    bytes_per_value: usize,
2997}
2998
2999impl DecodePageTask for FixedFullZipDecodeTask {
3000    fn decode(self: Box<Self>) -> Result<DecodedPage> {
3001        // Multiply by 2 to make a stab at the size of the output buffer (which will be decompressed and thus bigger)
3002        let estimated_size_bytes = self
3003            .data
3004            .iter()
3005            .map(|task_item| task_item.data.data_size() as usize)
3006            .sum::<usize>()
3007            * 2;
3008        let mut data_builder =
3009            DataBlockBuilder::with_capacity_estimate(estimated_size_bytes as u64);
3010
3011        if self.details.ctrl_word_parser.bytes_per_word() == 0 {
3012            // Fast path, no need to unzip because there is no rep/def
3013            //
3014            // We decompress each buffer and add it to our output buffer
3015            for task_item in self.data.into_iter() {
3016                let PerValueDataBlock::Fixed(fixed_data) = task_item.data else {
3017                    unreachable!()
3018                };
3019                let PerValueDecompressor::Fixed(decompressor) = &self.details.value_decompressor
3020                else {
3021                    unreachable!()
3022                };
3023                debug_assert_eq!(fixed_data.num_values, task_item.rows_in_buf);
3024                let decompressed = decompressor.decompress(fixed_data, task_item.rows_in_buf)?;
3025                data_builder.append(&decompressed, 0..task_item.rows_in_buf);
3026            }
3027
3028            let unraveler = RepDefUnraveler::new(
3029                None,
3030                None,
3031                self.details.def_meaning.clone(),
3032                self.num_rows as u64,
3033            );
3034
3035            Ok(DecodedPage {
3036                data: data_builder.finish(),
3037                repdef: unraveler,
3038            })
3039        } else {
3040            // Slow path, unzipping needed
3041            let mut rep = Vec::with_capacity(self.num_rows);
3042            let mut def = Vec::with_capacity(self.num_rows);
3043
3044            for task_item in self.data.into_iter() {
3045                let PerValueDataBlock::Fixed(fixed_data) = task_item.data else {
3046                    unreachable!()
3047                };
3048                let mut buf_slice = fixed_data.data.as_ref();
3049                let num_values = fixed_data.num_values as usize;
3050                // We will be unzipping repdef in to `rep` and `def` and the
3051                // values into `values` (which contains the compressed values)
3052                let mut values = Vec::with_capacity(
3053                    fixed_data.data.len()
3054                        - (self.details.ctrl_word_parser.bytes_per_word() * num_values),
3055                );
3056                let mut visible_items = 0;
3057                for _ in 0..num_values {
3058                    // Extract rep/def
3059                    self.details
3060                        .ctrl_word_parser
3061                        .parse(buf_slice, &mut rep, &mut def);
3062                    buf_slice = &buf_slice[self.details.ctrl_word_parser.bytes_per_word()..];
3063
3064                    let is_visible = def
3065                        .last()
3066                        .map(|d| *d <= self.details.max_visible_def)
3067                        .unwrap_or(true);
3068                    if is_visible {
3069                        // Extract value
3070                        values.extend_from_slice(buf_slice[..self.bytes_per_value].as_ref());
3071                        buf_slice = &buf_slice[self.bytes_per_value..];
3072                        visible_items += 1;
3073                    }
3074                }
3075
3076                // Finally, we decompress the values and add them to our output buffer
3077                let values_buf = LanceBuffer::from(values);
3078                let fixed_data = FixedWidthDataBlock {
3079                    bits_per_value: self.bytes_per_value as u64 * 8,
3080                    block_info: BlockInfo::new(),
3081                    data: values_buf,
3082                    num_values: visible_items,
3083                };
3084                let PerValueDecompressor::Fixed(decompressor) = &self.details.value_decompressor
3085                else {
3086                    unreachable!()
3087                };
3088                let decompressed = decompressor.decompress(fixed_data, visible_items)?;
3089                data_builder.append(&decompressed, 0..visible_items);
3090            }
3091
3092            let repetition = if rep.is_empty() { None } else { Some(rep) };
3093            let definition = if def.is_empty() { None } else { Some(def) };
3094
3095            let unraveler = RepDefUnraveler::new(
3096                repetition,
3097                definition,
3098                self.details.def_meaning.clone(),
3099                self.num_rows as u64,
3100            );
3101            let data = data_builder.finish();
3102
3103            Ok(DecodedPage {
3104                data,
3105                repdef: unraveler,
3106            })
3107        }
3108    }
3109}
3110
3111#[derive(Debug)]
3112struct StructuralPrimitiveFieldSchedulingJob<'a> {
3113    scheduler: &'a StructuralPrimitiveFieldScheduler,
3114    ranges: Vec<Range<u64>>,
3115    page_idx: usize,
3116    range_idx: usize,
3117    global_row_offset: u64,
3118}
3119
3120impl<'a> StructuralPrimitiveFieldSchedulingJob<'a> {
3121    pub fn new(scheduler: &'a StructuralPrimitiveFieldScheduler, ranges: Vec<Range<u64>>) -> Self {
3122        Self {
3123            scheduler,
3124            ranges,
3125            page_idx: 0,
3126            range_idx: 0,
3127            global_row_offset: 0,
3128        }
3129    }
3130}
3131
3132impl StructuralSchedulingJob for StructuralPrimitiveFieldSchedulingJob<'_> {
3133    fn schedule_next(&mut self, context: &mut SchedulerContext) -> Result<Vec<ScheduledScanLine>> {
3134        if self.range_idx >= self.ranges.len() {
3135            return Ok(Vec::new());
3136        }
3137        // Get our current range
3138        let mut range = self.ranges[self.range_idx].clone();
3139        let priority = range.start;
3140
3141        let mut cur_page = &self.scheduler.page_schedulers[self.page_idx];
3142        trace!(
3143            "Current range is {:?} and current page has {} rows",
3144            range, cur_page.num_rows
3145        );
3146        // Skip entire pages until we have some overlap with our next range
3147        while cur_page.num_rows + self.global_row_offset <= range.start {
3148            self.global_row_offset += cur_page.num_rows;
3149            self.page_idx += 1;
3150            trace!("Skipping entire page of {} rows", cur_page.num_rows);
3151            cur_page = &self.scheduler.page_schedulers[self.page_idx];
3152        }
3153
3154        // Now the cur_page has overlap with range.  Continue looping through ranges
3155        // until we find a range that exceeds the current page
3156
3157        let mut ranges_in_page = Vec::new();
3158        while cur_page.num_rows + self.global_row_offset > range.start {
3159            range.start = range.start.max(self.global_row_offset);
3160            let start_in_page = range.start - self.global_row_offset;
3161            let end_in_page = start_in_page + (range.end - range.start);
3162            let end_in_page = end_in_page.min(cur_page.num_rows);
3163            let last_in_range = (end_in_page + self.global_row_offset) >= range.end;
3164
3165            ranges_in_page.push(start_in_page..end_in_page);
3166            if last_in_range {
3167                self.range_idx += 1;
3168                if self.range_idx == self.ranges.len() {
3169                    break;
3170                }
3171                range = self.ranges[self.range_idx].clone();
3172            } else {
3173                break;
3174            }
3175        }
3176
3177        trace!(
3178            "Scheduling {} rows across {} ranges from page with {} rows (priority={}, column_index={}, page_index={})",
3179            ranges_in_page.iter().map(|r| r.end - r.start).sum::<u64>(),
3180            ranges_in_page.len(),
3181            cur_page.num_rows,
3182            priority,
3183            self.scheduler.column_index,
3184            cur_page.page_index,
3185        );
3186
3187        self.global_row_offset += cur_page.num_rows;
3188        self.page_idx += 1;
3189
3190        let page_decoders = cur_page
3191            .scheduler
3192            .schedule_ranges(&ranges_in_page, context.io())?;
3193
3194        let cur_path = context.current_path();
3195        page_decoders
3196            .into_iter()
3197            .map(|page_load_task| {
3198                let cur_path = cur_path.clone();
3199                let page_decoder = page_load_task.decoder_fut;
3200                let unloaded_page = async move {
3201                    let page_decoder = page_decoder.await?;
3202                    Ok(LoadedPageShard {
3203                        decoder: page_decoder,
3204                        path: cur_path,
3205                    })
3206                }
3207                .boxed();
3208                Ok(ScheduledScanLine {
3209                    decoders: vec![MessageType::UnloadedPage(UnloadedPageShard(unloaded_page))],
3210                    rows_scheduled: page_load_task.num_rows,
3211                })
3212            })
3213            .collect::<Result<Vec<_>>>()
3214    }
3215}
3216
3217#[derive(Debug)]
3218struct PageInfoAndScheduler {
3219    page_index: usize,
3220    num_rows: u64,
3221    scheduler: Box<dyn StructuralPageScheduler>,
3222}
3223
3224/// A scheduler for a leaf node
3225///
3226/// Here we look at the layout of the various pages and delegate scheduling to a scheduler
3227/// appropriate for the layout of the page.
3228#[derive(Debug)]
3229pub struct StructuralPrimitiveFieldScheduler {
3230    page_schedulers: Vec<PageInfoAndScheduler>,
3231    column_index: u32,
3232}
3233
3234impl StructuralPrimitiveFieldScheduler {
3235    pub fn try_new(
3236        column_info: &ColumnInfo,
3237        decompressors: &dyn DecompressionStrategy,
3238        cache_repetition_index: bool,
3239        target_field: &Field,
3240    ) -> Result<Self> {
3241        let page_schedulers = column_info
3242            .page_infos
3243            .iter()
3244            .enumerate()
3245            .map(|(page_index, page_info)| {
3246                Self::page_info_to_scheduler(
3247                    page_info,
3248                    page_index,
3249                    decompressors,
3250                    cache_repetition_index,
3251                    target_field,
3252                )
3253            })
3254            .collect::<Result<Vec<_>>>()?;
3255        Ok(Self {
3256            page_schedulers,
3257            column_index: column_info.index,
3258        })
3259    }
3260
3261    fn page_layout_to_scheduler(
3262        page_info: &PageInfo,
3263        page_layout: &PageLayout,
3264        decompressors: &dyn DecompressionStrategy,
3265        cache_repetition_index: bool,
3266        target_field: &Field,
3267    ) -> Result<Box<dyn StructuralPageScheduler>> {
3268        use pb21::page_layout::Layout;
3269        Ok(match page_layout.layout.as_ref().expect_ok()? {
3270            Layout::MiniBlockLayout(mini_block) => Box::new(MiniBlockScheduler::try_new(
3271                &page_info.buffer_offsets_and_sizes,
3272                page_info.priority,
3273                mini_block.num_items,
3274                mini_block,
3275                decompressors,
3276            )?),
3277            Layout::FullZipLayout(full_zip) => {
3278                let mut scheduler = FullZipScheduler::try_new(
3279                    &page_info.buffer_offsets_and_sizes,
3280                    page_info.priority,
3281                    page_info.num_rows,
3282                    full_zip,
3283                    decompressors,
3284                )?;
3285                scheduler.enable_cache = cache_repetition_index;
3286                Box::new(scheduler)
3287            }
3288            Layout::ConstantLayout(constant_layout) => {
3289                let def_meaning = constant_layout
3290                    .layers
3291                    .iter()
3292                    .map(|l| ProtobufUtils21::repdef_layer_to_def_interp(*l))
3293                    .collect::<Vec<_>>();
3294                let has_scalar_value = constant_layout.inline_value.is_some()
3295                    || page_info.buffer_offsets_and_sizes.len() == 1
3296                    || page_info.buffer_offsets_and_sizes.len() == 3;
3297                if has_scalar_value {
3298                    Box::new(constant::ConstantPageScheduler::try_new(
3299                        page_info.buffer_offsets_and_sizes.clone(),
3300                        constant_layout.inline_value.clone(),
3301                        target_field.data_type(),
3302                        def_meaning.into(),
3303                    )?) as Box<dyn StructuralPageScheduler>
3304                } else if def_meaning.len() == 1
3305                    && def_meaning[0] == DefinitionInterpretation::NullableItem
3306                {
3307                    Box::new(SimpleAllNullScheduler::default()) as Box<dyn StructuralPageScheduler>
3308                } else {
3309                    let rep_decompressor = constant_layout
3310                        .rep_compression
3311                        .as_ref()
3312                        .map(|encoding| decompressors.create_block_decompressor(encoding))
3313                        .transpose()?
3314                        .map(Arc::from);
3315
3316                    let def_decompressor = constant_layout
3317                        .def_compression
3318                        .as_ref()
3319                        .map(|encoding| decompressors.create_block_decompressor(encoding))
3320                        .transpose()?
3321                        .map(Arc::from);
3322
3323                    Box::new(ComplexAllNullScheduler::new(
3324                        page_info.buffer_offsets_and_sizes.clone(),
3325                        def_meaning.into(),
3326                        rep_decompressor,
3327                        def_decompressor,
3328                        constant_layout.num_rep_values,
3329                        constant_layout.num_def_values,
3330                    )) as Box<dyn StructuralPageScheduler>
3331                }
3332            }
3333            Layout::BlobLayout(blob) => {
3334                let inner_scheduler = Self::page_layout_to_scheduler(
3335                    page_info,
3336                    blob.inner_layout.as_ref().expect_ok()?.as_ref(),
3337                    decompressors,
3338                    cache_repetition_index,
3339                    target_field,
3340                )?;
3341                let def_meaning = blob
3342                    .layers
3343                    .iter()
3344                    .map(|l| ProtobufUtils21::repdef_layer_to_def_interp(*l))
3345                    .collect::<Vec<_>>();
3346                if matches!(target_field.data_type(), DataType::Struct(_)) {
3347                    // User wants to decode blob into struct
3348                    Box::new(BlobDescriptionPageScheduler::new(
3349                        inner_scheduler,
3350                        def_meaning.into(),
3351                    ))
3352                } else {
3353                    // User wants to decode blob into binary data
3354                    Box::new(BlobPageScheduler::new(
3355                        inner_scheduler,
3356                        page_info.priority,
3357                        page_info.num_rows,
3358                        def_meaning.into(),
3359                    ))
3360                }
3361            }
3362        })
3363    }
3364
3365    fn page_info_to_scheduler(
3366        page_info: &PageInfo,
3367        page_index: usize,
3368        decompressors: &dyn DecompressionStrategy,
3369        cache_repetition_index: bool,
3370        target_field: &Field,
3371    ) -> Result<PageInfoAndScheduler> {
3372        let page_layout = page_info.encoding.as_structural();
3373        let scheduler = Self::page_layout_to_scheduler(
3374            page_info,
3375            page_layout,
3376            decompressors,
3377            cache_repetition_index,
3378            target_field,
3379        )?;
3380        Ok(PageInfoAndScheduler {
3381            page_index,
3382            num_rows: page_info.num_rows,
3383            scheduler,
3384        })
3385    }
3386}
3387
3388pub trait CachedPageData: Any + Send + Sync + DeepSizeOf + 'static {
3389    fn as_arc_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync + 'static>;
3390}
3391
3392pub struct NoCachedPageData;
3393
3394impl DeepSizeOf for NoCachedPageData {
3395    fn deep_size_of_children(&self, _ctx: &mut Context) -> usize {
3396        0
3397    }
3398}
3399impl CachedPageData for NoCachedPageData {
3400    fn as_arc_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync + 'static> {
3401        self
3402    }
3403}
3404
3405pub struct CachedFieldData {
3406    pages: Vec<Arc<dyn CachedPageData>>,
3407}
3408
3409impl DeepSizeOf for CachedFieldData {
3410    fn deep_size_of_children(&self, ctx: &mut Context) -> usize {
3411        self.pages.deep_size_of_children(ctx)
3412    }
3413}
3414
3415// Cache key for field data
3416#[derive(Debug, Clone)]
3417pub struct FieldDataCacheKey {
3418    pub column_index: u32,
3419}
3420
3421impl CacheKey for FieldDataCacheKey {
3422    type ValueType = CachedFieldData;
3423
3424    fn key(&self) -> std::borrow::Cow<'_, str> {
3425        self.column_index.to_string().into()
3426    }
3427}
3428
3429impl StructuralFieldScheduler for StructuralPrimitiveFieldScheduler {
3430    fn initialize<'a>(
3431        &'a mut self,
3432        _filter: &'a FilterExpression,
3433        context: &'a SchedulerContext,
3434    ) -> BoxFuture<'a, Result<()>> {
3435        let cache_key = FieldDataCacheKey {
3436            column_index: self.column_index,
3437        };
3438        let cache = context.cache().clone();
3439
3440        async move {
3441            if let Some(cached_data) = cache.get_with_key(&cache_key).await {
3442                self.page_schedulers
3443                    .iter_mut()
3444                    .zip(cached_data.pages.iter())
3445                    .for_each(|(page_scheduler, cached_data)| {
3446                        page_scheduler.scheduler.load(cached_data);
3447                    });
3448                return Ok(());
3449            }
3450
3451            let page_data = self
3452                .page_schedulers
3453                .iter_mut()
3454                .map(|s| s.scheduler.initialize(context.io()))
3455                .collect::<FuturesOrdered<_>>();
3456
3457            let page_data = page_data.try_collect::<Vec<_>>().await?;
3458            let cached_data = Arc::new(CachedFieldData { pages: page_data });
3459            cache.insert_with_key(&cache_key, cached_data).await;
3460            Ok(())
3461        }
3462        .boxed()
3463    }
3464
3465    fn schedule_ranges<'a>(
3466        &'a self,
3467        ranges: &[Range<u64>],
3468        _filter: &FilterExpression,
3469    ) -> Result<Box<dyn StructuralSchedulingJob + 'a>> {
3470        let ranges = ranges.to_vec();
3471        Ok(Box::new(StructuralPrimitiveFieldSchedulingJob::new(
3472            self, ranges,
3473        )))
3474    }
3475}
3476
3477/// Takes the output from several pages decoders and
3478/// concatenates them.
3479#[derive(Debug)]
3480pub struct StructuralCompositeDecodeArrayTask {
3481    tasks: Vec<Box<dyn DecodePageTask>>,
3482    should_validate: bool,
3483    data_type: DataType,
3484}
3485
3486impl StructuralCompositeDecodeArrayTask {
3487    fn restore_validity(
3488        array: Arc<dyn Array>,
3489        unraveler: &mut CompositeRepDefUnraveler,
3490    ) -> Arc<dyn Array> {
3491        let validity = unraveler.unravel_validity(array.len());
3492        let Some(validity) = validity else {
3493            return array;
3494        };
3495        if array.data_type() == &DataType::Null {
3496            // We unravel from a null array but we don't add the null buffer because arrow-rs doesn't like it
3497            return array;
3498        }
3499        assert_eq!(validity.len(), array.len());
3500        // SAFETY: We've should have already asserted the buffers are all valid, we are just
3501        // adding null buffers to the array here
3502        make_array(unsafe {
3503            array
3504                .to_data()
3505                .into_builder()
3506                .nulls(Some(validity))
3507                .build_unchecked()
3508        })
3509    }
3510}
3511
3512impl StructuralDecodeArrayTask for StructuralCompositeDecodeArrayTask {
3513    fn decode(self: Box<Self>) -> Result<DecodedArray> {
3514        let mut arrays = Vec::with_capacity(self.tasks.len());
3515        let mut unravelers = Vec::with_capacity(self.tasks.len());
3516        for task in self.tasks {
3517            let decoded = task.decode()?;
3518            unravelers.push(decoded.repdef);
3519
3520            let array = make_array(
3521                decoded
3522                    .data
3523                    .into_arrow(self.data_type.clone(), self.should_validate)?,
3524            );
3525
3526            arrays.push(array);
3527        }
3528        let array_refs = arrays.iter().map(|arr| arr.as_ref()).collect::<Vec<_>>();
3529        let array = arrow_select::concat::concat(&array_refs)?;
3530        let mut repdef = CompositeRepDefUnraveler::new(unravelers);
3531
3532        let array = Self::restore_validity(array, &mut repdef);
3533
3534        Ok(DecodedArray { array, repdef })
3535    }
3536}
3537
3538#[derive(Debug)]
3539pub struct StructuralPrimitiveFieldDecoder {
3540    field: Arc<ArrowField>,
3541    page_decoders: VecDeque<Box<dyn StructuralPageDecoder>>,
3542    should_validate: bool,
3543    rows_drained_in_current: u64,
3544}
3545
3546impl StructuralPrimitiveFieldDecoder {
3547    pub fn new(field: &Arc<ArrowField>, should_validate: bool) -> Self {
3548        Self {
3549            field: field.clone(),
3550            page_decoders: VecDeque::new(),
3551            should_validate,
3552            rows_drained_in_current: 0,
3553        }
3554    }
3555}
3556
3557impl StructuralFieldDecoder for StructuralPrimitiveFieldDecoder {
3558    fn accept_page(&mut self, child: LoadedPageShard) -> Result<()> {
3559        assert!(child.path.is_empty());
3560        self.page_decoders.push_back(child.decoder);
3561        Ok(())
3562    }
3563
3564    fn drain(&mut self, num_rows: u64) -> Result<Box<dyn StructuralDecodeArrayTask>> {
3565        let mut remaining = num_rows;
3566        let mut tasks = Vec::new();
3567        while remaining > 0 {
3568            let cur_page = self.page_decoders.front_mut().unwrap();
3569            let num_in_page = cur_page.num_rows() - self.rows_drained_in_current;
3570            let to_take = num_in_page.min(remaining);
3571
3572            let task = cur_page.drain(to_take)?;
3573            tasks.push(task);
3574
3575            if to_take == num_in_page {
3576                self.page_decoders.pop_front();
3577                self.rows_drained_in_current = 0;
3578            } else {
3579                self.rows_drained_in_current += to_take;
3580            }
3581
3582            remaining -= to_take;
3583        }
3584        Ok(Box::new(StructuralCompositeDecodeArrayTask {
3585            tasks,
3586            should_validate: self.should_validate,
3587            data_type: self.field.data_type().clone(),
3588        }))
3589    }
3590
3591    fn data_type(&self) -> &DataType {
3592        self.field.data_type()
3593    }
3594}
3595
3596/// The serialized representation of full-zip data
3597struct SerializedFullZip {
3598    /// The zipped values buffer
3599    values: LanceBuffer,
3600    /// The repetition index (only present if there is repetition)
3601    repetition_index: Option<LanceBuffer>,
3602}
3603
3604// We align and pad mini-blocks to 8 byte boundaries for two reasons.  First,
3605// to allow us to store a chunk size in 12 bits.
3606//
3607// If we directly record the size in bytes with 12 bits we would be limited to
3608// 4KiB which is too small.  Since we know each mini-block consists of 8 byte
3609// words we can store the # of words instead which gives us 32KiB.  We want
3610// at least 24KiB so we can handle even the worst case of
3611// - 4Ki values compressed into an 8186 byte buffer
3612// - 4 bytes to describe rep & def lengths
3613// - 16KiB of rep & def buffer (this will almost never happen but life is easier if we
3614//   plan for it)
3615//
3616// Second, each chunk in a mini-block is aligned to 8 bytes.  This allows multi-byte
3617// values like offsets to be stored in a mini-block and safely read back out.  It also
3618// helps ensure zero-copy reads in cases where zero-copy is possible (e.g. no decoding
3619// needed).
3620//
3621// Note: by "aligned to 8 bytes" we mean BOTH "aligned to 8 bytes from the start of
3622// the page" and "aligned to 8 bytes from the start of the file."
3623const MINIBLOCK_ALIGNMENT: usize = 8;
3624
3625/// An encoder for primitive (leaf) arrays
3626///
3627/// This encoder is fairly complicated and follows a number of paths depending
3628/// on the data.
3629///
3630/// First, we convert the validity & offsets information into repetition and
3631/// definition levels.  Then we compress the data itself into a single buffer.
3632///
3633/// If the data is narrow then we encode the data in small chunks (each chunk
3634/// should be a few disk sectors and contains a buffer of repetition, a buffer
3635/// of definition, and a buffer of value data).  This approach is called
3636/// "mini-block".  These mini-blocks are stored into a single data buffer.
3637///
3638/// If the data is wide then we zip together the repetition and definition value
3639/// with the value data into a single buffer.  This approach is called "zipped".
3640///
3641/// If there is any repetition information then we create a repetition index
3642///
3643/// In addition, the compression process may create zero or more metadata buffers.
3644/// For example, a dictionary compression will create dictionary metadata.  Any
3645/// mini-block approach has a metadata buffer of block sizes.  This metadata is
3646/// stored in a separate buffer on disk and read at initialization time.
3647///
3648/// TODO: We should concatenate metadata buffers from all pages into a single buffer
3649/// at (roughly) the end of the file so there is, at most, one read per column of
3650/// metadata per file.
3651pub struct PrimitiveStructuralEncoder {
3652    // Accumulates arrays until we have enough data to justify a disk page
3653    accumulation_queue: AccumulationQueue,
3654
3655    keep_original_array: bool,
3656    support_large_chunk: bool,
3657    accumulated_repdefs: Vec<RepDefBuilder>,
3658    // The compression strategy we will use to compress the data
3659    compression_strategy: Arc<dyn CompressionStrategy>,
3660    column_index: u32,
3661    field: Field,
3662    encoding_metadata: Arc<HashMap<String, String>>,
3663    version: LanceFileVersion,
3664}
3665
3666struct CompressedLevelsChunk {
3667    data: LanceBuffer,
3668    num_levels: u16,
3669}
3670
3671struct CompressedLevels {
3672    data: Vec<CompressedLevelsChunk>,
3673    compression: CompressiveEncoding,
3674    rep_index: Option<LanceBuffer>,
3675}
3676
3677struct SerializedMiniBlockPage {
3678    num_buffers: u64,
3679    data: LanceBuffer,
3680    metadata: LanceBuffer,
3681}
3682
3683#[derive(Debug, Clone, Copy)]
3684struct DictEncodingBudget {
3685    max_dict_entries: u32,
3686    max_encoded_size: usize,
3687}
3688
3689impl PrimitiveStructuralEncoder {
3690    pub fn try_new(
3691        options: &EncodingOptions,
3692        compression_strategy: Arc<dyn CompressionStrategy>,
3693        column_index: u32,
3694        field: Field,
3695        encoding_metadata: Arc<HashMap<String, String>>,
3696    ) -> Result<Self> {
3697        Ok(Self {
3698            accumulation_queue: AccumulationQueue::new(
3699                options.cache_bytes_per_column,
3700                column_index,
3701                options.keep_original_array,
3702            ),
3703            support_large_chunk: options.support_large_chunk(),
3704            keep_original_array: options.keep_original_array,
3705            accumulated_repdefs: Vec::new(),
3706            column_index,
3707            compression_strategy,
3708            field,
3709            encoding_metadata,
3710            version: options.version,
3711        })
3712    }
3713
3714    // TODO: This is a heuristic we may need to tune at some point
3715    //
3716    // As data gets narrow then the "zipping" process gets too expensive
3717    //   and we prefer mini-block
3718    // As data gets wide then the # of values per block shrinks (very wide)
3719    //   data doesn't even fit in a mini-block and the block overhead gets
3720    //   too large and we prefer zipped.
3721    fn is_narrow(data_block: &DataBlock) -> bool {
3722        const MINIBLOCK_MAX_BYTE_LENGTH_PER_VALUE: u64 = 256;
3723
3724        if let Some(max_len_array) = data_block.get_stat(Stat::MaxLength) {
3725            let max_len_array = max_len_array
3726                .as_any()
3727                .downcast_ref::<PrimitiveArray<UInt64Type>>()
3728                .unwrap();
3729            if max_len_array.value(0) < MINIBLOCK_MAX_BYTE_LENGTH_PER_VALUE {
3730                return true;
3731            }
3732        }
3733        false
3734    }
3735
3736    fn prefers_miniblock(
3737        data_block: &DataBlock,
3738        encoding_metadata: &HashMap<String, String>,
3739    ) -> bool {
3740        // If the user specifically requested miniblock then use it
3741        if let Some(user_requested) = encoding_metadata.get(STRUCTURAL_ENCODING_META_KEY) {
3742            return user_requested.to_lowercase() == STRUCTURAL_ENCODING_MINIBLOCK;
3743        }
3744        // Otherwise only use miniblock if it is narrow
3745        Self::is_narrow(data_block)
3746    }
3747
3748    fn prefers_fullzip(encoding_metadata: &HashMap<String, String>) -> bool {
3749        // Fullzip is the backup option so the only reason we wouldn't use it is if the
3750        // user specifically requested not to use it (in which case we're probably going
3751        // to emit an error)
3752        if let Some(user_requested) = encoding_metadata.get(STRUCTURAL_ENCODING_META_KEY) {
3753            return user_requested.to_lowercase() == STRUCTURAL_ENCODING_FULLZIP;
3754        }
3755        true
3756    }
3757
3758    // Converts value data, repetition levels, and definition levels into a single
3759    // buffer of mini-blocks.  In addition, creates a buffer of mini-block metadata
3760    // which tells us the size of each block.  Finally, if repetition is present then
3761    // we also create a buffer for the repetition index.
3762    //
3763    // Each chunk is serialized as:
3764    // | num_bufs (1 byte) | buf_lens (2 bytes per buffer) | P | buf0 | P | buf1 | ... | bufN | P |
3765    //
3766    // P - Padding inserted to ensure each buffer is 8-byte aligned and the buffer size is a multiple
3767    //     of 8 bytes (so that the next chunk is 8-byte aligned).
3768    //
3769    // Each block has a u16 word of metadata.  The upper 12 bits contain the
3770    // # of 8-byte words in the block (if the block does not fill the final word
3771    // then up to 7 bytes of padding are added).  The lower 4 bits describe the log_2
3772    // number of values (e.g. if there are 1024 then the lower 4 bits will be
3773    // 0xA)  All blocks except the last must have power-of-two number of values.
3774    // This not only makes metadata smaller but it makes decoding easier since
3775    // batch sizes are typically a power of 2.  4 bits would allow us to express
3776    // up to 16Ki values but we restrict this further to 4Ki values.
3777    //
3778    // This means blocks can have 1 to 4Ki values and 8 - 32Ki bytes.
3779    //
3780    // All metadata words are serialized (as little endian) into a single buffer
3781    // of metadata values.
3782    //
3783    // If there is repetition then we also create a repetition index.  This is a
3784    // single buffer of integer vectors (stored in row major order).  There is one
3785    // entry for each chunk.  The size of the vector is based on the depth of random
3786    // access we want to support.
3787    //
3788    // A vector of size 2 is the minimum and will support row-based random access (e.g.
3789    // "take the 57th row").  A vector of size 3 will support 1 level of nested access
3790    // (e.g. "take the 3rd item in the 57th row").  A vector of size 4 will support 2
3791    // levels of nested access and so on.
3792    //
3793    // The first number in the vector is the number of top-level rows that complete in
3794    // the chunk.  The second number is the number of second-level rows that complete
3795    // after the final top-level row completed (or beginning of the chunk if no top-level
3796    // row completes in the chunk).  And so on.  The final number in the vector is always
3797    // the number of leftover items not covered by earlier entries in the vector.
3798    //
3799    // Currently we are limited to 0 levels of nested access but that will change in the
3800    // future.
3801    //
3802    // The repetition index and the chunk metadata are read at initialization time and
3803    // cached in memory.
3804    fn serialize_miniblocks(
3805        miniblocks: MiniBlockCompressed,
3806        rep: Option<Vec<CompressedLevelsChunk>>,
3807        def: Option<Vec<CompressedLevelsChunk>>,
3808        support_large_chunk: bool,
3809    ) -> SerializedMiniBlockPage {
3810        let bytes_rep = rep
3811            .as_ref()
3812            .map(|rep| rep.iter().map(|r| r.data.len()).sum::<usize>())
3813            .unwrap_or(0);
3814        let bytes_def = def
3815            .as_ref()
3816            .map(|def| def.iter().map(|d| d.data.len()).sum::<usize>())
3817            .unwrap_or(0);
3818        let bytes_data = miniblocks.data.iter().map(|d| d.len()).sum::<usize>();
3819        let mut num_buffers = miniblocks.data.len();
3820        if rep.is_some() {
3821            num_buffers += 1;
3822        }
3823        if def.is_some() {
3824            num_buffers += 1;
3825        }
3826        // 2 bytes for the length of each buffer and up to 7 bytes of padding per buffer
3827        let max_extra = 9 * num_buffers;
3828        let mut data_buffer = Vec::with_capacity(bytes_rep + bytes_def + bytes_data + max_extra);
3829        let chunk_size_bytes = if support_large_chunk { 4 } else { 2 };
3830        let mut meta_buffer = Vec::with_capacity(miniblocks.chunks.len() * chunk_size_bytes);
3831
3832        let mut rep_iter = rep.map(|r| r.into_iter());
3833        let mut def_iter = def.map(|d| d.into_iter());
3834
3835        let mut buffer_offsets = vec![0; miniblocks.data.len()];
3836        for chunk in miniblocks.chunks {
3837            let start_pos = data_buffer.len();
3838            // Start of chunk should be aligned
3839            debug_assert_eq!(start_pos % MINIBLOCK_ALIGNMENT, 0);
3840
3841            let rep = rep_iter.as_mut().map(|r| r.next().unwrap());
3842            let def = def_iter.as_mut().map(|d| d.next().unwrap());
3843
3844            // Write the number of levels, or 0 if there is no rep/def
3845            let num_levels = rep
3846                .as_ref()
3847                .map(|r| r.num_levels)
3848                .unwrap_or(def.as_ref().map(|d| d.num_levels).unwrap_or(0));
3849            data_buffer.extend_from_slice(&num_levels.to_le_bytes());
3850
3851            // Write the buffer lengths
3852            if let Some(rep) = rep.as_ref() {
3853                let bytes_rep = u16::try_from(rep.data.len()).unwrap();
3854                data_buffer.extend_from_slice(&bytes_rep.to_le_bytes());
3855            }
3856            if let Some(def) = def.as_ref() {
3857                let bytes_def = u16::try_from(def.data.len()).unwrap();
3858                data_buffer.extend_from_slice(&bytes_def.to_le_bytes());
3859            }
3860
3861            if support_large_chunk {
3862                for &buffer_size in &chunk.buffer_sizes {
3863                    data_buffer.extend_from_slice(&buffer_size.to_le_bytes());
3864                }
3865            } else {
3866                for &buffer_size in &chunk.buffer_sizes {
3867                    data_buffer.extend_from_slice(&(buffer_size as u16).to_le_bytes());
3868                }
3869            }
3870
3871            // Pad
3872            let add_padding = |data_buffer: &mut Vec<u8>| {
3873                let pad = pad_bytes::<MINIBLOCK_ALIGNMENT>(data_buffer.len());
3874                data_buffer.extend(iter::repeat_n(FILL_BYTE, pad));
3875            };
3876            add_padding(&mut data_buffer);
3877
3878            // Write the buffers themselves
3879            if let Some(rep) = rep.as_ref() {
3880                data_buffer.extend_from_slice(&rep.data);
3881                add_padding(&mut data_buffer);
3882            }
3883            if let Some(def) = def.as_ref() {
3884                data_buffer.extend_from_slice(&def.data);
3885                add_padding(&mut data_buffer);
3886            }
3887            for (buffer_size, (buffer, buffer_offset)) in chunk
3888                .buffer_sizes
3889                .iter()
3890                .zip(miniblocks.data.iter().zip(buffer_offsets.iter_mut()))
3891            {
3892                let start = *buffer_offset;
3893                let end = start + *buffer_size as usize;
3894                *buffer_offset += *buffer_size as usize;
3895                data_buffer.extend_from_slice(&buffer[start..end]);
3896                add_padding(&mut data_buffer);
3897            }
3898
3899            let chunk_bytes = data_buffer.len() - start_pos;
3900            let max_chunk_size = if support_large_chunk {
3901                4 * 1024 * 1024 * 1024 // 4GB limit with u32 metadata
3902            } else {
3903                32 * 1024 // 32KiB limit with u16 metadata
3904            };
3905            assert!(chunk_bytes <= max_chunk_size);
3906            assert!(chunk_bytes > 0);
3907            assert_eq!(chunk_bytes % 8, 0);
3908            // 4Ki values max
3909            assert!(chunk.log_num_values <= 12);
3910            // We subtract 1 here from chunk_bytes because we want to be able to express
3911            // a size of 32KiB and not (32Ki - 8)B which is what we'd get otherwise with
3912            // 0xFFF
3913            let divided_bytes = chunk_bytes / MINIBLOCK_ALIGNMENT;
3914            let divided_bytes_minus_one = (divided_bytes - 1) as u64;
3915
3916            let metadata = (divided_bytes_minus_one << 4) | chunk.log_num_values as u64;
3917            if support_large_chunk {
3918                meta_buffer.extend_from_slice(&(metadata as u32).to_le_bytes());
3919            } else {
3920                meta_buffer.extend_from_slice(&(metadata as u16).to_le_bytes());
3921            }
3922        }
3923
3924        let data_buffer = LanceBuffer::from(data_buffer);
3925        let metadata_buffer = LanceBuffer::from(meta_buffer);
3926
3927        SerializedMiniBlockPage {
3928            num_buffers: miniblocks.data.len() as u64,
3929            data: data_buffer,
3930            metadata: metadata_buffer,
3931        }
3932    }
3933
3934    /// Compresses a buffer of levels into chunks
3935    ///
3936    /// If these are repetition levels then we also calculate the repetition index here (that
3937    /// is the third return value)
3938    fn compress_levels(
3939        mut levels: RepDefSlicer<'_>,
3940        num_elements: u64,
3941        compression_strategy: &dyn CompressionStrategy,
3942        chunks: &[MiniBlockChunk],
3943        // This will be 0 if we are compressing def levels
3944        max_rep: u16,
3945    ) -> Result<CompressedLevels> {
3946        let mut rep_index = if max_rep > 0 {
3947            Vec::with_capacity(chunks.len())
3948        } else {
3949            vec![]
3950        };
3951        // Make the levels into a FixedWidth data block
3952        let num_levels = levels.num_levels() as u64;
3953        let levels_buf = levels.all_levels().clone();
3954
3955        let mut fixed_width_block = FixedWidthDataBlock {
3956            data: levels_buf,
3957            bits_per_value: 16,
3958            num_values: num_levels,
3959            block_info: BlockInfo::new(),
3960        };
3961        // Compute statistics to enable optimal compression for rep/def levels
3962        fixed_width_block.compute_stat();
3963
3964        let levels_block = DataBlock::FixedWidth(fixed_width_block);
3965        let levels_field = Field::new_arrow("", DataType::UInt16, false)?;
3966        // Pick a block compressor
3967        let (compressor, compressor_desc) =
3968            compression_strategy.create_block_compressor(&levels_field, &levels_block)?;
3969        // Compress blocks of levels (sized according to the chunks)
3970        let mut level_chunks = Vec::with_capacity(chunks.len());
3971        let mut values_counter = 0;
3972        for (chunk_idx, chunk) in chunks.iter().enumerate() {
3973            let chunk_num_values = chunk.num_values(values_counter, num_elements);
3974            debug_assert!(chunk_num_values > 0);
3975            values_counter += chunk_num_values;
3976            let chunk_levels = if chunk_idx < chunks.len() - 1 {
3977                levels.slice_next(chunk_num_values as usize)
3978            } else {
3979                levels.slice_rest()
3980            };
3981            let num_chunk_levels = (chunk_levels.len() / 2) as u64;
3982            if max_rep > 0 {
3983                // If max_rep > 0 then we are working with rep levels and we need
3984                // to calculate the repetition index.  The repetition index for a
3985                // chunk is currently 2 values (in the future it may be more).
3986                //
3987                // The first value is the number of rows that _finish_ in the
3988                // chunk.
3989                //
3990                // The second value is the number of "leftovers" after the last
3991                // finished row in the chunk.
3992                let rep_values = chunk_levels.borrow_to_typed_slice::<u16>();
3993                let rep_values = rep_values.as_ref();
3994
3995                // We skip 1 here because a max_rep at spot 0 doesn't count as a finished list (we
3996                // will count it in the previous chunk)
3997                let mut num_rows = rep_values.iter().skip(1).filter(|v| **v == max_rep).count();
3998                let num_leftovers = if chunk_idx < chunks.len() - 1 {
3999                    rep_values
4000                        .iter()
4001                        .rev()
4002                        .position(|v| *v == max_rep)
4003                        // # of leftovers includes the max_rep spot
4004                        .map(|pos| pos + 1)
4005                        .unwrap_or(rep_values.len())
4006                } else {
4007                    // Last chunk can't have leftovers
4008                    0
4009                };
4010
4011                if chunk_idx != 0 && rep_values.first() == Some(&max_rep) {
4012                    // This chunk starts with a new row and so, if we thought we had leftovers
4013                    // in the previous chunk, we were mistaken
4014                    // TODO: Can use unchecked here
4015                    let rep_len = rep_index.len();
4016                    if rep_index[rep_len - 1] != 0 {
4017                        // We thought we had leftovers but that was actually a full row
4018                        rep_index[rep_len - 2] += 1;
4019                        rep_index[rep_len - 1] = 0;
4020                    }
4021                }
4022
4023                if chunk_idx == chunks.len() - 1 {
4024                    // The final list
4025                    num_rows += 1;
4026                }
4027                rep_index.push(num_rows as u64);
4028                rep_index.push(num_leftovers as u64);
4029            }
4030            let mut chunk_fixed_width = FixedWidthDataBlock {
4031                data: chunk_levels,
4032                bits_per_value: 16,
4033                num_values: num_chunk_levels,
4034                block_info: BlockInfo::new(),
4035            };
4036            chunk_fixed_width.compute_stat();
4037            let chunk_levels_block = DataBlock::FixedWidth(chunk_fixed_width);
4038            let compressed_levels = compressor.compress(chunk_levels_block)?;
4039            level_chunks.push(CompressedLevelsChunk {
4040                data: compressed_levels,
4041                num_levels: num_chunk_levels as u16,
4042            });
4043        }
4044        debug_assert_eq!(levels.num_levels_remaining(), 0);
4045        let rep_index = if rep_index.is_empty() {
4046            None
4047        } else {
4048            Some(LanceBuffer::reinterpret_vec(rep_index))
4049        };
4050        Ok(CompressedLevels {
4051            data: level_chunks,
4052            compression: compressor_desc,
4053            rep_index,
4054        })
4055    }
4056
4057    fn encode_simple_all_null(
4058        column_idx: u32,
4059        num_rows: u64,
4060        row_number: u64,
4061    ) -> Result<EncodedPage> {
4062        let description =
4063            ProtobufUtils21::constant_layout(&[DefinitionInterpretation::NullableItem], None);
4064        Ok(EncodedPage {
4065            column_idx,
4066            data: vec![],
4067            description: PageEncoding::Structural(description),
4068            num_rows,
4069            row_number,
4070        })
4071    }
4072
4073    fn encode_complex_all_null_vals(
4074        data: &Arc<[u16]>,
4075        compression_strategy: &dyn CompressionStrategy,
4076    ) -> Result<(LanceBuffer, pb21::CompressiveEncoding)> {
4077        let buffer = LanceBuffer::reinterpret_slice(data.clone());
4078        let mut fixed_width_block = FixedWidthDataBlock {
4079            data: buffer,
4080            bits_per_value: 16,
4081            num_values: data.len() as u64,
4082            block_info: BlockInfo::new(),
4083        };
4084        fixed_width_block.compute_stat();
4085
4086        let levels_block = DataBlock::FixedWidth(fixed_width_block);
4087        let levels_field = Field::new_arrow("", DataType::UInt16, false)?;
4088        let (compressor, encoding) =
4089            compression_strategy.create_block_compressor(&levels_field, &levels_block)?;
4090        let compressed_buffer = compressor.compress(levels_block)?;
4091        Ok((compressed_buffer, encoding))
4092    }
4093
4094    // Encodes a page where all values are null but we have rep/def
4095    // information that we need to store (e.g. to distinguish between
4096    // different kinds of null)
4097    fn encode_complex_all_null(
4098        column_idx: u32,
4099        repdef: crate::repdef::SerializedRepDefs,
4100        row_number: u64,
4101        num_rows: u64,
4102        version: LanceFileVersion,
4103        compression_strategy: &dyn CompressionStrategy,
4104    ) -> Result<EncodedPage> {
4105        if version.resolve() < LanceFileVersion::V2_2 {
4106            let rep_bytes = if let Some(rep) = repdef.repetition_levels.as_ref() {
4107                LanceBuffer::reinterpret_slice(rep.clone())
4108            } else {
4109                LanceBuffer::empty()
4110            };
4111
4112            let def_bytes = if let Some(def) = repdef.definition_levels.as_ref() {
4113                LanceBuffer::reinterpret_slice(def.clone())
4114            } else {
4115                LanceBuffer::empty()
4116            };
4117
4118            let description = ProtobufUtils21::constant_layout(&repdef.def_meaning, None);
4119            return Ok(EncodedPage {
4120                column_idx,
4121                data: vec![rep_bytes, def_bytes],
4122                description: PageEncoding::Structural(description),
4123                num_rows,
4124                row_number,
4125            });
4126        }
4127
4128        let (rep_bytes, rep_encoding, num_rep_values) = if let Some(rep) =
4129            repdef.repetition_levels.as_ref()
4130        {
4131            let num_values = rep.len() as u64;
4132            let (buffer, encoding) = Self::encode_complex_all_null_vals(rep, compression_strategy)?;
4133            (buffer, Some(encoding), num_values)
4134        } else {
4135            (LanceBuffer::empty(), None, 0)
4136        };
4137
4138        let (def_bytes, def_encoding, num_def_values) = if let Some(def) =
4139            repdef.definition_levels.as_ref()
4140        {
4141            let num_values = def.len() as u64;
4142            let (buffer, encoding) = Self::encode_complex_all_null_vals(def, compression_strategy)?;
4143            (buffer, Some(encoding), num_values)
4144        } else {
4145            (LanceBuffer::empty(), None, 0)
4146        };
4147
4148        let description = ProtobufUtils21::compressed_all_null_constant_layout(
4149            &repdef.def_meaning,
4150            rep_encoding,
4151            def_encoding,
4152            num_rep_values,
4153            num_def_values,
4154        );
4155        Ok(EncodedPage {
4156            column_idx,
4157            data: vec![rep_bytes, def_bytes],
4158            description: PageEncoding::Structural(description),
4159            num_rows,
4160            row_number,
4161        })
4162    }
4163
4164    fn leaf_validity(
4165        repdef: &crate::repdef::SerializedRepDefs,
4166        num_values: usize,
4167    ) -> Result<Option<BooleanBuffer>> {
4168        let rep = repdef
4169            .repetition_levels
4170            .as_ref()
4171            .map(|rep| rep.as_ref().to_vec());
4172        let def = repdef
4173            .definition_levels
4174            .as_ref()
4175            .map(|def| def.as_ref().to_vec());
4176        let mut unraveler = RepDefUnraveler::new(
4177            rep,
4178            def,
4179            repdef.def_meaning.clone().into(),
4180            num_values as u64,
4181        );
4182        if unraveler.is_all_valid() {
4183            return Ok(None);
4184        }
4185        let mut validity = BooleanBufferBuilder::new(num_values);
4186        unraveler.unravel_validity(&mut validity);
4187        Ok(Some(validity.finish()))
4188    }
4189
4190    fn is_constant_values(
4191        arrays: &[ArrayRef],
4192        scalar: &ArrayRef,
4193        validity: Option<&BooleanBuffer>,
4194    ) -> Result<bool> {
4195        debug_assert_eq!(scalar.len(), 1);
4196        debug_assert_eq!(scalar.null_count(), 0);
4197
4198        match scalar.data_type() {
4199            DataType::Boolean => {
4200                let mut global_idx = 0usize;
4201                let scalar_val = scalar.as_boolean().value(0);
4202                for arr in arrays {
4203                    let bool_arr = arr.as_boolean();
4204                    for i in 0..arr.len() {
4205                        let is_valid = validity.map(|v| v.value(global_idx)).unwrap_or(true);
4206                        global_idx += 1;
4207                        if !is_valid {
4208                            continue;
4209                        }
4210                        if bool_arr.value(i) != scalar_val {
4211                            return Ok(false);
4212                        }
4213                    }
4214                }
4215                Ok(true)
4216            }
4217            DataType::Utf8 => Self::is_constant_utf8::<i32>(arrays, scalar, validity),
4218            DataType::LargeUtf8 => Self::is_constant_utf8::<i64>(arrays, scalar, validity),
4219            DataType::Binary => Self::is_constant_binary::<i32>(arrays, scalar, validity),
4220            DataType::LargeBinary => Self::is_constant_binary::<i64>(arrays, scalar, validity),
4221            data_type => {
4222                let mut global_idx = 0usize;
4223                let Some(byte_width) = data_type.byte_width_opt() else {
4224                    return Ok(false);
4225                };
4226                let scalar_data = scalar.to_data();
4227                if scalar_data.buffers().len() != 1 || !scalar_data.child_data().is_empty() {
4228                    return Ok(false);
4229                }
4230                let scalar_bytes = scalar_data.buffers()[0].as_slice();
4231                if scalar_bytes.len() != byte_width {
4232                    return Ok(false);
4233                }
4234
4235                for arr in arrays {
4236                    let data = arr.to_data();
4237                    if data.buffers().is_empty() {
4238                        return Ok(false);
4239                    }
4240                    let buf = data.buffers()[0].as_slice();
4241                    let base = data.offset();
4242                    for i in 0..arr.len() {
4243                        let is_valid = validity.map(|v| v.value(global_idx)).unwrap_or(true);
4244                        global_idx += 1;
4245                        if !is_valid {
4246                            continue;
4247                        }
4248                        let start = (base + i) * byte_width;
4249                        if buf[start..start + byte_width] != scalar_bytes[..] {
4250                            return Ok(false);
4251                        }
4252                    }
4253                }
4254                Ok(true)
4255            }
4256        }
4257    }
4258
4259    fn is_constant_utf8<O: arrow_array::OffsetSizeTrait>(
4260        arrays: &[ArrayRef],
4261        scalar: &ArrayRef,
4262        validity: Option<&BooleanBuffer>,
4263    ) -> Result<bool> {
4264        debug_assert_eq!(scalar.len(), 1);
4265        let scalar_val = scalar.as_string::<O>().value(0).as_bytes();
4266        let mut global_idx = 0usize;
4267        for arr in arrays {
4268            let str_arr = arr.as_string::<O>();
4269            for i in 0..arr.len() {
4270                let is_valid = validity.map(|v| v.value(global_idx)).unwrap_or(true);
4271                global_idx += 1;
4272                if !is_valid {
4273                    continue;
4274                }
4275                if str_arr.value(i).as_bytes() != scalar_val {
4276                    return Ok(false);
4277                }
4278            }
4279        }
4280        Ok(true)
4281    }
4282
4283    fn is_constant_binary<O: arrow_array::OffsetSizeTrait>(
4284        arrays: &[ArrayRef],
4285        scalar: &ArrayRef,
4286        validity: Option<&BooleanBuffer>,
4287    ) -> Result<bool> {
4288        debug_assert_eq!(scalar.len(), 1);
4289        let scalar_val = scalar.as_binary::<O>().value(0);
4290        let mut global_idx = 0usize;
4291        for arr in arrays {
4292            let bin_arr = arr.as_binary::<O>();
4293            for i in 0..arr.len() {
4294                let is_valid = validity.map(|v| v.value(global_idx)).unwrap_or(true);
4295                global_idx += 1;
4296                if !is_valid {
4297                    continue;
4298                }
4299                if bin_arr.value(i) != scalar_val {
4300                    return Ok(false);
4301                }
4302            }
4303        }
4304        Ok(true)
4305    }
4306
4307    fn find_constant_scalar(
4308        arrays: &[ArrayRef],
4309        validity: Option<&BooleanBuffer>,
4310    ) -> Result<Option<ArrayRef>> {
4311        if arrays.is_empty() {
4312            return Ok(None);
4313        }
4314
4315        let global_scalar_idx = if let Some(validity) = validity {
4316            let Some(idx) = (0..validity.len()).find(|&i| validity.value(i)) else {
4317                return Ok(None);
4318            };
4319            idx
4320        } else {
4321            0
4322        };
4323
4324        let mut idx_remaining = global_scalar_idx;
4325        let mut scalar_arr_idx = 0usize;
4326        while scalar_arr_idx < arrays.len() {
4327            let len = arrays[scalar_arr_idx].len();
4328            if idx_remaining < len {
4329                break;
4330            }
4331            idx_remaining -= len;
4332            scalar_arr_idx += 1;
4333        }
4334
4335        if scalar_arr_idx >= arrays.len() {
4336            return Ok(None);
4337        }
4338
4339        let scalar =
4340            lance_arrow::scalar::extract_scalar_value(&arrays[scalar_arr_idx], idx_remaining)?;
4341        if scalar.null_count() != 0 {
4342            return Ok(None);
4343        }
4344        if !Self::is_constant_values(arrays, &scalar, validity)? {
4345            return Ok(None);
4346        }
4347        Ok(Some(scalar))
4348    }
4349
4350    fn resolve_dict_values_compression_metadata(
4351        field_metadata: &HashMap<String, String>,
4352        env_compression: Option<String>,
4353        env_compression_level: Option<String>,
4354    ) -> HashMap<String, String> {
4355        let mut metadata = HashMap::new();
4356
4357        let compression = field_metadata
4358            .get(DICT_VALUES_COMPRESSION_META_KEY)
4359            .cloned()
4360            .or(env_compression)
4361            .unwrap_or_else(|| DEFAULT_DICT_VALUES_COMPRESSION.to_string());
4362        metadata.insert(COMPRESSION_META_KEY.to_string(), compression);
4363
4364        if let Some(compression_level) = field_metadata
4365            .get(DICT_VALUES_COMPRESSION_LEVEL_META_KEY)
4366            .cloned()
4367            .or(env_compression_level)
4368        {
4369            metadata.insert(COMPRESSION_LEVEL_META_KEY.to_string(), compression_level);
4370        }
4371
4372        metadata
4373    }
4374
4375    fn build_dict_values_compressor_field(field: &Field) -> Result<Field> {
4376        // This is an internal synthetic field used only to feed metadata into
4377        // `create_block_compressor` for dictionary values. The concrete type/name here
4378        // are not semantically meaningful; we rely on explicit metadata below to control
4379        // general compression selection for dictionary values.
4380        let mut dict_values_field = Field::new_arrow("", DataType::UInt16, false)?;
4381        dict_values_field.metadata = Self::resolve_dict_values_compression_metadata(
4382            &field.metadata,
4383            env::var(DICT_VALUES_COMPRESSION_ENV_VAR).ok(),
4384            env::var(DICT_VALUES_COMPRESSION_LEVEL_ENV_VAR).ok(),
4385        );
4386        Ok(dict_values_field)
4387    }
4388
4389    #[allow(clippy::too_many_arguments)]
4390    fn encode_miniblock(
4391        column_idx: u32,
4392        field: &Field,
4393        compression_strategy: &dyn CompressionStrategy,
4394        data: DataBlock,
4395        repdef: crate::repdef::SerializedRepDefs,
4396        row_number: u64,
4397        dictionary_data: Option<DataBlock>,
4398        num_rows: u64,
4399        support_large_chunk: bool,
4400    ) -> Result<EncodedPage> {
4401        if let DataBlock::AllNull(_null_block) = data {
4402            // We should not be using mini-block for all-null.  There are other structural
4403            // encodings for that.
4404            unreachable!()
4405        }
4406
4407        let num_items = data.num_values();
4408
4409        let compressor = compression_strategy.create_miniblock_compressor(field, &data)?;
4410        let (compressed_data, value_encoding) = compressor.compress(data)?;
4411
4412        let max_rep = repdef.def_meaning.iter().filter(|l| l.is_list()).count() as u16;
4413
4414        let mut compressed_rep = repdef
4415            .rep_slicer()
4416            .map(|rep_slicer| {
4417                Self::compress_levels(
4418                    rep_slicer,
4419                    num_items,
4420                    compression_strategy,
4421                    &compressed_data.chunks,
4422                    max_rep,
4423                )
4424            })
4425            .transpose()?;
4426
4427        let (rep_index, rep_index_depth) =
4428            match compressed_rep.as_mut().and_then(|cr| cr.rep_index.as_mut()) {
4429                Some(rep_index) => (Some(rep_index.clone()), 1),
4430                None => (None, 0),
4431            };
4432
4433        let mut compressed_def = repdef
4434            .def_slicer()
4435            .map(|def_slicer| {
4436                Self::compress_levels(
4437                    def_slicer,
4438                    num_items,
4439                    compression_strategy,
4440                    &compressed_data.chunks,
4441                    /*max_rep=*/ 0,
4442                )
4443            })
4444            .transpose()?;
4445
4446        // TODO: Parquet sparsely encodes values here.  We could do the same but
4447        // then we won't have log2 values per chunk.  This means more metadata
4448        // and potentially more decoder asymmetry.  However, it may be worth
4449        // investigating at some point
4450
4451        let rep_data = compressed_rep
4452            .as_mut()
4453            .map(|cr| std::mem::take(&mut cr.data));
4454        let def_data = compressed_def
4455            .as_mut()
4456            .map(|cd| std::mem::take(&mut cd.data));
4457
4458        let serialized =
4459            Self::serialize_miniblocks(compressed_data, rep_data, def_data, support_large_chunk);
4460
4461        // Metadata, Data, Dictionary, (maybe) Repetition Index
4462        let mut data = Vec::with_capacity(4);
4463        data.push(serialized.metadata);
4464        data.push(serialized.data);
4465
4466        if let Some(dictionary_data) = dictionary_data {
4467            let num_dictionary_items = dictionary_data.num_values();
4468            let dict_values_field = Self::build_dict_values_compressor_field(field)?;
4469
4470            let (compressor, dictionary_encoding) = compression_strategy
4471                .create_block_compressor(&dict_values_field, &dictionary_data)?;
4472            let dictionary_buffer = compressor.compress(dictionary_data)?;
4473
4474            data.push(dictionary_buffer);
4475            if let Some(rep_index) = rep_index {
4476                data.push(rep_index);
4477            }
4478
4479            let description = ProtobufUtils21::miniblock_layout(
4480                compressed_rep.map(|cr| cr.compression),
4481                compressed_def.map(|cd| cd.compression),
4482                value_encoding,
4483                rep_index_depth,
4484                serialized.num_buffers,
4485                Some((dictionary_encoding, num_dictionary_items)),
4486                &repdef.def_meaning,
4487                num_items,
4488                support_large_chunk,
4489            );
4490            Ok(EncodedPage {
4491                num_rows,
4492                column_idx,
4493                data,
4494                description: PageEncoding::Structural(description),
4495                row_number,
4496            })
4497        } else {
4498            let description = ProtobufUtils21::miniblock_layout(
4499                compressed_rep.map(|cr| cr.compression),
4500                compressed_def.map(|cd| cd.compression),
4501                value_encoding,
4502                rep_index_depth,
4503                serialized.num_buffers,
4504                None,
4505                &repdef.def_meaning,
4506                num_items,
4507                support_large_chunk,
4508            );
4509
4510            if let Some(rep_index) = rep_index {
4511                let view = rep_index.borrow_to_typed_slice::<u64>();
4512                let total = view.chunks_exact(2).map(|c| c[0]).sum::<u64>();
4513                debug_assert_eq!(total, num_rows);
4514
4515                data.push(rep_index);
4516            }
4517
4518            Ok(EncodedPage {
4519                num_rows,
4520                column_idx,
4521                data,
4522                description: PageEncoding::Structural(description),
4523                row_number,
4524            })
4525        }
4526    }
4527
4528    // For fixed-size data we encode < control word | data > for each value
4529    fn serialize_full_zip_fixed(
4530        fixed: FixedWidthDataBlock,
4531        mut repdef: ControlWordIterator,
4532        num_values: u64,
4533    ) -> SerializedFullZip {
4534        let len = fixed.data.len() + repdef.bytes_per_word() * num_values as usize;
4535        let mut zipped_data = Vec::with_capacity(len);
4536
4537        let max_rep_index_val = if repdef.has_repetition() {
4538            len as u64
4539        } else {
4540            // Setting this to 0 means we won't write a repetition index
4541            0
4542        };
4543        let mut rep_index_builder =
4544            BytepackedIntegerEncoder::with_capacity(num_values as usize + 1, max_rep_index_val);
4545
4546        // I suppose we can just pad to the nearest byte but I'm not sure we need to worry about this anytime soon
4547        // because it is unlikely compression of large values is going to yield a result that is not byte aligned
4548        assert_eq!(
4549            fixed.bits_per_value % 8,
4550            0,
4551            "Non-byte aligned full-zip compression not yet supported"
4552        );
4553
4554        let bytes_per_value = fixed.bits_per_value as usize / 8;
4555        let mut offset = 0;
4556
4557        if bytes_per_value == 0 {
4558            // No data, just dump the repdef into the buffer
4559            while let Some(control) = repdef.append_next(&mut zipped_data) {
4560                if control.is_new_row {
4561                    // We have finished a row
4562                    debug_assert!(offset <= len);
4563                    // SAFETY: We know that `start <= len`
4564                    unsafe { rep_index_builder.append(offset as u64) };
4565                }
4566                offset = zipped_data.len();
4567            }
4568        } else {
4569            // We have data, zip it with the repdef
4570            let mut data_iter = fixed.data.chunks_exact(bytes_per_value);
4571            while let Some(control) = repdef.append_next(&mut zipped_data) {
4572                if control.is_new_row {
4573                    // We have finished a row
4574                    debug_assert!(offset <= len);
4575                    // SAFETY: We know that `start <= len`
4576                    unsafe { rep_index_builder.append(offset as u64) };
4577                }
4578                if control.is_visible {
4579                    let value = data_iter.next().unwrap();
4580                    zipped_data.extend_from_slice(value);
4581                }
4582                offset = zipped_data.len();
4583            }
4584        }
4585
4586        debug_assert_eq!(zipped_data.len(), len);
4587        // Put the final value in the rep index
4588        // SAFETY: `zipped_data.len() == len`
4589        unsafe {
4590            rep_index_builder.append(zipped_data.len() as u64);
4591        }
4592
4593        let zipped_data = LanceBuffer::from(zipped_data);
4594        let rep_index = rep_index_builder.into_data();
4595        let rep_index = if rep_index.is_empty() {
4596            None
4597        } else {
4598            Some(LanceBuffer::from(rep_index))
4599        };
4600        SerializedFullZip {
4601            values: zipped_data,
4602            repetition_index: rep_index,
4603        }
4604    }
4605
4606    // For variable-size data we encode < control word | length | data > for each value
4607    //
4608    // In addition, we create a second buffer, the repetition index
4609    fn serialize_full_zip_variable(
4610        variable: VariableWidthBlock,
4611        mut repdef: ControlWordIterator,
4612        num_items: u64,
4613    ) -> SerializedFullZip {
4614        let bytes_per_offset = variable.bits_per_offset as usize / 8;
4615        assert_eq!(
4616            variable.bits_per_offset % 8,
4617            0,
4618            "Only byte-aligned offsets supported"
4619        );
4620        let len = variable.data.len()
4621            + repdef.bytes_per_word() * num_items as usize
4622            + bytes_per_offset * variable.num_values as usize;
4623        let mut buf = Vec::with_capacity(len);
4624
4625        let max_rep_index_val = len as u64;
4626        let mut rep_index_builder =
4627            BytepackedIntegerEncoder::with_capacity(num_items as usize + 1, max_rep_index_val);
4628
4629        // TODO: byte pack the item lengths with varint encoding
4630        match bytes_per_offset {
4631            4 => {
4632                let offs = variable.offsets.borrow_to_typed_slice::<u32>();
4633                let mut rep_offset = 0;
4634                let mut windows_iter = offs.as_ref().windows(2);
4635                while let Some(control) = repdef.append_next(&mut buf) {
4636                    if control.is_new_row {
4637                        // We have finished a row
4638                        debug_assert!(rep_offset <= len);
4639                        // SAFETY: We know that `buf.len() <= len`
4640                        unsafe { rep_index_builder.append(rep_offset as u64) };
4641                    }
4642                    if control.is_visible {
4643                        let window = windows_iter.next().unwrap();
4644                        if control.is_valid_item {
4645                            buf.extend_from_slice(&(window[1] - window[0]).to_le_bytes());
4646                            buf.extend_from_slice(
4647                                &variable.data[window[0] as usize..window[1] as usize],
4648                            );
4649                        }
4650                    }
4651                    rep_offset = buf.len();
4652                }
4653            }
4654            8 => {
4655                let offs = variable.offsets.borrow_to_typed_slice::<u64>();
4656                let mut rep_offset = 0;
4657                let mut windows_iter = offs.as_ref().windows(2);
4658                while let Some(control) = repdef.append_next(&mut buf) {
4659                    if control.is_new_row {
4660                        // We have finished a row
4661                        debug_assert!(rep_offset <= len);
4662                        // SAFETY: We know that `buf.len() <= len`
4663                        unsafe { rep_index_builder.append(rep_offset as u64) };
4664                    }
4665                    if control.is_visible {
4666                        let window = windows_iter.next().unwrap();
4667                        if control.is_valid_item {
4668                            buf.extend_from_slice(&(window[1] - window[0]).to_le_bytes());
4669                            buf.extend_from_slice(
4670                                &variable.data[window[0] as usize..window[1] as usize],
4671                            );
4672                        }
4673                    }
4674                    rep_offset = buf.len();
4675                }
4676            }
4677            _ => panic!("Unsupported offset size"),
4678        }
4679
4680        // We might have saved a few bytes by not copying lengths when the length was zero.  However,
4681        // if we are over `len` then we have a bug.
4682        debug_assert!(buf.len() <= len);
4683        // Put the final value in the rep index
4684        // SAFETY: `zipped_data.len() == len`
4685        unsafe {
4686            rep_index_builder.append(buf.len() as u64);
4687        }
4688
4689        let zipped_data = LanceBuffer::from(buf);
4690        let rep_index = rep_index_builder.into_data();
4691        debug_assert!(!rep_index.is_empty());
4692        let rep_index = Some(LanceBuffer::from(rep_index));
4693        SerializedFullZip {
4694            values: zipped_data,
4695            repetition_index: rep_index,
4696        }
4697    }
4698
4699    /// Serializes data into a single buffer according to the full-zip format which zips
4700    /// together the repetition, definition, and value data into a single buffer.
4701    fn serialize_full_zip(
4702        compressed_data: PerValueDataBlock,
4703        repdef: ControlWordIterator,
4704        num_items: u64,
4705    ) -> SerializedFullZip {
4706        match compressed_data {
4707            PerValueDataBlock::Fixed(fixed) => {
4708                Self::serialize_full_zip_fixed(fixed, repdef, num_items)
4709            }
4710            PerValueDataBlock::Variable(var) => {
4711                Self::serialize_full_zip_variable(var, repdef, num_items)
4712            }
4713        }
4714    }
4715
4716    fn encode_full_zip(
4717        column_idx: u32,
4718        field: &Field,
4719        compression_strategy: &dyn CompressionStrategy,
4720        data: DataBlock,
4721        repdef: crate::repdef::SerializedRepDefs,
4722        row_number: u64,
4723        num_lists: u64,
4724    ) -> Result<EncodedPage> {
4725        let max_rep = repdef
4726            .repetition_levels
4727            .as_ref()
4728            .map_or(0, |r| r.iter().max().copied().unwrap_or(0));
4729        let max_def = repdef
4730            .definition_levels
4731            .as_ref()
4732            .map_or(0, |d| d.iter().max().copied().unwrap_or(0));
4733
4734        // To handle FSL we just flatten
4735        // let data = data.flatten();
4736
4737        let (num_items, num_visible_items) =
4738            if let Some(rep_levels) = repdef.repetition_levels.as_ref() {
4739                // If there are rep levels there may be "invisible" items and we need to encode
4740                // rep_levels.len() things which might be larger than data.num_values()
4741                (rep_levels.len() as u64, data.num_values())
4742            } else {
4743                // If there are no rep levels then we encode data.num_values() things
4744                (data.num_values(), data.num_values())
4745            };
4746
4747        let max_visible_def = repdef.max_visible_level.unwrap_or(u16::MAX);
4748
4749        let repdef_iter = build_control_word_iterator(
4750            repdef.repetition_levels.as_deref(),
4751            max_rep,
4752            repdef.definition_levels.as_deref(),
4753            max_def,
4754            max_visible_def,
4755            num_items as usize,
4756        );
4757        let bits_rep = repdef_iter.bits_rep();
4758        let bits_def = repdef_iter.bits_def();
4759
4760        let compressor = compression_strategy.create_per_value(field, &data)?;
4761        let (compressed_data, value_encoding) = compressor.compress(data)?;
4762
4763        let description = match &compressed_data {
4764            PerValueDataBlock::Fixed(fixed) => ProtobufUtils21::fixed_full_zip_layout(
4765                bits_rep,
4766                bits_def,
4767                fixed.bits_per_value as u32,
4768                value_encoding,
4769                &repdef.def_meaning,
4770                num_items as u32,
4771                num_visible_items as u32,
4772            ),
4773            PerValueDataBlock::Variable(variable) => ProtobufUtils21::variable_full_zip_layout(
4774                bits_rep,
4775                bits_def,
4776                variable.bits_per_offset as u32,
4777                value_encoding,
4778                &repdef.def_meaning,
4779                num_items as u32,
4780                num_visible_items as u32,
4781            ),
4782        };
4783
4784        let zipped = Self::serialize_full_zip(compressed_data, repdef_iter, num_items);
4785
4786        let data = if let Some(repindex) = zipped.repetition_index {
4787            vec![zipped.values, repindex]
4788        } else {
4789            vec![zipped.values]
4790        };
4791
4792        Ok(EncodedPage {
4793            num_rows: num_lists,
4794            column_idx,
4795            data,
4796            description: PageEncoding::Structural(description),
4797            row_number,
4798        })
4799    }
4800
4801    fn should_dictionary_encode(
4802        data_block: &DataBlock,
4803        field: &Field,
4804        version: LanceFileVersion,
4805    ) -> Option<DictEncodingBudget> {
4806        const DEFAULT_SAMPLE_SIZE: usize = 4096;
4807        const DEFAULT_SAMPLE_UNIQUE_RATIO: f64 = 0.98;
4808
4809        // Since we only dictionary encode FixedWidth and VariableWidth blocks for now, we skip
4810        // estimating the size for other types.
4811        match data_block {
4812            DataBlock::FixedWidth(fixed) => {
4813                if fixed.bits_per_value == 64 && version < LanceFileVersion::V2_2 {
4814                    return None;
4815                }
4816                if fixed.bits_per_value != 64 && fixed.bits_per_value != 128 {
4817                    return None;
4818                }
4819                if fixed.bits_per_value % 8 != 0 {
4820                    return None;
4821                }
4822            }
4823            DataBlock::VariableWidth(var) => {
4824                if var.bits_per_offset != 32 && var.bits_per_offset != 64 {
4825                    return None;
4826                }
4827            }
4828            _ => return None,
4829        }
4830
4831        // Don't dictionary encode tiny arrays.
4832        let too_small = env::var("LANCE_ENCODING_DICT_TOO_SMALL")
4833            .ok()
4834            .and_then(|val| val.parse().ok())
4835            .unwrap_or(100);
4836        if data_block.num_values() < too_small {
4837            return None;
4838        }
4839
4840        let num_values = data_block.num_values();
4841
4842        // Apply divisor threshold and cap. This is intentionally conservative: the goal is to
4843        // avoid spending too much CPU trying to estimate very high cardinalities.
4844        let divisor: u64 = field
4845            .metadata
4846            .get(DICT_DIVISOR_META_KEY)
4847            .and_then(|val| val.parse().ok())
4848            .or_else(|| {
4849                env::var("LANCE_ENCODING_DICT_DIVISOR")
4850                    .ok()
4851                    .and_then(|val| val.parse().ok())
4852            })
4853            .unwrap_or(DEFAULT_DICT_DIVISOR);
4854
4855        let max_cardinality: u64 = env::var("LANCE_ENCODING_DICT_MAX_CARDINALITY")
4856            .ok()
4857            .and_then(|val| val.parse().ok())
4858            .unwrap_or(DEFAULT_DICT_MAX_CARDINALITY);
4859
4860        let threshold_cardinality = num_values
4861            .checked_div(divisor.max(1))
4862            .unwrap_or(0)
4863            .min(max_cardinality);
4864        if threshold_cardinality == 0 {
4865            return None;
4866        }
4867
4868        // Get size ratio from metadata or env var.
4869        let threshold_ratio = field
4870            .metadata
4871            .get(DICT_SIZE_RATIO_META_KEY)
4872            .and_then(|val| val.parse::<f64>().ok())
4873            .or_else(|| {
4874                env::var("LANCE_ENCODING_DICT_SIZE_RATIO")
4875                    .ok()
4876                    .and_then(|val| val.parse().ok())
4877            })
4878            .unwrap_or(DEFAULT_DICT_SIZE_RATIO);
4879
4880        if threshold_ratio <= 0.0 || threshold_ratio > 1.0 {
4881            panic!(
4882                "Invalid parameter: dict-size-ratio is {} which is not in the range (0, 1].",
4883                threshold_ratio
4884            );
4885        }
4886
4887        let data_size = data_block.data_size();
4888        if data_size == 0 {
4889            return None;
4890        }
4891
4892        let max_encoded_size = (data_size as f64 * threshold_ratio) as u64;
4893        let max_encoded_size = usize::try_from(max_encoded_size).ok()?;
4894
4895        // Avoid probing dictionary encoding on data that appears to be near-unique.
4896        if Self::sample_is_near_unique(
4897            data_block,
4898            DEFAULT_SAMPLE_SIZE,
4899            DEFAULT_SAMPLE_UNIQUE_RATIO,
4900        )? {
4901            return None;
4902        }
4903
4904        let max_dict_entries = u32::try_from(threshold_cardinality.min(i32::MAX as u64)).ok()?;
4905        Some(DictEncodingBudget {
4906            max_dict_entries,
4907            max_encoded_size,
4908        })
4909    }
4910
4911    /// Probe whether a page looks near-unique before attempting dictionary encoding.
4912    ///
4913    /// The probe uses deterministic stride sampling (not RNG sampling), which keeps
4914    /// the check cheap and reproducible across runs. The result is only a gate for
4915    /// whether we try dictionary encoding, not a cardinality statistic.
4916    fn sample_is_near_unique(
4917        data_block: &DataBlock,
4918        max_samples: usize,
4919        unique_ratio_threshold: f64,
4920    ) -> Option<bool> {
4921        use std::collections::HashSet;
4922
4923        if unique_ratio_threshold <= 0.0 || unique_ratio_threshold > 1.0 {
4924            return None;
4925        }
4926
4927        let num_values = usize::try_from(data_block.num_values()).ok()?;
4928        if num_values == 0 {
4929            return Some(false);
4930        }
4931
4932        let sample_count = num_values.min(max_samples).max(1);
4933        // Uniform stride sampling across the page.
4934        let step = (num_values / sample_count).max(1);
4935
4936        match data_block {
4937            DataBlock::FixedWidth(fixed) => match fixed.bits_per_value {
4938                64 => {
4939                    let values = fixed.data.borrow_to_typed_slice::<u64>();
4940                    let values = values.as_ref();
4941                    let mut unique: HashSet<u64> = HashSet::with_capacity(sample_count.min(1024));
4942                    for idx in (0..num_values).step_by(step).take(sample_count) {
4943                        unique.insert(values.get(idx).copied()?);
4944                    }
4945                    let ratio = unique.len() as f64 / sample_count as f64;
4946                    // Avoid overreacting to tiny pages with too few samples.
4947                    Some(sample_count >= 1024 && ratio >= unique_ratio_threshold)
4948                }
4949                128 => {
4950                    let values = fixed.data.borrow_to_typed_slice::<u128>();
4951                    let values = values.as_ref();
4952                    let mut unique: HashSet<u128> = HashSet::with_capacity(sample_count.min(1024));
4953                    for idx in (0..num_values).step_by(step).take(sample_count) {
4954                        unique.insert(values.get(idx).copied()?);
4955                    }
4956                    let ratio = unique.len() as f64 / sample_count as f64;
4957                    Some(sample_count >= 1024 && ratio >= unique_ratio_threshold)
4958                }
4959                _ => Some(false),
4960            },
4961            DataBlock::VariableWidth(var) => {
4962                use xxhash_rust::xxh3::xxh3_64;
4963
4964                // Hash variable-width slices instead of storing borrowed slice keys.
4965                let mut unique: HashSet<u64> = HashSet::with_capacity(sample_count.min(1024));
4966                match var.bits_per_offset {
4967                    32 => {
4968                        let offsets_ref = var.offsets.borrow_to_typed_slice::<u32>();
4969                        let offsets: &[u32] = offsets_ref.as_ref();
4970                        for i in (0..num_values).step_by(step).take(sample_count) {
4971                            let start = usize::try_from(*offsets.get(i)?).ok()?;
4972                            let end = usize::try_from(*offsets.get(i + 1)?).ok()?;
4973                            if start > end || end > var.data.len() {
4974                                return None;
4975                            }
4976                            unique.insert(xxh3_64(&var.data[start..end]));
4977                        }
4978                    }
4979                    64 => {
4980                        let offsets_ref = var.offsets.borrow_to_typed_slice::<u64>();
4981                        let offsets: &[u64] = offsets_ref.as_ref();
4982                        for i in (0..num_values).step_by(step).take(sample_count) {
4983                            let start = usize::try_from(*offsets.get(i)?).ok()?;
4984                            let end = usize::try_from(*offsets.get(i + 1)?).ok()?;
4985                            if start > end || end > var.data.len() {
4986                                return None;
4987                            }
4988                            unique.insert(xxh3_64(&var.data[start..end]));
4989                        }
4990                    }
4991                    _ => return Some(false),
4992                }
4993                let ratio = unique.len() as f64 / sample_count as f64;
4994                Some(sample_count >= 1024 && ratio >= unique_ratio_threshold)
4995            }
4996            _ => Some(false),
4997        }
4998    }
4999
5000    // Creates an encode task, consuming all buffered data
5001    fn do_flush(
5002        &mut self,
5003        arrays: Vec<ArrayRef>,
5004        repdefs: Vec<RepDefBuilder>,
5005        row_number: u64,
5006        num_rows: u64,
5007    ) -> Result<Vec<EncodeTask>> {
5008        let column_idx = self.column_index;
5009        let compression_strategy = self.compression_strategy.clone();
5010        let field = self.field.clone();
5011        let encoding_metadata = self.encoding_metadata.clone();
5012        let support_large_chunk = self.support_large_chunk;
5013        let version = self.version;
5014        let task = spawn_cpu(move || {
5015            let num_values = arrays.iter().map(|arr| arr.len() as u64).sum();
5016            let is_simple_validity = repdefs.iter().all(|rd| rd.is_simple_validity());
5017            let has_repdef_info = repdefs.iter().any(|rd| !rd.is_empty());
5018            let repdef = RepDefBuilder::serialize(repdefs);
5019
5020            if num_values == 0 {
5021                // We should not encode empty arrays.  So if we get here that should mean that we
5022                // either have all empty lists or all null lists (or a mix).  We still need to encode
5023                // the rep/def information but we can skip the data encoding.
5024                log::debug!("Encoding column {} with {} items ({} rows) using complex-null layout", column_idx, num_values, num_rows);
5025                return Self::encode_complex_all_null(
5026                    column_idx,
5027                    repdef,
5028                    row_number,
5029                    num_rows,
5030                    version,
5031                    compression_strategy.as_ref(),
5032                );
5033            }
5034
5035            let leaf_validity = Self::leaf_validity(&repdef, num_values as usize)?;
5036            let all_null = leaf_validity
5037                .as_ref()
5038                .map(|validity| validity.count_set_bits() == 0)
5039                .unwrap_or(false);
5040
5041            if all_null {
5042                return if is_simple_validity {
5043                    log::debug!(
5044                        "Encoding column {} with {} items ({} rows) using simple-null layout",
5045                        column_idx,
5046                        num_values,
5047                        num_rows
5048                    );
5049                    Self::encode_simple_all_null(column_idx, num_values, row_number)
5050                } else {
5051                    log::debug!(
5052                        "Encoding column {} with {} items ({} rows) using complex-null layout",
5053                        column_idx,
5054                        num_values,
5055                        num_rows
5056                    );
5057                    Self::encode_complex_all_null(
5058                        column_idx,
5059                        repdef,
5060                        row_number,
5061                        num_rows,
5062                        version,
5063                        compression_strategy.as_ref(),
5064                    )
5065                };
5066            }
5067
5068            if let DataType::Struct(fields) = &field.data_type()
5069                && fields.is_empty()
5070            {
5071                if has_repdef_info {
5072                    return Err(Error::invalid_input_source(format!("Empty structs with rep/def information are not yet supported.  The field {} is an empty struct that either has nulls or is in a list.", field.name).into()));
5073                }
5074                // This is maybe a little confusing but the reader should never look at this anyways and it
5075                // seems like overkill to invent a new layout just for "empty structs".
5076                return Self::encode_simple_all_null(column_idx, num_values, row_number);
5077            }
5078
5079            let data_block = DataBlock::from_arrays(&arrays, num_values);
5080
5081            if version.resolve() >= LanceFileVersion::V2_2
5082                && let Some(scalar) = Self::find_constant_scalar(&arrays, leaf_validity.as_ref())?
5083            {
5084                log::debug!(
5085                    "Encoding column {} with {} items ({} rows) using constant layout",
5086                    column_idx,
5087                    num_values,
5088                    num_rows
5089                );
5090                return constant::encode_constant_page(
5091                    column_idx,
5092                    scalar,
5093                    repdef,
5094                    row_number,
5095                    num_rows,
5096                );
5097            }
5098
5099            let requires_full_zip_packed_struct =
5100                if let DataBlock::Struct(ref struct_data_block) = data_block {
5101                    struct_data_block.has_variable_width_child()
5102                } else {
5103                    false
5104                };
5105
5106            if requires_full_zip_packed_struct {
5107                log::debug!(
5108                    "Encoding column {} with {} items using full-zip packed struct layout",
5109                    column_idx,
5110                    num_values
5111                );
5112                return Self::encode_full_zip(
5113                    column_idx,
5114                    &field,
5115                    compression_strategy.as_ref(),
5116                    data_block,
5117                    repdef,
5118                    row_number,
5119                    num_rows,
5120                );
5121            }
5122
5123            if let DataBlock::Dictionary(dict) = data_block {
5124                log::debug!("Encoding column {} with {} items using dictionary encoding (already dictionary encoded)", column_idx, num_values);
5125                let (mut indices_data_block, dictionary_data_block) = dict.into_parts();
5126                // TODO: https://github.com/lancedb/lance/issues/4809
5127                // If we compute stats on dictionary_data_block => panic.
5128                // If we don't compute stats on indices_data_block => panic.
5129                // This is messy.  Don't make me call compute_stat ever.
5130                indices_data_block.compute_stat();
5131                Self::encode_miniblock(
5132                    column_idx,
5133                    &field,
5134                    compression_strategy.as_ref(),
5135                    indices_data_block,
5136                    repdef,
5137                    row_number,
5138                    Some(dictionary_data_block),
5139                    num_rows,
5140                    support_large_chunk,
5141                )
5142            } else {
5143                // Try dictionary encoding first if applicable. If encoding aborts, fall back to the
5144                // preferred structural encoding.
5145                let dict_result = Self::should_dictionary_encode(&data_block, &field, version)
5146                    .and_then(|budget| {
5147                        log::debug!(
5148                            "Encoding column {} with {} items using dictionary encoding (mini-block layout)",
5149                            column_idx,
5150                            num_values
5151                        );
5152                        dict::dictionary_encode(
5153                            &data_block,
5154                            budget.max_dict_entries,
5155                            budget.max_encoded_size,
5156                        )
5157                    });
5158
5159                if let Some((indices_data_block, dictionary_data_block)) = dict_result {
5160                    Self::encode_miniblock(
5161                        column_idx,
5162                        &field,
5163                        compression_strategy.as_ref(),
5164                        indices_data_block,
5165                        repdef,
5166                        row_number,
5167                        Some(dictionary_data_block),
5168                        num_rows,
5169                        support_large_chunk,
5170                    )
5171                } else if Self::prefers_miniblock(&data_block, encoding_metadata.as_ref()) {
5172                    log::debug!(
5173                        "Encoding column {} with {} items using mini-block layout",
5174                        column_idx,
5175                        num_values
5176                    );
5177                    Self::encode_miniblock(
5178                        column_idx,
5179                        &field,
5180                        compression_strategy.as_ref(),
5181                        data_block,
5182                        repdef,
5183                        row_number,
5184                        None,
5185                        num_rows,
5186                        support_large_chunk,
5187                    )
5188                } else if Self::prefers_fullzip(encoding_metadata.as_ref()) {
5189                    log::debug!(
5190                        "Encoding column {} with {} items using full-zip layout",
5191                        column_idx,
5192                        num_values
5193                    );
5194                    Self::encode_full_zip(
5195                        column_idx,
5196                        &field,
5197                        compression_strategy.as_ref(),
5198                        data_block,
5199                        repdef,
5200                        row_number,
5201                        num_rows,
5202                    )
5203                } else {
5204                    Err(Error::invalid_input_source(format!("Cannot determine structural encoding for field {}.  This typically indicates an invalid value of the field metadata key {}", field.name, STRUCTURAL_ENCODING_META_KEY).into()))
5205                }
5206            }
5207        })
5208        .boxed();
5209        Ok(vec![task])
5210    }
5211
5212    fn extract_validity_buf(
5213        array: Arc<dyn Array>,
5214        repdef: &mut RepDefBuilder,
5215        keep_original_array: bool,
5216    ) -> Result<Arc<dyn Array>> {
5217        if let Some(validity) = array.nulls() {
5218            if keep_original_array {
5219                repdef.add_validity_bitmap(validity.clone());
5220            } else {
5221                repdef.add_validity_bitmap(deep_copy_nulls(Some(validity)).unwrap());
5222            }
5223            let data_no_nulls = array.to_data().into_builder().nulls(None).build()?;
5224            Ok(make_array(data_no_nulls))
5225        } else {
5226            repdef.add_no_null(array.len());
5227            Ok(array)
5228        }
5229    }
5230
5231    fn extract_validity(
5232        mut array: Arc<dyn Array>,
5233        repdef: &mut RepDefBuilder,
5234        keep_original_array: bool,
5235    ) -> Result<Arc<dyn Array>> {
5236        match array.data_type() {
5237            DataType::Null => {
5238                repdef.add_validity_bitmap(NullBuffer::new(BooleanBuffer::new_unset(array.len())));
5239                Ok(array)
5240            }
5241            DataType::Dictionary(_, _) => {
5242                array = dict::normalize_dict_nulls(array)?;
5243                Self::extract_validity_buf(array, repdef, keep_original_array)
5244            }
5245            // Extract our validity buf but NOT any child validity bufs. (they will be encoded in
5246            // as part of the values).  Note: for FSL we do not use repdef.add_fsl because we do
5247            // NOT want to increase the repdef depth.
5248            //
5249            // This would be quite catasrophic for something like vector embeddings.  Imagine we
5250            // had thousands of vectors and some were null but no vector contained null items.  If
5251            // we treated the vectors (primitive FSL) like we treat structural FSL we would end up
5252            // with a rep/def value for every single item in the vector.
5253            _ => Self::extract_validity_buf(array, repdef, keep_original_array),
5254        }
5255    }
5256}
5257
5258impl FieldEncoder for PrimitiveStructuralEncoder {
5259    // Buffers data, if there is enough to write a page then we create an encode task
5260    fn maybe_encode(
5261        &mut self,
5262        array: ArrayRef,
5263        _external_buffers: &mut OutOfLineBuffers,
5264        mut repdef: RepDefBuilder,
5265        row_number: u64,
5266        num_rows: u64,
5267    ) -> Result<Vec<EncodeTask>> {
5268        let array = Self::extract_validity(array, &mut repdef, self.keep_original_array)?;
5269        self.accumulated_repdefs.push(repdef);
5270
5271        if let Some((arrays, row_number, num_rows)) =
5272            self.accumulation_queue.insert(array, row_number, num_rows)
5273        {
5274            let accumulated_repdefs = std::mem::take(&mut self.accumulated_repdefs);
5275            Ok(self.do_flush(arrays, accumulated_repdefs, row_number, num_rows)?)
5276        } else {
5277            Ok(vec![])
5278        }
5279    }
5280
5281    // If there is any data left in the buffer then create an encode task from it
5282    fn flush(&mut self, _external_buffers: &mut OutOfLineBuffers) -> Result<Vec<EncodeTask>> {
5283        if let Some((arrays, row_number, num_rows)) = self.accumulation_queue.flush() {
5284            let accumulated_repdefs = std::mem::take(&mut self.accumulated_repdefs);
5285            Ok(self.do_flush(arrays, accumulated_repdefs, row_number, num_rows)?)
5286        } else {
5287            Ok(vec![])
5288        }
5289    }
5290
5291    fn num_columns(&self) -> u32 {
5292        1
5293    }
5294
5295    fn finish(
5296        &mut self,
5297        _external_buffers: &mut OutOfLineBuffers,
5298    ) -> BoxFuture<'_, Result<Vec<crate::encoder::EncodedColumn>>> {
5299        std::future::ready(Ok(vec![EncodedColumn::default()])).boxed()
5300    }
5301}
5302
5303#[cfg(test)]
5304#[allow(clippy::single_range_in_vec_init)]
5305mod tests {
5306    use super::{
5307        ChunkInstructions, DataBlock, DecodeMiniBlockTask, FixedPerValueDecompressor,
5308        FixedWidthDataBlock, FullZipCacheableState, FullZipDecodeDetails, FullZipReadSource,
5309        FullZipRepIndexDetails, FullZipScheduler, MiniBlockRepIndex, PerValueDecompressor,
5310        PreambleAction, StructuralPageScheduler, VariableFullZipDecoder,
5311    };
5312    use crate::buffer::LanceBuffer;
5313    use crate::compression::DefaultDecompressionStrategy;
5314    use crate::constants::{
5315        COMPRESSION_LEVEL_META_KEY, COMPRESSION_META_KEY, DICT_VALUES_COMPRESSION_LEVEL_META_KEY,
5316        DICT_VALUES_COMPRESSION_META_KEY, STRUCTURAL_ENCODING_META_KEY,
5317        STRUCTURAL_ENCODING_MINIBLOCK,
5318    };
5319    use crate::data::BlockInfo;
5320    use crate::decoder::PageEncoding;
5321    use crate::encodings::logical::primitive::{
5322        ChunkDrainInstructions, PrimitiveStructuralEncoder,
5323    };
5324    use crate::format::ProtobufUtils21;
5325    use crate::format::pb21;
5326    use crate::format::pb21::compressive_encoding::Compression;
5327    use crate::testing::{TestCases, check_round_trip_encoding_of_data};
5328    use crate::version::LanceFileVersion;
5329    use arrow_array::{ArrayRef, Int8Array, StringArray};
5330    use arrow_schema::DataType;
5331    use std::collections::HashMap;
5332    use std::{collections::VecDeque, sync::Arc};
5333
5334    #[test]
5335    fn test_is_narrow() {
5336        let int8_array = Int8Array::from(vec![1, 2, 3]);
5337        let array_ref: ArrayRef = Arc::new(int8_array);
5338        let block = DataBlock::from_array(array_ref);
5339
5340        assert!(PrimitiveStructuralEncoder::is_narrow(&block));
5341
5342        let string_array = StringArray::from(vec![Some("hello"), Some("world")]);
5343        let block = DataBlock::from_array(string_array);
5344        assert!(PrimitiveStructuralEncoder::is_narrow(&block));
5345
5346        let string_array = StringArray::from(vec![
5347            Some("hello world".repeat(100)),
5348            Some("world".to_string()),
5349        ]);
5350        let block = DataBlock::from_array(string_array);
5351        assert!((!PrimitiveStructuralEncoder::is_narrow(&block)));
5352    }
5353
5354    #[test]
5355    fn test_map_range() {
5356        // Null in the middle
5357        // [[A, B, C], [D, E], NULL, [F, G, H]]
5358        let rep = Some(vec![1, 0, 0, 1, 0, 1, 1, 0, 0]);
5359        let def = Some(vec![0, 0, 0, 0, 0, 1, 0, 0, 0]);
5360        let max_visible_def = 0;
5361        let total_items = 8;
5362        let max_rep = 1;
5363
5364        let check = |range, expected_item_range, expected_level_range| {
5365            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
5366                range,
5367                rep.as_ref(),
5368                def.as_ref(),
5369                max_rep,
5370                max_visible_def,
5371                total_items,
5372                PreambleAction::Absent,
5373            );
5374            assert_eq!(item_range, expected_item_range);
5375            assert_eq!(level_range, expected_level_range);
5376        };
5377
5378        check(0..1, 0..3, 0..3);
5379        check(1..2, 3..5, 3..5);
5380        check(2..3, 5..5, 5..6);
5381        check(3..4, 5..8, 6..9);
5382        check(0..2, 0..5, 0..5);
5383        check(1..3, 3..5, 3..6);
5384        check(2..4, 5..8, 5..9);
5385        check(0..3, 0..5, 0..6);
5386        check(1..4, 3..8, 3..9);
5387        check(0..4, 0..8, 0..9);
5388
5389        // Null at start
5390        // [NULL, [A, B], [C]]
5391        let rep = Some(vec![1, 1, 0, 1]);
5392        let def = Some(vec![1, 0, 0, 0]);
5393        let max_visible_def = 0;
5394        let total_items = 3;
5395
5396        let check = |range, expected_item_range, expected_level_range| {
5397            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
5398                range,
5399                rep.as_ref(),
5400                def.as_ref(),
5401                max_rep,
5402                max_visible_def,
5403                total_items,
5404                PreambleAction::Absent,
5405            );
5406            assert_eq!(item_range, expected_item_range);
5407            assert_eq!(level_range, expected_level_range);
5408        };
5409
5410        check(0..1, 0..0, 0..1);
5411        check(1..2, 0..2, 1..3);
5412        check(2..3, 2..3, 3..4);
5413        check(0..2, 0..2, 0..3);
5414        check(1..3, 0..3, 1..4);
5415        check(0..3, 0..3, 0..4);
5416
5417        // Null at end
5418        // [[A], [B, C], NULL]
5419        let rep = Some(vec![1, 1, 0, 1]);
5420        let def = Some(vec![0, 0, 0, 1]);
5421        let max_visible_def = 0;
5422        let total_items = 3;
5423
5424        let check = |range, expected_item_range, expected_level_range| {
5425            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
5426                range,
5427                rep.as_ref(),
5428                def.as_ref(),
5429                max_rep,
5430                max_visible_def,
5431                total_items,
5432                PreambleAction::Absent,
5433            );
5434            assert_eq!(item_range, expected_item_range);
5435            assert_eq!(level_range, expected_level_range);
5436        };
5437
5438        check(0..1, 0..1, 0..1);
5439        check(1..2, 1..3, 1..3);
5440        check(2..3, 3..3, 3..4);
5441        check(0..2, 0..3, 0..3);
5442        check(1..3, 1..3, 1..4);
5443        check(0..3, 0..3, 0..4);
5444
5445        // No nulls, with repetition
5446        // [[A, B], [C, D], [E, F]]
5447        let rep = Some(vec![1, 0, 1, 0, 1, 0]);
5448        let def: Option<&[u16]> = None;
5449        let max_visible_def = 0;
5450        let total_items = 6;
5451
5452        let check = |range, expected_item_range, expected_level_range| {
5453            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
5454                range,
5455                rep.as_ref(),
5456                def.as_ref(),
5457                max_rep,
5458                max_visible_def,
5459                total_items,
5460                PreambleAction::Absent,
5461            );
5462            assert_eq!(item_range, expected_item_range);
5463            assert_eq!(level_range, expected_level_range);
5464        };
5465
5466        check(0..1, 0..2, 0..2);
5467        check(1..2, 2..4, 2..4);
5468        check(2..3, 4..6, 4..6);
5469        check(0..2, 0..4, 0..4);
5470        check(1..3, 2..6, 2..6);
5471        check(0..3, 0..6, 0..6);
5472
5473        // No repetition, with nulls (this case is trivial)
5474        // [A, B, NULL, C]
5475        let rep: Option<&[u16]> = None;
5476        let def = Some(vec![0, 0, 1, 0]);
5477        let max_visible_def = 1;
5478        let total_items = 4;
5479
5480        let check = |range, expected_item_range, expected_level_range| {
5481            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
5482                range,
5483                rep.as_ref(),
5484                def.as_ref(),
5485                max_rep,
5486                max_visible_def,
5487                total_items,
5488                PreambleAction::Absent,
5489            );
5490            assert_eq!(item_range, expected_item_range);
5491            assert_eq!(level_range, expected_level_range);
5492        };
5493
5494        check(0..1, 0..1, 0..1);
5495        check(1..2, 1..2, 1..2);
5496        check(2..3, 2..3, 2..3);
5497        check(0..2, 0..2, 0..2);
5498        check(1..3, 1..3, 1..3);
5499        check(0..3, 0..3, 0..3);
5500
5501        // Tricky case, this chunk is a continuation and starts with a rep-index = 0
5502        // [[..., A] [B, C], NULL]
5503        //
5504        // What we do will depend on the preamble action
5505        let rep = Some(vec![0, 1, 0, 1]);
5506        let def = Some(vec![0, 0, 0, 1]);
5507        let max_visible_def = 0;
5508        let total_items = 3;
5509
5510        let check = |range, expected_item_range, expected_level_range| {
5511            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
5512                range,
5513                rep.as_ref(),
5514                def.as_ref(),
5515                max_rep,
5516                max_visible_def,
5517                total_items,
5518                PreambleAction::Take,
5519            );
5520            assert_eq!(item_range, expected_item_range);
5521            assert_eq!(level_range, expected_level_range);
5522        };
5523
5524        // If we are taking the preamble then the range must start at 0
5525        check(0..1, 0..3, 0..3);
5526        check(0..2, 0..3, 0..4);
5527
5528        let check = |range, expected_item_range, expected_level_range| {
5529            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
5530                range,
5531                rep.as_ref(),
5532                def.as_ref(),
5533                max_rep,
5534                max_visible_def,
5535                total_items,
5536                PreambleAction::Skip,
5537            );
5538            assert_eq!(item_range, expected_item_range);
5539            assert_eq!(level_range, expected_level_range);
5540        };
5541
5542        check(0..1, 1..3, 1..3);
5543        check(1..2, 3..3, 3..4);
5544        check(0..2, 1..3, 1..4);
5545
5546        // Another preamble case but now it doesn't end with a new list
5547        // [[..., A], NULL, [D, E]]
5548        //
5549        // What we do will depend on the preamble action
5550        let rep = Some(vec![0, 1, 1, 0]);
5551        let def = Some(vec![0, 1, 0, 0]);
5552        let max_visible_def = 0;
5553        let total_items = 4;
5554
5555        let check = |range, expected_item_range, expected_level_range| {
5556            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
5557                range,
5558                rep.as_ref(),
5559                def.as_ref(),
5560                max_rep,
5561                max_visible_def,
5562                total_items,
5563                PreambleAction::Take,
5564            );
5565            assert_eq!(item_range, expected_item_range);
5566            assert_eq!(level_range, expected_level_range);
5567        };
5568
5569        // If we are taking the preamble then the range must start at 0
5570        check(0..1, 0..1, 0..2);
5571        check(0..2, 0..3, 0..4);
5572
5573        let check = |range, expected_item_range, expected_level_range| {
5574            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
5575                range,
5576                rep.as_ref(),
5577                def.as_ref(),
5578                max_rep,
5579                max_visible_def,
5580                total_items,
5581                PreambleAction::Skip,
5582            );
5583            assert_eq!(item_range, expected_item_range);
5584            assert_eq!(level_range, expected_level_range);
5585        };
5586
5587        // If we are taking the preamble then the range must start at 0
5588        check(0..1, 1..1, 1..2);
5589        check(1..2, 1..3, 2..4);
5590        check(0..2, 1..3, 1..4);
5591
5592        // Now a preamble case without any definition levels
5593        // [[..., A] [B, C], [D]]
5594        let rep = Some(vec![0, 1, 0, 1]);
5595        let def: Option<Vec<u16>> = None;
5596        let max_visible_def = 0;
5597        let total_items = 4;
5598
5599        let check = |range, expected_item_range, expected_level_range| {
5600            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
5601                range,
5602                rep.as_ref(),
5603                def.as_ref(),
5604                max_rep,
5605                max_visible_def,
5606                total_items,
5607                PreambleAction::Take,
5608            );
5609            assert_eq!(item_range, expected_item_range);
5610            assert_eq!(level_range, expected_level_range);
5611        };
5612
5613        // If we are taking the preamble then the range must start at 0
5614        check(0..1, 0..3, 0..3);
5615        check(0..2, 0..4, 0..4);
5616
5617        let check = |range, expected_item_range, expected_level_range| {
5618            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
5619                range,
5620                rep.as_ref(),
5621                def.as_ref(),
5622                max_rep,
5623                max_visible_def,
5624                total_items,
5625                PreambleAction::Skip,
5626            );
5627            assert_eq!(item_range, expected_item_range);
5628            assert_eq!(level_range, expected_level_range);
5629        };
5630
5631        check(0..1, 1..3, 1..3);
5632        check(1..2, 3..4, 3..4);
5633        check(0..2, 1..4, 1..4);
5634
5635        // If we have nested lists then non-top level lists may be empty/null
5636        // and we need to make sure we still handle them as invisible items (we
5637        // failed to do this previously)
5638        let rep = Some(vec![2, 1, 2, 0, 1, 2]);
5639        let def = Some(vec![0, 1, 2, 0, 0, 0]);
5640        let max_rep = 2;
5641        let max_visible_def = 0;
5642        let total_items = 4;
5643
5644        let check = |range, expected_item_range, expected_level_range| {
5645            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
5646                range,
5647                rep.as_ref(),
5648                def.as_ref(),
5649                max_rep,
5650                max_visible_def,
5651                total_items,
5652                PreambleAction::Absent,
5653            );
5654            assert_eq!(item_range, expected_item_range);
5655            assert_eq!(level_range, expected_level_range);
5656        };
5657
5658        check(0..3, 0..4, 0..6);
5659        check(0..1, 0..1, 0..2);
5660        check(1..2, 1..3, 2..5);
5661        check(2..3, 3..4, 5..6);
5662
5663        // Invisible items in a preamble that we are taking (regressing a previous failure)
5664        let rep = Some(vec![0, 0, 1, 0, 1, 1]);
5665        let def = Some(vec![0, 1, 0, 0, 0, 0]);
5666        let max_rep = 1;
5667        let max_visible_def = 0;
5668        let total_items = 5;
5669
5670        let check = |range, expected_item_range, expected_level_range| {
5671            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
5672                range,
5673                rep.as_ref(),
5674                def.as_ref(),
5675                max_rep,
5676                max_visible_def,
5677                total_items,
5678                PreambleAction::Take,
5679            );
5680            assert_eq!(item_range, expected_item_range);
5681            assert_eq!(level_range, expected_level_range);
5682        };
5683
5684        check(0..0, 0..1, 0..2);
5685        check(0..1, 0..3, 0..4);
5686        check(0..2, 0..4, 0..5);
5687
5688        // Skip preamble (with invis items) and skip a few rows (with invis items)
5689        // and then take a few rows but not all the rows
5690        let rep = Some(vec![0, 1, 0, 1, 0, 1, 0, 1]);
5691        let def = Some(vec![1, 0, 1, 1, 0, 0, 0, 0]);
5692        let max_rep = 1;
5693        let max_visible_def = 0;
5694        let total_items = 5;
5695
5696        let check = |range, expected_item_range, expected_level_range| {
5697            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
5698                range,
5699                rep.as_ref(),
5700                def.as_ref(),
5701                max_rep,
5702                max_visible_def,
5703                total_items,
5704                PreambleAction::Skip,
5705            );
5706            assert_eq!(item_range, expected_item_range);
5707            assert_eq!(level_range, expected_level_range);
5708        };
5709
5710        check(2..3, 2..4, 5..7);
5711    }
5712
5713    #[test]
5714    fn test_slice_batch_data_and_rebase_offsets_u32() {
5715        let data = LanceBuffer::copy_slice(b"0123456789abcdefghij");
5716        let offsets = LanceBuffer::reinterpret_vec(vec![6_u32, 8_u32, 8_u32, 12_u32]);
5717
5718        let (sliced_data, normalized_offsets) =
5719            VariableFullZipDecoder::slice_batch_data_and_rebase_offsets(&data, &offsets, 32)
5720                .unwrap();
5721
5722        assert_eq!(sliced_data.as_ref(), b"6789ab");
5723        let normalized = normalized_offsets.borrow_to_typed_slice::<u32>();
5724        assert_eq!(normalized.as_ref(), &[0, 2, 2, 6]);
5725    }
5726
5727    #[test]
5728    fn test_slice_batch_data_and_rebase_offsets_u64() {
5729        let data = LanceBuffer::copy_slice(b"abcdefghijklmnopqrstuvwxyz");
5730        let offsets = LanceBuffer::reinterpret_vec(vec![10_u64, 12_u64, 16_u64, 20_u64]);
5731
5732        let (sliced_data, normalized_offsets) =
5733            VariableFullZipDecoder::slice_batch_data_and_rebase_offsets(&data, &offsets, 64)
5734                .unwrap();
5735
5736        assert_eq!(sliced_data.as_ref(), b"klmnopqrst");
5737        let normalized = normalized_offsets.borrow_to_typed_slice::<u64>();
5738        assert_eq!(normalized.as_ref(), &[0, 2, 6, 10]);
5739    }
5740
5741    #[test]
5742    fn test_slice_batch_data_and_rebase_offsets_rejects_invalid_offsets() {
5743        let data = LanceBuffer::copy_slice(b"abcd");
5744        let offsets = LanceBuffer::reinterpret_vec(vec![3_u32, 2_u32]);
5745
5746        let err = VariableFullZipDecoder::slice_batch_data_and_rebase_offsets(&data, &offsets, 32)
5747            .expect_err("offset end before start should error");
5748        assert!(err.to_string().contains("less than base"));
5749    }
5750
5751    #[test]
5752    fn test_schedule_instructions() {
5753        // Convert repetition index to bytes for testing
5754        let rep_data: Vec<u64> = vec![5, 2, 3, 0, 4, 7, 2, 0];
5755        let rep_bytes: Vec<u8> = rep_data.iter().flat_map(|v| v.to_le_bytes()).collect();
5756        let repetition_index = MiniBlockRepIndex::decode_from_bytes(&rep_bytes, 2);
5757
5758        let check = |user_ranges, expected_instructions| {
5759            let instructions =
5760                ChunkInstructions::schedule_instructions(&repetition_index, user_ranges);
5761            assert_eq!(instructions, expected_instructions);
5762        };
5763
5764        // The instructions we expect if we're grabbing the whole range
5765        let expected_take_all = vec![
5766            ChunkInstructions {
5767                chunk_idx: 0,
5768                preamble: PreambleAction::Absent,
5769                rows_to_skip: 0,
5770                rows_to_take: 6,
5771                take_trailer: true,
5772            },
5773            ChunkInstructions {
5774                chunk_idx: 1,
5775                preamble: PreambleAction::Take,
5776                rows_to_skip: 0,
5777                rows_to_take: 2,
5778                take_trailer: false,
5779            },
5780            ChunkInstructions {
5781                chunk_idx: 2,
5782                preamble: PreambleAction::Absent,
5783                rows_to_skip: 0,
5784                rows_to_take: 5,
5785                take_trailer: true,
5786            },
5787            ChunkInstructions {
5788                chunk_idx: 3,
5789                preamble: PreambleAction::Take,
5790                rows_to_skip: 0,
5791                rows_to_take: 1,
5792                take_trailer: false,
5793            },
5794        ];
5795
5796        // Take all as 1 range
5797        check(&[0..14], expected_take_all.clone());
5798
5799        // Take all a individual rows
5800        check(
5801            &[
5802                0..1,
5803                1..2,
5804                2..3,
5805                3..4,
5806                4..5,
5807                5..6,
5808                6..7,
5809                7..8,
5810                8..9,
5811                9..10,
5812                10..11,
5813                11..12,
5814                12..13,
5815                13..14,
5816            ],
5817            expected_take_all,
5818        );
5819
5820        // Test some partial takes
5821
5822        // 2 rows in the same chunk but not contiguous
5823        check(
5824            &[0..1, 3..4],
5825            vec![
5826                ChunkInstructions {
5827                    chunk_idx: 0,
5828                    preamble: PreambleAction::Absent,
5829                    rows_to_skip: 0,
5830                    rows_to_take: 1,
5831                    take_trailer: false,
5832                },
5833                ChunkInstructions {
5834                    chunk_idx: 0,
5835                    preamble: PreambleAction::Absent,
5836                    rows_to_skip: 3,
5837                    rows_to_take: 1,
5838                    take_trailer: false,
5839                },
5840            ],
5841        );
5842
5843        // Taking just a trailer/preamble
5844        check(
5845            &[5..6],
5846            vec![
5847                ChunkInstructions {
5848                    chunk_idx: 0,
5849                    preamble: PreambleAction::Absent,
5850                    rows_to_skip: 5,
5851                    rows_to_take: 1,
5852                    take_trailer: true,
5853                },
5854                ChunkInstructions {
5855                    chunk_idx: 1,
5856                    preamble: PreambleAction::Take,
5857                    rows_to_skip: 0,
5858                    rows_to_take: 0,
5859                    take_trailer: false,
5860                },
5861            ],
5862        );
5863
5864        // Skipping an entire chunk
5865        check(
5866            &[7..10],
5867            vec![
5868                ChunkInstructions {
5869                    chunk_idx: 1,
5870                    preamble: PreambleAction::Skip,
5871                    rows_to_skip: 1,
5872                    rows_to_take: 1,
5873                    take_trailer: false,
5874                },
5875                ChunkInstructions {
5876                    chunk_idx: 2,
5877                    preamble: PreambleAction::Absent,
5878                    rows_to_skip: 0,
5879                    rows_to_take: 2,
5880                    take_trailer: false,
5881                },
5882            ],
5883        );
5884    }
5885
5886    #[test]
5887    fn test_drain_instructions() {
5888        fn drain_from_instructions(
5889            instructions: &mut VecDeque<ChunkInstructions>,
5890            mut rows_desired: u64,
5891            need_preamble: &mut bool,
5892            skip_in_chunk: &mut u64,
5893        ) -> Vec<ChunkDrainInstructions> {
5894            // Note: instructions.len() is an upper bound, we typically take much fewer
5895            let mut drain_instructions = Vec::with_capacity(instructions.len());
5896            while rows_desired > 0 || *need_preamble {
5897                let (next_instructions, consumed_chunk) = instructions
5898                    .front()
5899                    .unwrap()
5900                    .drain_from_instruction(&mut rows_desired, need_preamble, skip_in_chunk);
5901                if consumed_chunk {
5902                    instructions.pop_front();
5903                }
5904                drain_instructions.push(next_instructions);
5905            }
5906            drain_instructions
5907        }
5908
5909        // Convert repetition index to bytes for testing
5910        let rep_data: Vec<u64> = vec![5, 2, 3, 0, 4, 7, 2, 0];
5911        let rep_bytes: Vec<u8> = rep_data.iter().flat_map(|v| v.to_le_bytes()).collect();
5912        let repetition_index = MiniBlockRepIndex::decode_from_bytes(&rep_bytes, 2);
5913        let user_ranges = vec![1..7, 10..14];
5914
5915        // First, schedule the ranges
5916        let scheduled = ChunkInstructions::schedule_instructions(&repetition_index, &user_ranges);
5917
5918        let mut to_drain = VecDeque::from(scheduled.clone());
5919
5920        // Now we drain in batches of 4
5921
5922        let mut need_preamble = false;
5923        let mut skip_in_chunk = 0;
5924
5925        let next_batch =
5926            drain_from_instructions(&mut to_drain, 4, &mut need_preamble, &mut skip_in_chunk);
5927
5928        assert!(!need_preamble);
5929        assert_eq!(skip_in_chunk, 4);
5930        assert_eq!(
5931            next_batch,
5932            vec![ChunkDrainInstructions {
5933                chunk_instructions: scheduled[0].clone(),
5934                rows_to_take: 4,
5935                rows_to_skip: 0,
5936                preamble_action: PreambleAction::Absent,
5937            }]
5938        );
5939
5940        let next_batch =
5941            drain_from_instructions(&mut to_drain, 4, &mut need_preamble, &mut skip_in_chunk);
5942
5943        assert!(!need_preamble);
5944        assert_eq!(skip_in_chunk, 2);
5945
5946        assert_eq!(
5947            next_batch,
5948            vec![
5949                ChunkDrainInstructions {
5950                    chunk_instructions: scheduled[0].clone(),
5951                    rows_to_take: 1,
5952                    rows_to_skip: 4,
5953                    preamble_action: PreambleAction::Absent,
5954                },
5955                ChunkDrainInstructions {
5956                    chunk_instructions: scheduled[1].clone(),
5957                    rows_to_take: 1,
5958                    rows_to_skip: 0,
5959                    preamble_action: PreambleAction::Take,
5960                },
5961                ChunkDrainInstructions {
5962                    chunk_instructions: scheduled[2].clone(),
5963                    rows_to_take: 2,
5964                    rows_to_skip: 0,
5965                    preamble_action: PreambleAction::Absent,
5966                }
5967            ]
5968        );
5969
5970        let next_batch =
5971            drain_from_instructions(&mut to_drain, 2, &mut need_preamble, &mut skip_in_chunk);
5972
5973        assert!(!need_preamble);
5974        assert_eq!(skip_in_chunk, 0);
5975
5976        assert_eq!(
5977            next_batch,
5978            vec![
5979                ChunkDrainInstructions {
5980                    chunk_instructions: scheduled[2].clone(),
5981                    rows_to_take: 1,
5982                    rows_to_skip: 2,
5983                    preamble_action: PreambleAction::Absent,
5984                },
5985                ChunkDrainInstructions {
5986                    chunk_instructions: scheduled[3].clone(),
5987                    rows_to_take: 1,
5988                    rows_to_skip: 0,
5989                    preamble_action: PreambleAction::Take,
5990                },
5991            ]
5992        );
5993
5994        // Regression case.  Need a chunk with preamble, rows, and trailer (the middle chunk here)
5995        let rep_data: Vec<u64> = vec![5, 2, 3, 3, 20, 0];
5996        let rep_bytes: Vec<u8> = rep_data.iter().flat_map(|v| v.to_le_bytes()).collect();
5997        let repetition_index = MiniBlockRepIndex::decode_from_bytes(&rep_bytes, 2);
5998        let user_ranges = vec![0..28];
5999
6000        // First, schedule the ranges
6001        let scheduled = ChunkInstructions::schedule_instructions(&repetition_index, &user_ranges);
6002
6003        let mut to_drain = VecDeque::from(scheduled.clone());
6004
6005        // Drain first chunk and some of second chunk
6006
6007        let mut need_preamble = false;
6008        let mut skip_in_chunk = 0;
6009
6010        let next_batch =
6011            drain_from_instructions(&mut to_drain, 7, &mut need_preamble, &mut skip_in_chunk);
6012
6013        assert_eq!(
6014            next_batch,
6015            vec![
6016                ChunkDrainInstructions {
6017                    chunk_instructions: scheduled[0].clone(),
6018                    rows_to_take: 6,
6019                    rows_to_skip: 0,
6020                    preamble_action: PreambleAction::Absent,
6021                },
6022                ChunkDrainInstructions {
6023                    chunk_instructions: scheduled[1].clone(),
6024                    rows_to_take: 1,
6025                    rows_to_skip: 0,
6026                    preamble_action: PreambleAction::Take,
6027                },
6028            ]
6029        );
6030
6031        assert!(!need_preamble);
6032        assert_eq!(skip_in_chunk, 1);
6033
6034        // Now, the tricky part.  We drain the second chunk, including the trailer, and need to make sure
6035        // we get a drain task to take the preamble of the third chunk (and nothing else)
6036        let next_batch =
6037            drain_from_instructions(&mut to_drain, 2, &mut need_preamble, &mut skip_in_chunk);
6038
6039        assert_eq!(
6040            next_batch,
6041            vec![
6042                ChunkDrainInstructions {
6043                    chunk_instructions: scheduled[1].clone(),
6044                    rows_to_take: 2,
6045                    rows_to_skip: 1,
6046                    preamble_action: PreambleAction::Skip,
6047                },
6048                ChunkDrainInstructions {
6049                    chunk_instructions: scheduled[2].clone(),
6050                    rows_to_take: 0,
6051                    rows_to_skip: 0,
6052                    preamble_action: PreambleAction::Take,
6053                },
6054            ]
6055        );
6056
6057        assert!(!need_preamble);
6058        assert_eq!(skip_in_chunk, 0);
6059    }
6060
6061    #[tokio::test]
6062    async fn test_fullzip_initialize_is_lazy() {
6063        use futures::{FutureExt, future::BoxFuture};
6064        use std::ops::Range;
6065        use std::sync::Mutex;
6066
6067        #[derive(Debug, Clone)]
6068        struct RecordingScheduler {
6069            data: bytes::Bytes,
6070            requests: Arc<Mutex<Vec<Vec<Range<u64>>>>>,
6071        }
6072
6073        impl RecordingScheduler {
6074            fn new(data: bytes::Bytes) -> Self {
6075                Self {
6076                    data,
6077                    requests: Arc::new(Mutex::new(Vec::new())),
6078                }
6079            }
6080
6081            fn requests(&self) -> Vec<Vec<Range<u64>>> {
6082                self.requests.lock().unwrap().clone()
6083            }
6084        }
6085
6086        impl crate::EncodingsIo for RecordingScheduler {
6087            fn submit_request(
6088                &self,
6089                ranges: Vec<Range<u64>>,
6090                _priority: u64,
6091            ) -> BoxFuture<'static, crate::Result<Vec<bytes::Bytes>>> {
6092                self.requests.lock().unwrap().push(ranges.clone());
6093                let data = ranges
6094                    .into_iter()
6095                    .map(|range| self.data.slice(range.start as usize..range.end as usize))
6096                    .collect::<Vec<_>>();
6097                std::future::ready(Ok(data)).boxed()
6098            }
6099        }
6100
6101        #[derive(Debug)]
6102        struct TestFixedDecompressor;
6103
6104        impl FixedPerValueDecompressor for TestFixedDecompressor {
6105            fn decompress(
6106                &self,
6107                _data: FixedWidthDataBlock,
6108                _num_rows: u64,
6109            ) -> crate::Result<DataBlock> {
6110                unimplemented!("Test decompressor")
6111            }
6112
6113            fn bits_per_value(&self) -> u64 {
6114                32
6115            }
6116        }
6117
6118        let io = Arc::new(RecordingScheduler::new(bytes::Bytes::from(vec![
6119            0;
6120            16 * 1024
6121        ])));
6122        let mut scheduler = FullZipScheduler {
6123            data_buf_position: 0,
6124            data_buf_size: 4096,
6125            rep_index: Some(FullZipRepIndexDetails {
6126                buf_position: 1000,
6127                bytes_per_value: 4,
6128            }),
6129            priority: 0,
6130            rows_in_page: 100,
6131            bits_per_offset: 32,
6132            details: Arc::new(FullZipDecodeDetails {
6133                value_decompressor: PerValueDecompressor::Fixed(Arc::new(TestFixedDecompressor)),
6134                def_meaning: Arc::new([crate::repdef::DefinitionInterpretation::NullableItem]),
6135                ctrl_word_parser: crate::repdef::ControlWordParser::new(0, 1),
6136                max_rep: 0,
6137                max_visible_def: 0,
6138            }),
6139            cached_state: None,
6140            enable_cache: false,
6141        };
6142
6143        let io_dyn: Arc<dyn crate::EncodingsIo> = io.clone();
6144        let cached_data = scheduler.initialize(&io_dyn).await.unwrap();
6145
6146        assert!(
6147            cached_data
6148                .as_arc_any()
6149                .downcast_ref::<super::NoCachedPageData>()
6150                .is_some(),
6151            "FullZip initialize should not eagerly load repetition index data"
6152        );
6153        assert!(scheduler.cached_state.is_none());
6154        assert!(
6155            io.requests().is_empty(),
6156            "FullZip initialize should not issue any I/O"
6157        );
6158    }
6159
6160    #[tokio::test]
6161    async fn test_fullzip_read_source_slices_prefetched_page() {
6162        let page_start = 200_u64;
6163        let page_data = LanceBuffer::copy_slice(&[0, 1, 2, 3, 4, 5, 6, 7]);
6164        let source = FullZipReadSource::PrefetchedPage {
6165            base_offset: page_start,
6166            data: page_data,
6167        };
6168        let ranges = vec![
6169            page_start..(page_start + 3),
6170            (page_start + 4)..(page_start + 8),
6171        ];
6172        let mut data = source.fetch(&ranges, 0).await.unwrap();
6173        assert_eq!(data.pop_front().unwrap().as_ref(), &[0, 1, 2]);
6174        assert_eq!(data.pop_front().unwrap().as_ref(), &[4, 5, 6, 7]);
6175    }
6176
6177    #[tokio::test]
6178    async fn test_fullzip_initialize_caches_rep_index_when_enabled() {
6179        use futures::{FutureExt, future::BoxFuture};
6180        use std::ops::Range;
6181        use std::sync::Mutex;
6182
6183        #[derive(Debug, Clone)]
6184        struct RecordingScheduler {
6185            data: bytes::Bytes,
6186            requests: Arc<Mutex<Vec<Vec<Range<u64>>>>>,
6187        }
6188
6189        impl RecordingScheduler {
6190            fn new(data: bytes::Bytes) -> Self {
6191                Self {
6192                    data,
6193                    requests: Arc::new(Mutex::new(Vec::new())),
6194                }
6195            }
6196
6197            fn requests(&self) -> Vec<Vec<Range<u64>>> {
6198                self.requests.lock().unwrap().clone()
6199            }
6200        }
6201
6202        impl crate::EncodingsIo for RecordingScheduler {
6203            fn submit_request(
6204                &self,
6205                ranges: Vec<Range<u64>>,
6206                _priority: u64,
6207            ) -> BoxFuture<'static, crate::Result<Vec<bytes::Bytes>>> {
6208                self.requests.lock().unwrap().push(ranges.clone());
6209                let data = ranges
6210                    .into_iter()
6211                    .map(|range| self.data.slice(range.start as usize..range.end as usize))
6212                    .collect::<Vec<_>>();
6213                std::future::ready(Ok(data)).boxed()
6214            }
6215        }
6216
6217        #[derive(Debug)]
6218        struct TestFixedDecompressor;
6219
6220        impl FixedPerValueDecompressor for TestFixedDecompressor {
6221            fn decompress(
6222                &self,
6223                _data: FixedWidthDataBlock,
6224                _num_rows: u64,
6225            ) -> crate::Result<DataBlock> {
6226                unimplemented!("Test decompressor")
6227            }
6228
6229            fn bits_per_value(&self) -> u64 {
6230                32
6231            }
6232        }
6233
6234        let rows_in_page = 100_u64;
6235        let bytes_per_value = 4_u64;
6236        let rep_start = 1000_u64;
6237        let rep_size = ((rows_in_page + 1) * bytes_per_value) as usize;
6238        let mut data = vec![0_u8; 16 * 1024];
6239        data[rep_start as usize..rep_start as usize + rep_size].fill(7);
6240        let io = Arc::new(RecordingScheduler::new(bytes::Bytes::from(data)));
6241
6242        let mut scheduler = FullZipScheduler {
6243            data_buf_position: 0,
6244            data_buf_size: 4096,
6245            rep_index: Some(FullZipRepIndexDetails {
6246                buf_position: rep_start,
6247                bytes_per_value,
6248            }),
6249            priority: 0,
6250            rows_in_page,
6251            bits_per_offset: 32,
6252            details: Arc::new(FullZipDecodeDetails {
6253                value_decompressor: PerValueDecompressor::Fixed(Arc::new(TestFixedDecompressor)),
6254                def_meaning: Arc::new([crate::repdef::DefinitionInterpretation::NullableItem]),
6255                ctrl_word_parser: crate::repdef::ControlWordParser::new(0, 1),
6256                max_rep: 0,
6257                max_visible_def: 0,
6258            }),
6259            cached_state: None,
6260            enable_cache: true,
6261        };
6262
6263        let io_dyn: Arc<dyn crate::EncodingsIo> = io.clone();
6264        let cached_data = scheduler.initialize(&io_dyn).await.unwrap();
6265        assert!(
6266            cached_data
6267                .as_arc_any()
6268                .downcast_ref::<FullZipCacheableState>()
6269                .is_some()
6270        );
6271        assert!(scheduler.cached_state.is_some());
6272        assert_eq!(
6273            io.requests(),
6274            vec![vec![
6275                rep_start..(rep_start + (rows_in_page + 1) * bytes_per_value)
6276            ]]
6277        );
6278    }
6279
6280    #[tokio::test]
6281    async fn test_fullzip_full_page_bypasses_rep_index_io() {
6282        use futures::{FutureExt, future::BoxFuture};
6283        use std::ops::Range;
6284        use std::sync::Mutex;
6285
6286        #[derive(Debug, Clone)]
6287        struct RecordingScheduler {
6288            data: bytes::Bytes,
6289            requests: Arc<Mutex<Vec<Vec<Range<u64>>>>>,
6290        }
6291
6292        impl RecordingScheduler {
6293            fn new(data: bytes::Bytes) -> Self {
6294                Self {
6295                    data,
6296                    requests: Arc::new(Mutex::new(Vec::new())),
6297                }
6298            }
6299
6300            fn requests(&self) -> Vec<Vec<Range<u64>>> {
6301                self.requests.lock().unwrap().clone()
6302            }
6303        }
6304
6305        impl crate::EncodingsIo for RecordingScheduler {
6306            fn submit_request(
6307                &self,
6308                ranges: Vec<Range<u64>>,
6309                _priority: u64,
6310            ) -> BoxFuture<'static, crate::Result<Vec<bytes::Bytes>>> {
6311                self.requests.lock().unwrap().push(ranges.clone());
6312                let data = ranges
6313                    .into_iter()
6314                    .map(|range| self.data.slice(range.start as usize..range.end as usize))
6315                    .collect::<Vec<_>>();
6316                std::future::ready(Ok(data)).boxed()
6317            }
6318        }
6319
6320        #[derive(Debug)]
6321        struct TestFixedDecompressor;
6322
6323        impl FixedPerValueDecompressor for TestFixedDecompressor {
6324            fn decompress(
6325                &self,
6326                _data: FixedWidthDataBlock,
6327                _num_rows: u64,
6328            ) -> crate::Result<DataBlock> {
6329                unimplemented!("Test decompressor")
6330            }
6331
6332            fn bits_per_value(&self) -> u64 {
6333                32
6334            }
6335        }
6336
6337        let rows_in_page = 100_u64;
6338        let data_start = 256_u64;
6339        let data_size = 500_u64;
6340        let rep_start = 4096_u64;
6341        let bytes_per_value = 4_u64;
6342
6343        let mut bytes = vec![0_u8; 16 * 1024];
6344        for i in 0..=rows_in_page {
6345            let offset = (i * 5) as u32;
6346            let pos = rep_start as usize + (i * bytes_per_value) as usize;
6347            bytes[pos..pos + 4].copy_from_slice(&offset.to_le_bytes());
6348        }
6349        let io = Arc::new(RecordingScheduler::new(bytes::Bytes::from(bytes)));
6350
6351        let scheduler = FullZipScheduler {
6352            data_buf_position: data_start,
6353            data_buf_size: data_size,
6354            rep_index: Some(FullZipRepIndexDetails {
6355                buf_position: rep_start,
6356                bytes_per_value,
6357            }),
6358            priority: 0,
6359            rows_in_page,
6360            bits_per_offset: 32,
6361            details: Arc::new(FullZipDecodeDetails {
6362                value_decompressor: PerValueDecompressor::Fixed(Arc::new(TestFixedDecompressor)),
6363                def_meaning: Arc::new([crate::repdef::DefinitionInterpretation::NullableItem]),
6364                ctrl_word_parser: crate::repdef::ControlWordParser::new(0, 1),
6365                max_rep: 0,
6366                max_visible_def: 0,
6367            }),
6368            cached_state: None,
6369            enable_cache: false,
6370        };
6371
6372        let io_dyn: Arc<dyn crate::EncodingsIo> = io.clone();
6373        let tasks = scheduler
6374            .schedule_ranges_rep(
6375                &[0..rows_in_page],
6376                &io_dyn,
6377                FullZipRepIndexDetails {
6378                    buf_position: rep_start,
6379                    bytes_per_value,
6380                },
6381            )
6382            .unwrap();
6383
6384        let requests = io.requests();
6385        assert_eq!(requests.len(), 1);
6386        assert_eq!(requests[0], vec![data_start..(data_start + data_size)]);
6387
6388        let _ = tasks.into_iter().next().unwrap().decoder_fut.await.unwrap();
6389        let requests_after_await = io.requests();
6390        assert_eq!(
6391            requests_after_await.len(),
6392            1,
6393            "full page path should not issue rep-index I/O"
6394        );
6395    }
6396
6397    /// This test is used to reproduce fuzz test https://github.com/lancedb/lance/issues/4492
6398    #[tokio::test]
6399    async fn test_fuzz_issue_4492_empty_rep_values() {
6400        use lance_datagen::{RowCount, Seed, array, gen_batch};
6401
6402        let seed = 1823859942947654717u64;
6403        let num_rows = 2741usize;
6404
6405        // Generate the exact same data that caused the failure
6406        let batch_gen = gen_batch().with_seed(Seed::from(seed));
6407        let base_generator = array::rand_type(&DataType::FixedSizeBinary(32));
6408        let list_generator = array::rand_list_any(base_generator, false);
6409
6410        let batch = batch_gen
6411            .anon_col(list_generator)
6412            .into_batch_rows(RowCount::from(num_rows as u64))
6413            .unwrap();
6414
6415        let list_array = batch.column(0).clone();
6416
6417        // Force miniblock encoding
6418        let mut metadata = HashMap::new();
6419        metadata.insert(
6420            STRUCTURAL_ENCODING_META_KEY.to_string(),
6421            STRUCTURAL_ENCODING_MINIBLOCK.to_string(),
6422        );
6423
6424        let test_cases = TestCases::default()
6425            .with_min_file_version(LanceFileVersion::V2_1)
6426            .with_batch_size(100)
6427            .with_range(0..num_rows.min(500) as u64)
6428            .with_indices(vec![0, num_rows as u64 / 2, (num_rows - 1) as u64]);
6429
6430        check_round_trip_encoding_of_data(vec![list_array], &test_cases, metadata).await
6431    }
6432
6433    async fn test_minichunk_size_helper(
6434        string_data: Vec<Option<String>>,
6435        minichunk_size: u64,
6436        file_version: LanceFileVersion,
6437    ) {
6438        use crate::constants::MINICHUNK_SIZE_META_KEY;
6439        use crate::testing::{TestCases, check_round_trip_encoding_of_data};
6440        use arrow_array::{ArrayRef, StringArray};
6441        use std::sync::Arc;
6442
6443        let string_array: ArrayRef = Arc::new(StringArray::from(string_data));
6444
6445        let mut metadata = HashMap::new();
6446        metadata.insert(
6447            MINICHUNK_SIZE_META_KEY.to_string(),
6448            minichunk_size.to_string(),
6449        );
6450        metadata.insert(
6451            STRUCTURAL_ENCODING_META_KEY.to_string(),
6452            STRUCTURAL_ENCODING_MINIBLOCK.to_string(),
6453        );
6454
6455        let test_cases = TestCases::default()
6456            .with_min_file_version(file_version)
6457            .with_batch_size(1000);
6458
6459        check_round_trip_encoding_of_data(vec![string_array], &test_cases, metadata).await;
6460    }
6461
6462    #[tokio::test]
6463    async fn test_minichunk_size_roundtrip() {
6464        // Test that minichunk size can be configured and works correctly in round-trip encoding
6465        let mut string_data = Vec::new();
6466        for i in 0..100 {
6467            string_data.push(Some(format!("test_string_{}", i).repeat(50)));
6468        }
6469        // configure minichunk size to 64 bytes (smaller than the default 4kb) for Lance 2.1
6470        test_minichunk_size_helper(string_data, 64, LanceFileVersion::V2_1).await;
6471    }
6472
6473    #[tokio::test]
6474    async fn test_minichunk_size_128kb_v2_2() {
6475        // Test that minichunk size can be configured to 128KB and works correctly with Lance 2.2
6476        let mut string_data = Vec::new();
6477        // create a 500kb string array
6478        for i in 0..10000 {
6479            string_data.push(Some(format!("test_string_{}", i).repeat(50)));
6480        }
6481        test_minichunk_size_helper(string_data, 128 * 1024, LanceFileVersion::V2_2).await;
6482    }
6483
6484    #[tokio::test]
6485    async fn test_binary_large_minichunk_size_over_max_miniblock_values() {
6486        let mut string_data = Vec::new();
6487        // 128kb/chunk / 6 bytes (t_9999) = 21845 > max 4096 items per chunk
6488        for i in 0..10000 {
6489            string_data.push(Some(format!("t_{}", i)));
6490        }
6491        test_minichunk_size_helper(string_data, 128 * 1024, LanceFileVersion::V2_2).await;
6492    }
6493
6494    #[tokio::test]
6495    async fn test_large_dictionary_general_compression() {
6496        use arrow_array::{ArrayRef, StringArray};
6497        use std::collections::HashMap;
6498        use std::sync::Arc;
6499
6500        // Create large string dictionary data (>32KiB) with low cardinality
6501        // Use 100 unique strings, each 500 bytes long = 50KB dictionary
6502        let unique_values: Vec<String> = (0..100)
6503            .map(|i| format!("value_{:04}_{}", i, "x".repeat(500)))
6504            .collect();
6505
6506        // Repeat these strings many times to create a large array
6507        let repeated_strings: Vec<_> = unique_values
6508            .iter()
6509            .cycle()
6510            .take(100_000)
6511            .map(|s| Some(s.as_str()))
6512            .collect();
6513
6514        let string_array = Arc::new(StringArray::from(repeated_strings)) as ArrayRef;
6515
6516        // Configure test to use V2_2 and verify encoding
6517        let test_cases = TestCases::default()
6518            .with_min_file_version(LanceFileVersion::V2_2)
6519            .with_verify_encoding(Arc::new(|cols: &[crate::encoder::EncodedColumn], _| {
6520                assert_eq!(cols.len(), 1);
6521                let col = &cols[0];
6522
6523                // Navigate to the dictionary encoding in the page layout
6524                if let Some(PageEncoding::Structural(page_layout)) =
6525                    &col.final_pages.first().map(|p| &p.description)
6526                    && let Some(pb21::page_layout::Layout::MiniBlockLayout(mini_block)) =
6527                        &page_layout.layout
6528                    && let Some(dictionary_encoding) = &mini_block.dictionary
6529                {
6530                    match dictionary_encoding.compression.as_ref() {
6531                        Some(Compression::General(general)) => {
6532                            // Verify it's using LZ4 or Zstd
6533                            let compression = general.compression.as_ref().unwrap();
6534                            assert!(
6535                                compression.scheme()
6536                                    == pb21::CompressionScheme::CompressionAlgorithmLz4
6537                                    || compression.scheme()
6538                                        == pb21::CompressionScheme::CompressionAlgorithmZstd,
6539                                "Expected LZ4 or Zstd compression for large dictionary"
6540                            );
6541                        }
6542                        _ => panic!("Expected General compression for large dictionary"),
6543                    }
6544                }
6545            }));
6546
6547        check_round_trip_encoding_of_data(vec![string_array], &test_cases, HashMap::new()).await;
6548    }
6549
6550    fn dictionary_encoding_from_page(
6551        page: &crate::encoder::EncodedPage,
6552    ) -> &crate::format::pb21::CompressiveEncoding {
6553        let PageEncoding::Structural(layout) = &page.description else {
6554            panic!("Expected structural page encoding");
6555        };
6556        let pb21::page_layout::Layout::MiniBlockLayout(layout) = layout.layout.as_ref().unwrap()
6557        else {
6558            panic!("Expected mini-block layout");
6559        };
6560        layout
6561            .dictionary
6562            .as_ref()
6563            .unwrap_or_else(|| panic!("Expected dictionary encoding"))
6564    }
6565
6566    async fn encode_variable_dict_page(
6567        metadata: HashMap<String, String>,
6568    ) -> crate::encoder::EncodedPage {
6569        use arrow_array::types::Int32Type;
6570        use arrow_array::{ArrayRef, DictionaryArray, Int32Array, StringArray};
6571
6572        let values = Arc::new(StringArray::from(
6573            (0..128)
6574                .map(|i| format!("value_{i:04}_{}", "x".repeat(256)))
6575                .collect::<Vec<_>>(),
6576        )) as ArrayRef;
6577        let keys = Int32Array::from_iter_values((0..20_000).map(|i| i % 128));
6578        let dict_array =
6579            Arc::new(DictionaryArray::<Int32Type>::try_new(keys, values).unwrap()) as ArrayRef;
6580
6581        let field = arrow_schema::Field::new(
6582            "dict_col",
6583            DataType::Dictionary(Box::new(DataType::Int32), Box::new(DataType::Utf8)),
6584            false,
6585        )
6586        .with_metadata(metadata);
6587
6588        encode_first_page(field, dict_array, LanceFileVersion::V2_2).await
6589    }
6590
6591    async fn encode_auto_fixed_dict_page(
6592        metadata: HashMap<String, String>,
6593    ) -> crate::encoder::EncodedPage {
6594        use arrow_array::{ArrayRef, Decimal128Array};
6595
6596        // 128-bit fixed-width values with low cardinality to trigger dictionary encoding.
6597        let values = (0..20_000)
6598            .map(|i| match i % 3 {
6599                0 => 10_i128,
6600                1 => 20_i128,
6601                _ => 30_i128,
6602            })
6603            .collect::<Vec<_>>();
6604        let decimal = Decimal128Array::from_iter_values(values)
6605            .with_precision_and_scale(38, 0)
6606            .unwrap();
6607        let decimal = Arc::new(decimal) as ArrayRef;
6608
6609        let mut field_metadata = metadata;
6610        // Strongly encourage dictionary encoding for this synthetic test data.
6611        field_metadata.insert(
6612            "lance-encoding:dict-size-ratio".to_string(),
6613            "0.99".to_string(),
6614        );
6615        let field = arrow_schema::Field::new("fixed_col", DataType::Decimal128(38, 0), false)
6616            .with_metadata(field_metadata);
6617
6618        encode_first_page(field, decimal, LanceFileVersion::V2_2).await
6619    }
6620
6621    #[tokio::test]
6622    async fn test_dict_values_general_compression_default_lz4_for_variable_dict_values() {
6623        let page = encode_variable_dict_page(HashMap::new()).await;
6624        let dictionary_encoding = dictionary_encoding_from_page(&page);
6625        let Some(Compression::General(general)) = dictionary_encoding.compression.as_ref() else {
6626            panic!("Expected General compression for dictionary values");
6627        };
6628        let compression = general.compression.as_ref().unwrap();
6629        assert_eq!(
6630            compression.scheme(),
6631            pb21::CompressionScheme::CompressionAlgorithmLz4
6632        );
6633    }
6634
6635    #[tokio::test]
6636    async fn test_dict_values_general_compression_default_lz4_for_fixed_dict_values() {
6637        let page = encode_auto_fixed_dict_page(HashMap::new()).await;
6638        let dictionary_encoding = dictionary_encoding_from_page(&page);
6639        let Some(Compression::General(general)) = dictionary_encoding.compression.as_ref() else {
6640            panic!("Expected General compression for dictionary values");
6641        };
6642        let compression = general.compression.as_ref().unwrap();
6643        assert_eq!(
6644            compression.scheme(),
6645            pb21::CompressionScheme::CompressionAlgorithmLz4
6646        );
6647    }
6648
6649    #[tokio::test]
6650    async fn test_dict_values_general_compression_zstd() {
6651        let mut metadata = HashMap::new();
6652        metadata.insert(
6653            DICT_VALUES_COMPRESSION_META_KEY.to_string(),
6654            "zstd".to_string(),
6655        );
6656        let page = encode_variable_dict_page(metadata).await;
6657        let dictionary_encoding = dictionary_encoding_from_page(&page);
6658        let Some(Compression::General(general)) = dictionary_encoding.compression.as_ref() else {
6659            panic!("Expected General compression for dictionary values");
6660        };
6661        let compression = general.compression.as_ref().unwrap();
6662        assert_eq!(
6663            compression.scheme(),
6664            pb21::CompressionScheme::CompressionAlgorithmZstd
6665        );
6666    }
6667
6668    #[tokio::test]
6669    async fn test_dict_values_general_compression_none() {
6670        let mut metadata = HashMap::new();
6671        metadata.insert(
6672            DICT_VALUES_COMPRESSION_META_KEY.to_string(),
6673            "none".to_string(),
6674        );
6675        let page = encode_variable_dict_page(metadata).await;
6676        let dictionary_encoding = dictionary_encoding_from_page(&page);
6677        assert!(
6678            !matches!(
6679                dictionary_encoding.compression.as_ref(),
6680                Some(Compression::General(_))
6681            ),
6682            "Expected dictionary values to avoid General compression"
6683        );
6684    }
6685
6686    #[test]
6687    fn test_resolve_dict_values_compression_metadata_defaults_to_lz4() {
6688        let metadata = PrimitiveStructuralEncoder::resolve_dict_values_compression_metadata(
6689            &HashMap::new(),
6690            None,
6691            None,
6692        );
6693        assert_eq!(metadata.get(COMPRESSION_META_KEY), Some(&"lz4".to_string()),);
6694        assert!(!metadata.contains_key(COMPRESSION_LEVEL_META_KEY));
6695    }
6696
6697    #[test]
6698    fn test_resolve_dict_values_compression_metadata_metadata_overrides_env() {
6699        let field_metadata = HashMap::from([
6700            (
6701                DICT_VALUES_COMPRESSION_META_KEY.to_string(),
6702                "none".to_string(),
6703            ),
6704            (
6705                DICT_VALUES_COMPRESSION_LEVEL_META_KEY.to_string(),
6706                "7".to_string(),
6707            ),
6708        ]);
6709        let metadata = PrimitiveStructuralEncoder::resolve_dict_values_compression_metadata(
6710            &field_metadata,
6711            Some("zstd".to_string()),
6712            Some("3".to_string()),
6713        );
6714        assert_eq!(
6715            metadata.get(COMPRESSION_META_KEY),
6716            Some(&"none".to_string()),
6717        );
6718        assert_eq!(
6719            metadata.get(COMPRESSION_LEVEL_META_KEY),
6720            Some(&"7".to_string()),
6721        );
6722    }
6723
6724    #[test]
6725    fn test_resolve_dict_values_compression_metadata_env_fallback() {
6726        let metadata = PrimitiveStructuralEncoder::resolve_dict_values_compression_metadata(
6727            &HashMap::new(),
6728            Some("zstd".to_string()),
6729            Some("9".to_string()),
6730        );
6731        assert_eq!(
6732            metadata.get(COMPRESSION_META_KEY),
6733            Some(&"zstd".to_string()),
6734        );
6735        assert_eq!(
6736            metadata.get(COMPRESSION_LEVEL_META_KEY),
6737            Some(&"9".to_string()),
6738        );
6739    }
6740
6741    #[tokio::test]
6742    async fn test_dictionary_encode_int64() {
6743        use crate::constants::{DICT_SIZE_RATIO_META_KEY, STRUCTURAL_ENCODING_META_KEY};
6744        use crate::testing::{TestCases, check_round_trip_encoding_of_data};
6745        use crate::version::LanceFileVersion;
6746        use arrow_array::{ArrayRef, Int64Array};
6747        use std::collections::HashMap;
6748        use std::sync::Arc;
6749
6750        // Low cardinality with poor RLE opportunity.
6751        let values = (0..1000)
6752            .map(|i| match i % 3 {
6753                0 => 10i64,
6754                1 => 20i64,
6755                _ => 30i64,
6756            })
6757            .collect::<Vec<_>>();
6758        let array = Arc::new(Int64Array::from(values)) as ArrayRef;
6759
6760        let mut metadata = HashMap::new();
6761        metadata.insert(
6762            STRUCTURAL_ENCODING_META_KEY.to_string(),
6763            STRUCTURAL_ENCODING_MINIBLOCK.to_string(),
6764        );
6765        metadata.insert(DICT_SIZE_RATIO_META_KEY.to_string(), "0.99".to_string());
6766
6767        let test_cases = TestCases::default()
6768            .with_min_file_version(LanceFileVersion::V2_2)
6769            .with_batch_size(1000)
6770            .with_range(0..1000)
6771            .with_indices(vec![0, 1, 10, 999])
6772            .with_expected_encoding("dictionary");
6773
6774        check_round_trip_encoding_of_data(vec![array], &test_cases, metadata).await;
6775    }
6776
6777    #[tokio::test]
6778    async fn test_dictionary_encode_float64() {
6779        use crate::constants::{DICT_SIZE_RATIO_META_KEY, STRUCTURAL_ENCODING_META_KEY};
6780        use crate::testing::{TestCases, check_round_trip_encoding_of_data};
6781        use crate::version::LanceFileVersion;
6782        use arrow_array::{ArrayRef, Float64Array};
6783        use std::collections::HashMap;
6784        use std::sync::Arc;
6785
6786        // Low cardinality with poor RLE opportunity.
6787        let values = (0..1000)
6788            .map(|i| match i % 3 {
6789                0 => 0.1f64,
6790                1 => 0.2f64,
6791                _ => 0.3f64,
6792            })
6793            .collect::<Vec<_>>();
6794        let array = Arc::new(Float64Array::from(values)) as ArrayRef;
6795
6796        let mut metadata = HashMap::new();
6797        metadata.insert(
6798            STRUCTURAL_ENCODING_META_KEY.to_string(),
6799            STRUCTURAL_ENCODING_MINIBLOCK.to_string(),
6800        );
6801        metadata.insert(DICT_SIZE_RATIO_META_KEY.to_string(), "0.99".to_string());
6802
6803        let test_cases = TestCases::default()
6804            .with_min_file_version(LanceFileVersion::V2_2)
6805            .with_batch_size(1000)
6806            .with_range(0..1000)
6807            .with_indices(vec![0, 1, 10, 999])
6808            .with_expected_encoding("dictionary");
6809
6810        check_round_trip_encoding_of_data(vec![array], &test_cases, metadata).await;
6811    }
6812
6813    #[test]
6814    fn test_miniblock_dictionary_out_of_line_bitpacking_decode() {
6815        let rows = 10_000;
6816        let unique_values = 2_000;
6817
6818        let dictionary_encoding =
6819            ProtobufUtils21::out_of_line_bitpacking(64, ProtobufUtils21::flat(11, None));
6820        let layout = pb21::MiniBlockLayout {
6821            rep_compression: None,
6822            def_compression: None,
6823            value_compression: Some(ProtobufUtils21::flat(64, None)),
6824            dictionary: Some(dictionary_encoding),
6825            num_dictionary_items: unique_values,
6826            layers: vec![pb21::RepDefLayer::RepdefAllValidItem as i32],
6827            num_buffers: 1,
6828            repetition_index_depth: 0,
6829            num_items: rows,
6830            has_large_chunk: false,
6831        };
6832
6833        let buffer_offsets_and_sizes = vec![(0, 0), (0, 0), (0, 0)];
6834        let scheduler = super::MiniBlockScheduler::try_new(
6835            &buffer_offsets_and_sizes,
6836            /*priority=*/ 0,
6837            /*items_in_page=*/ rows,
6838            &layout,
6839            &DefaultDecompressionStrategy::default(),
6840        )
6841        .unwrap();
6842
6843        let dictionary = scheduler.dictionary.unwrap();
6844        assert_eq!(dictionary.num_dictionary_items, unique_values);
6845        assert_eq!(
6846            dictionary.dictionary_data_alignment,
6847            crate::encoder::MIN_PAGE_BUFFER_ALIGNMENT
6848        );
6849    }
6850
6851    // Dictionary encoding decision tests
6852    fn create_test_fixed_data_block(
6853        num_values: u64,
6854        cardinality: u64,
6855        bits_per_value: u64,
6856    ) -> DataBlock {
6857        assert!(cardinality > 0);
6858        assert!(cardinality <= num_values);
6859        let block_info = BlockInfo::default();
6860
6861        assert_eq!(bits_per_value % 8, 0);
6862        let data = match bits_per_value {
6863            32 => {
6864                let values = (0..num_values)
6865                    .map(|i| (i % cardinality) as u32)
6866                    .collect::<Vec<_>>();
6867                crate::buffer::LanceBuffer::reinterpret_vec(values)
6868            }
6869            64 => {
6870                let values = (0..num_values).map(|i| i % cardinality).collect::<Vec<_>>();
6871                crate::buffer::LanceBuffer::reinterpret_vec(values)
6872            }
6873            128 => {
6874                let values = (0..num_values)
6875                    .map(|i| (i % cardinality) as u128)
6876                    .collect::<Vec<_>>();
6877                crate::buffer::LanceBuffer::reinterpret_vec(values)
6878            }
6879            _ => unreachable!(),
6880        };
6881        DataBlock::FixedWidth(FixedWidthDataBlock {
6882            bits_per_value,
6883            data,
6884            num_values,
6885            block_info,
6886        })
6887    }
6888
6889    /// Helper to create VariableWidth (string) test data block with exact cardinality
6890    fn create_test_variable_width_block(num_values: u64, cardinality: u64) -> DataBlock {
6891        use arrow_array::StringArray;
6892
6893        assert!(cardinality <= num_values && cardinality > 0);
6894
6895        let mut values = Vec::with_capacity(num_values as usize);
6896        for i in 0..num_values {
6897            values.push(format!("value_{:016}", i % cardinality));
6898        }
6899
6900        let array = StringArray::from(values);
6901        DataBlock::from_array(Arc::new(array) as ArrayRef)
6902    }
6903
6904    #[test]
6905    fn test_should_dictionary_encode() {
6906        use crate::constants::DICT_SIZE_RATIO_META_KEY;
6907        use lance_core::datatypes::Field as LanceField;
6908
6909        // Create data where dict encoding saves space
6910        let block = create_test_variable_width_block(1000, 10);
6911
6912        let mut metadata = HashMap::new();
6913        metadata.insert(DICT_SIZE_RATIO_META_KEY.to_string(), "0.8".to_string());
6914        let arrow_field =
6915            arrow_schema::Field::new("test", DataType::Utf8, false).with_metadata(metadata);
6916        let field = LanceField::try_from(&arrow_field).unwrap();
6917
6918        let result = PrimitiveStructuralEncoder::should_dictionary_encode(
6919            &block,
6920            &field,
6921            LanceFileVersion::V2_1,
6922        );
6923
6924        assert!(
6925            result.is_some(),
6926            "Should use dictionary encode based on size"
6927        );
6928    }
6929
6930    #[test]
6931    fn test_should_not_dictionary_encode_unsupported_bits() {
6932        use crate::constants::DICT_SIZE_RATIO_META_KEY;
6933        use lance_core::datatypes::Field as LanceField;
6934
6935        let block = create_test_fixed_data_block(1000, 1000, 32);
6936
6937        let mut metadata = HashMap::new();
6938        metadata.insert(DICT_SIZE_RATIO_META_KEY.to_string(), "0.8".to_string());
6939        let arrow_field =
6940            arrow_schema::Field::new("test", DataType::Int32, false).with_metadata(metadata);
6941        let field = LanceField::try_from(&arrow_field).unwrap();
6942
6943        let result = PrimitiveStructuralEncoder::should_dictionary_encode(
6944            &block,
6945            &field,
6946            LanceFileVersion::V2_1,
6947        );
6948
6949        assert!(
6950            result.is_none(),
6951            "Should not use dictionary encode for unsupported bit width"
6952        );
6953    }
6954
6955    #[test]
6956    fn test_should_not_dictionary_encode_near_unique_sample() {
6957        use crate::constants::DICT_SIZE_RATIO_META_KEY;
6958        use lance_core::datatypes::Field as LanceField;
6959
6960        let num_values = 5000;
6961        let block = create_test_variable_width_block(num_values, num_values);
6962
6963        let mut metadata = HashMap::new();
6964        metadata.insert(DICT_SIZE_RATIO_META_KEY.to_string(), "1.0".to_string());
6965        let arrow_field =
6966            arrow_schema::Field::new("test", DataType::Utf8, false).with_metadata(metadata);
6967        let field = LanceField::try_from(&arrow_field).unwrap();
6968
6969        let result = PrimitiveStructuralEncoder::should_dictionary_encode(
6970            &block,
6971            &field,
6972            LanceFileVersion::V2_1,
6973        );
6974
6975        assert!(
6976            result.is_none(),
6977            "Should not probe dictionary encoding for near-unique data"
6978        );
6979    }
6980
6981    async fn encode_first_page(
6982        field: arrow_schema::Field,
6983        array: ArrayRef,
6984        version: LanceFileVersion,
6985    ) -> crate::encoder::EncodedPage {
6986        use crate::encoder::{
6987            ColumnIndexSequence, EncodingOptions, MIN_PAGE_BUFFER_ALIGNMENT, OutOfLineBuffers,
6988            default_encoding_strategy,
6989        };
6990        use crate::repdef::RepDefBuilder;
6991
6992        let lance_field = lance_core::datatypes::Field::try_from(&field).unwrap();
6993        let encoding_strategy = default_encoding_strategy(version);
6994        let mut column_index_seq = ColumnIndexSequence::default();
6995        let encoding_options = EncodingOptions {
6996            cache_bytes_per_column: 1,
6997            max_page_bytes: 32 * 1024 * 1024,
6998            keep_original_array: true,
6999            buffer_alignment: MIN_PAGE_BUFFER_ALIGNMENT,
7000            version,
7001        };
7002
7003        let mut encoder = encoding_strategy
7004            .create_field_encoder(
7005                encoding_strategy.as_ref(),
7006                &lance_field,
7007                &mut column_index_seq,
7008                &encoding_options,
7009            )
7010            .unwrap();
7011
7012        let mut external_buffers = OutOfLineBuffers::new(0, MIN_PAGE_BUFFER_ALIGNMENT);
7013        let repdef = RepDefBuilder::default();
7014        let num_rows = array.len() as u64;
7015        let mut pages = Vec::new();
7016        for task in encoder
7017            .maybe_encode(array, &mut external_buffers, repdef, 0, num_rows)
7018            .unwrap()
7019        {
7020            pages.push(task.await.unwrap());
7021        }
7022        for task in encoder.flush(&mut external_buffers).unwrap() {
7023            pages.push(task.await.unwrap());
7024        }
7025        pages.into_iter().next().unwrap()
7026    }
7027
7028    #[tokio::test]
7029    async fn test_constant_layout_out_of_line_fixed_size_binary_v2_2() {
7030        use crate::format::pb21::page_layout::Layout;
7031
7032        let val = vec![0xABu8; 33];
7033        let arr: ArrayRef = Arc::new(
7034            arrow_array::FixedSizeBinaryArray::try_from_sparse_iter_with_size(
7035                std::iter::repeat_n(Some(val.as_slice()), 256),
7036                33,
7037            )
7038            .unwrap(),
7039        );
7040        let field = arrow_schema::Field::new("c", DataType::FixedSizeBinary(33), true);
7041        let page = encode_first_page(field, arr.clone(), LanceFileVersion::V2_2).await;
7042
7043        let PageEncoding::Structural(layout) = &page.description else {
7044            panic!("Expected structural encoding");
7045        };
7046        let Layout::ConstantLayout(layout) = layout.layout.as_ref().unwrap() else {
7047            panic!("Expected constant layout in slot 2");
7048        };
7049        assert!(layout.inline_value.is_none());
7050        assert_eq!(page.data.len(), 1);
7051
7052        let test_cases = TestCases::default()
7053            .with_min_file_version(LanceFileVersion::V2_2)
7054            .with_max_file_version(LanceFileVersion::V2_2)
7055            .with_page_sizes(vec![4096]);
7056        check_round_trip_encoding_of_data(vec![arr], &test_cases, HashMap::new()).await;
7057    }
7058
7059    #[tokio::test]
7060    async fn test_constant_layout_out_of_line_utf8_v2_2() {
7061        use crate::format::pb21::page_layout::Layout;
7062
7063        let arr: ArrayRef = Arc::new(arrow_array::StringArray::from_iter_values(
7064            std::iter::repeat_n("hello", 512),
7065        ));
7066        let field = arrow_schema::Field::new("c", DataType::Utf8, true);
7067        let page = encode_first_page(field, arr.clone(), LanceFileVersion::V2_2).await;
7068
7069        let PageEncoding::Structural(layout) = &page.description else {
7070            panic!("Expected structural encoding");
7071        };
7072        let Layout::ConstantLayout(layout) = layout.layout.as_ref().unwrap() else {
7073            panic!("Expected constant layout in slot 2");
7074        };
7075        assert!(layout.inline_value.is_none());
7076        assert_eq!(page.data.len(), 1);
7077
7078        let test_cases = TestCases::default()
7079            .with_min_file_version(LanceFileVersion::V2_2)
7080            .with_max_file_version(LanceFileVersion::V2_2)
7081            .with_page_sizes(vec![4096]);
7082        check_round_trip_encoding_of_data(vec![arr], &test_cases, HashMap::new()).await;
7083    }
7084
7085    #[tokio::test]
7086    async fn test_constant_layout_nullable_item_v2_2() {
7087        use crate::format::pb21::page_layout::Layout;
7088
7089        let arr: ArrayRef = Arc::new(arrow_array::Int32Array::from(vec![
7090            Some(7),
7091            None,
7092            Some(7),
7093            None,
7094            Some(7),
7095        ]));
7096        let field = arrow_schema::Field::new("c", DataType::Int32, true);
7097        let page = encode_first_page(field, arr.clone(), LanceFileVersion::V2_2).await;
7098
7099        let PageEncoding::Structural(layout) = &page.description else {
7100            panic!("Expected structural encoding");
7101        };
7102        let Layout::ConstantLayout(layout) = layout.layout.as_ref().unwrap() else {
7103            panic!("Expected constant layout in slot 2");
7104        };
7105        assert!(layout.inline_value.is_some());
7106        assert_eq!(page.data.len(), 2);
7107
7108        let test_cases = TestCases::default()
7109            .with_min_file_version(LanceFileVersion::V2_2)
7110            .with_max_file_version(LanceFileVersion::V2_2)
7111            .with_page_sizes(vec![4096]);
7112        check_round_trip_encoding_of_data(vec![arr], &test_cases, HashMap::new()).await;
7113    }
7114
7115    #[tokio::test]
7116    async fn test_constant_layout_list_repdef_v2_2() {
7117        use crate::format::pb21::page_layout::Layout;
7118        use arrow_array::builder::{Int32Builder, ListBuilder};
7119
7120        let mut builder = ListBuilder::new(Int32Builder::new());
7121        builder.values().append_value(7);
7122        builder.values().append_null();
7123        builder.values().append_value(7);
7124        builder.append(true);
7125
7126        builder.append(true);
7127
7128        builder.values().append_value(7);
7129        builder.append(true);
7130
7131        builder.append_null();
7132
7133        let arr: ArrayRef = Arc::new(builder.finish());
7134        let field = arrow_schema::Field::new(
7135            "c",
7136            DataType::List(Arc::new(arrow_schema::Field::new(
7137                "item",
7138                DataType::Int32,
7139                true,
7140            ))),
7141            true,
7142        );
7143        let page = encode_first_page(field, arr.clone(), LanceFileVersion::V2_2).await;
7144
7145        let PageEncoding::Structural(layout) = &page.description else {
7146            panic!("Expected structural encoding");
7147        };
7148        let Layout::ConstantLayout(layout) = layout.layout.as_ref().unwrap() else {
7149            panic!("Expected constant layout in slot 2");
7150        };
7151        assert!(layout.inline_value.is_some());
7152        assert_eq!(page.data.len(), 2);
7153
7154        let test_cases = TestCases::default()
7155            .with_min_file_version(LanceFileVersion::V2_2)
7156            .with_max_file_version(LanceFileVersion::V2_2)
7157            .with_page_sizes(vec![4096]);
7158        check_round_trip_encoding_of_data(vec![arr], &test_cases, HashMap::new()).await;
7159    }
7160
7161    #[tokio::test]
7162    async fn test_constant_layout_fixed_size_list_not_used_v2_2() {
7163        use crate::format::pb21::page_layout::Layout;
7164        use arrow_array::builder::{FixedSizeListBuilder, Int32Builder};
7165
7166        let mut builder = FixedSizeListBuilder::new(Int32Builder::new(), 3);
7167        for _ in 0..64 {
7168            builder.values().append_value(1);
7169            builder.values().append_null();
7170            builder.values().append_value(3);
7171            builder.append(true);
7172        }
7173        let arr: ArrayRef = Arc::new(builder.finish());
7174        let field = arrow_schema::Field::new(
7175            "c",
7176            DataType::FixedSizeList(
7177                Arc::new(arrow_schema::Field::new("item", DataType::Int32, true)),
7178                3,
7179            ),
7180            true,
7181        );
7182        let page = encode_first_page(field, arr.clone(), LanceFileVersion::V2_2).await;
7183
7184        if let PageEncoding::Structural(layout) = &page.description {
7185            assert!(
7186                !matches!(layout.layout.as_ref().unwrap(), Layout::ConstantLayout(_)),
7187                "FixedSizeList should not use constant layout yet"
7188            );
7189        }
7190
7191        let test_cases = TestCases::default()
7192            .with_min_file_version(LanceFileVersion::V2_2)
7193            .with_max_file_version(LanceFileVersion::V2_2)
7194            .with_page_sizes(vec![4096]);
7195        check_round_trip_encoding_of_data(vec![arr], &test_cases, HashMap::new()).await;
7196    }
7197
7198    #[tokio::test]
7199    async fn test_constant_layout_not_written_before_v2_2() {
7200        use crate::format::pb21::page_layout::Layout;
7201
7202        let arr: ArrayRef = Arc::new(arrow_array::Int32Array::from(vec![7; 1024]));
7203        let field = arrow_schema::Field::new("c", DataType::Int32, true);
7204        let page = encode_first_page(field, arr.clone(), LanceFileVersion::V2_1).await;
7205
7206        let PageEncoding::Structural(layout) = &page.description else {
7207            return;
7208        };
7209        assert!(
7210            !matches!(layout.layout.as_ref().unwrap(), Layout::ConstantLayout(_)),
7211            "Should not emit constant layout before v2.2"
7212        );
7213
7214        let test_cases = TestCases::default()
7215            .with_min_file_version(LanceFileVersion::V2_1)
7216            .with_max_file_version(LanceFileVersion::V2_1)
7217            .with_page_sizes(vec![4096]);
7218        check_round_trip_encoding_of_data(vec![arr], &test_cases, HashMap::new()).await;
7219    }
7220
7221    #[tokio::test]
7222    async fn test_all_null_constant_layout_still_works_v2_2() {
7223        use crate::format::pb21::page_layout::Layout;
7224
7225        let arr: ArrayRef = Arc::new(arrow_array::Int32Array::from(vec![None, None, None]));
7226        let field = arrow_schema::Field::new("c", DataType::Int32, true);
7227        let page = encode_first_page(field, arr.clone(), LanceFileVersion::V2_2).await;
7228
7229        let PageEncoding::Structural(layout) = &page.description else {
7230            panic!("Expected structural encoding");
7231        };
7232        let Layout::ConstantLayout(layout) = layout.layout.as_ref().unwrap() else {
7233            panic!("Expected layout in slot 2");
7234        };
7235        assert!(layout.inline_value.is_none());
7236        assert_eq!(page.data.len(), 0);
7237
7238        let test_cases = TestCases::default()
7239            .with_min_file_version(LanceFileVersion::V2_2)
7240            .with_max_file_version(LanceFileVersion::V2_2)
7241            .with_page_sizes(vec![4096]);
7242        check_round_trip_encoding_of_data(vec![arr], &test_cases, HashMap::new()).await;
7243    }
7244
7245    #[test]
7246    fn test_encode_decode_complex_all_null_vals_roundtrip() {
7247        use crate::compression::{
7248            DecompressionStrategy, DefaultCompressionStrategy, DefaultDecompressionStrategy,
7249        };
7250
7251        let values: Arc<[u16]> = Arc::from((0..2048).map(|i| (i % 5) as u16).collect::<Vec<u16>>());
7252
7253        let compression_strategy = DefaultCompressionStrategy::default();
7254        let decompression_strategy = DefaultDecompressionStrategy::default();
7255
7256        let (compressed_buf, encoding) = PrimitiveStructuralEncoder::encode_complex_all_null_vals(
7257            &values,
7258            &compression_strategy,
7259        )
7260        .unwrap();
7261
7262        let decompressor = decompression_strategy
7263            .create_block_decompressor(&encoding)
7264            .unwrap();
7265        let decompressed = decompressor
7266            .decompress(compressed_buf, values.len() as u64)
7267            .unwrap();
7268        let decompressed_fixed_width = decompressed.as_fixed_width().unwrap();
7269        assert_eq!(decompressed_fixed_width.num_values, values.len() as u64);
7270        assert_eq!(decompressed_fixed_width.bits_per_value, 16);
7271        let rep_result = decompressed_fixed_width.data.borrow_to_typed_slice::<u16>();
7272        assert_eq!(rep_result.as_ref(), values.as_ref());
7273    }
7274
7275    #[tokio::test]
7276    async fn test_complex_all_null_compression_gated_by_version() {
7277        use crate::format::pb21::page_layout::Layout;
7278        use arrow_array::ListArray;
7279
7280        let list_array = ListArray::from_iter_primitive::<arrow_array::types::Int32Type, _, _>(
7281            (0..1000).map(|i| if i % 2 == 0 { None } else { Some(vec![]) }),
7282        );
7283        let arr: ArrayRef = Arc::new(list_array);
7284        let field = arrow_schema::Field::new(
7285            "c",
7286            DataType::List(Arc::new(arrow_schema::Field::new(
7287                "item",
7288                DataType::Int32,
7289                true,
7290            ))),
7291            true,
7292        );
7293
7294        let page_v21 = encode_first_page(field.clone(), arr.clone(), LanceFileVersion::V2_1).await;
7295        let PageEncoding::Structural(layout_v21) = &page_v21.description else {
7296            panic!("Expected structural encoding");
7297        };
7298        let Layout::ConstantLayout(layout_v21) = layout_v21.layout.as_ref().unwrap() else {
7299            panic!("Expected constant layout");
7300        };
7301        assert!(layout_v21.rep_compression.is_none());
7302        assert!(layout_v21.def_compression.is_none());
7303        assert_eq!(layout_v21.num_rep_values, 0);
7304        assert_eq!(layout_v21.num_def_values, 0);
7305
7306        let page_v22 = encode_first_page(field, arr, LanceFileVersion::V2_2).await;
7307        let PageEncoding::Structural(layout_v22) = &page_v22.description else {
7308            panic!("Expected structural encoding");
7309        };
7310        let Layout::ConstantLayout(layout_v22) = layout_v22.layout.as_ref().unwrap() else {
7311            panic!("Expected constant layout");
7312        };
7313        assert!(layout_v22.def_compression.is_some());
7314        assert!(layout_v22.num_def_values > 0);
7315    }
7316
7317    #[tokio::test]
7318    async fn test_complex_all_null_round_trip() {
7319        use arrow_array::ListArray;
7320
7321        let list_array = ListArray::from_iter_primitive::<arrow_array::types::Int32Type, _, _>(
7322            (0..1000).map(|i| if i % 2 == 0 { None } else { Some(vec![]) }),
7323        );
7324
7325        let test_cases = TestCases::default().with_min_file_version(LanceFileVersion::V2_2);
7326        check_round_trip_encoding_of_data(vec![Arc::new(list_array)], &test_cases, HashMap::new())
7327            .await;
7328    }
7329    fn truncated_tail_details() -> std::sync::Arc<super::FullZipDecodeDetails> {
7330        use crate::compression::VariablePerValueDecompressor;
7331        use crate::encodings::physical::binary::VariableDecoder;
7332        use crate::repdef::{ControlWordParser, DefinitionInterpretation};
7333        use std::sync::Arc;
7334        Arc::new(super::FullZipDecodeDetails {
7335            value_decompressor: super::PerValueDecompressor::Variable(Arc::new(
7336                VariableDecoder::default(),
7337            )
7338                as Arc<dyn VariablePerValueDecompressor>),
7339            def_meaning: vec![DefinitionInterpretation::NullableItem].into(),
7340            ctrl_word_parser: ControlWordParser::new(0, 0),
7341            max_rep: 0,
7342            max_visible_def: 0,
7343        })
7344    }
7345
7346    fn decode_variable_full_zip(
7347        buf: Vec<u8>,
7348        bits_per_offset: u8,
7349    ) -> lance_core::Result<super::VariableFullZipDecoder> {
7350        use std::collections::VecDeque;
7351        let mut data = VecDeque::new();
7352        data.push_back(crate::buffer::LanceBuffer::from(buf));
7353        super::VariableFullZipDecoder::new(
7354            truncated_tail_details(),
7355            data,
7356            1,
7357            bits_per_offset,
7358            bits_per_offset,
7359        )
7360    }
7361
7362    /// A page whose item walk ends with a partial length prefix must surface a
7363    /// corrupt-file error rather than read past the end of the buffer.
7364    ///
7365    /// This asserts the error variant and message rather than merely expecting a
7366    /// panic: before the length prefix was bounds checked, the read was
7367    /// `get_unchecked` behind a `debug_assert!`, so a debug build panicked here
7368    /// (which a `#[should_panic]` test would have accepted as a pass) while a
7369    /// release build read up to 8 bytes out of a 4 byte allocation.
7370    #[test]
7371    fn variable_full_zip_truncated_length_prefix_is_corrupt_file() {
7372        use lance_core::Error;
7373
7374        for (bits, buf_len) in [(32u8, 3usize), (64u8, 4usize)] {
7375            let err = decode_variable_full_zip(vec![0xAA; buf_len], bits)
7376                .expect_err("a truncated length prefix must not decode");
7377            assert!(
7378                matches!(err, Error::CorruptFile { .. }),
7379                "expected CorruptFile for a {}-bit prefix with {} byte(s), got: {:?}",
7380                bits,
7381                buf_len,
7382                err
7383            );
7384            let msg = err.to_string();
7385            assert!(
7386                msg.contains("truncated length prefix"),
7387                "error should say what is wrong, got: {msg}"
7388            );
7389        }
7390    }
7391}