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        STRUCTURAL_ENCODING_SPARSE,
19    },
20    data::DictionaryDataBlock,
21    encodings::logical::primitive::blob::{BlobDescriptionPageScheduler, BlobPageScheduler},
22    format::{
23        ProtobufUtils21,
24        pb21::{self, CompressiveEncoding, PageLayout, compressive_encoding::Compression},
25    },
26};
27use arrow_array::{
28    Array, ArrayRef, PrimitiveArray, cast::AsArray, make_array, new_null_array, types::UInt64Type,
29};
30use arrow_buffer::{BooleanBuffer, BooleanBufferBuilder, NullBuffer, ScalarBuffer};
31use arrow_schema::{DataType, Field as ArrowField};
32use bytes::Bytes;
33use futures::{FutureExt, TryStreamExt, future::BoxFuture, stream::FuturesOrdered};
34use itertools::Itertools;
35use lance_arrow::DataTypeExt;
36use lance_arrow::deepcopy::deep_copy_nulls;
37use lance_core::{
38    cache::{CacheKey, CacheKeySchema, Context, DeepSizeOf, KeyBuilder},
39    error::{Error, LanceOptionExt},
40    utils::bit::pad_bytes,
41};
42use log::{debug, trace};
43
44use crate::encodings::logical::primitive::miniblock::MiniBlockChunk;
45use crate::encodings::physical::rle::{RleDecompressor, RleRuns};
46use crate::utils::bytepack::ByteUnpacker;
47use crate::{
48    compression::{
49        BlockDecompressor, CompressionStrategy, DecompressionStrategy, MiniBlockDecompressor,
50        compress_required_block, create_rle_decompressor,
51    },
52    data::{AllNullDataBlock, DataBlock, VariableWidthBlock},
53    utils::bytepack::BytepackedIntegerEncoder,
54};
55use crate::{
56    compression::{FixedPerValueDecompressor, VariablePerValueDecompressor},
57    encodings::logical::primitive::fullzip::PerValueDataBlock,
58};
59use crate::{
60    encodings::logical::primitive::miniblock::{MiniBlockCompressed, MiniBlockCompressionContext},
61    statistics::{ComputeStat, GetStat, Stat},
62};
63use crate::{
64    repdef::{
65        CompositeRepDefUnraveler, ControlWordIterator, ControlWordParser, DefinitionInterpretation,
66        MiniBlockRepDefBudget, NormalizedStructuralPlan, RepDefSlicer, SerializedRepDefs,
67        build_control_word_iterator,
68    },
69    utils::accumulation::AccumulationQueue,
70};
71use lance_core::{Result, datatypes::Field, utils::tokio::spawn_cpu};
72
73use crate::constants::{
74    COMPRESSION_LEVEL_META_KEY, COMPRESSION_META_KEY, DICT_DIVISOR_META_KEY,
75    DICT_SIZE_RATIO_META_KEY, DICT_VALUES_COMPRESSION_ENV_VAR,
76    DICT_VALUES_COMPRESSION_LEVEL_ENV_VAR, DICT_VALUES_COMPRESSION_LEVEL_META_KEY,
77    DICT_VALUES_COMPRESSION_META_KEY,
78};
79use crate::{
80    EncodingsIo,
81    buffer::LanceBuffer,
82    data::{BlockInfo, DataBlockBuilder, FixedWidthDataBlock},
83    decoder::{
84        ColumnInfo, DecodePageTask, DecodedArray, DecodedPage, FilterExpression, LoadedPageShard,
85        MessageType, PageEncoding, PageInfo, ScheduledScanLine, SchedulerContext,
86        StructuralDecodeArrayTask, StructuralFieldDecoder, StructuralFieldScheduler,
87        StructuralPageDecoder, StructuralSchedulingJob, UnloadedPageShard,
88    },
89    encoder::{
90        EncodeTask, EncodedColumn, EncodedPage, EncodingOptions, FieldEncoder, OutOfLineBuffers,
91    },
92    repdef::{LevelBuffer, RepDefBuilder, RepDefUnraveler},
93};
94
95pub mod blob;
96mod chunk_index;
97pub mod constant;
98pub mod dict;
99pub mod fullzip;
100mod layout;
101pub mod miniblock;
102pub(crate) mod sparse;
103
104use chunk_index::{ItemCounts, MiniBlockChunkIndex, PrefixSums, RowMapping, parse_nested_rep};
105
106const FILL_BYTE: u8 = 0xFE;
107const DEFAULT_DICT_DIVISOR: u64 = 2;
108const DEFAULT_DICT_MAX_CARDINALITY: u64 = 100_000;
109const DEFAULT_DICT_SIZE_RATIO: f64 = 0.8;
110const DEFAULT_DICT_VALUES_COMPRESSION: &str = "lz4";
111
112/// Largest level count a direct legacy u16-value/U8-run RLE frame can prove.
113///
114/// Mini-block level payload sizes are u16. After the eight-byte frame header, each run needs a
115/// two-byte value and a one-byte length, and each U8 run can represent at most 255 levels.
116const MAX_LEGACY_RLE_LEVELS: u64 = ((u16::MAX as u64 - std::mem::size_of::<u64>() as u64)
117    / (std::mem::size_of::<u16>() as u64 + std::mem::size_of::<u8>() as u64))
118    * u8::MAX as u64;
119
120struct PageLoadTask {
121    decoder_fut: BoxFuture<'static, Result<Box<dyn StructuralPageDecoder>>>,
122    num_rows: u64,
123}
124
125/// A trait for figuring out how to schedule the data within
126/// a single page.
127trait StructuralPageScheduler: std::fmt::Debug + Send {
128    /// Fetches any metadata required for the page
129    fn initialize<'a>(
130        &'a mut self,
131        io: &Arc<dyn EncodingsIo>,
132    ) -> BoxFuture<'a, Result<Arc<dyn CachedPageData>>>;
133    /// Loads metadata from a previous initialize call
134    fn load(&mut self, data: &Arc<dyn CachedPageData>);
135    /// Schedules the read of the given ranges in the page
136    ///
137    /// The read may be split into multiple "shards" if the page is extremely large.
138    /// Each shard maps to one or more rows and can be decoded independently.
139    ///
140    /// Note: this sharding is for splitting up very large pages into smaller reads to
141    /// avoid buffering too much data in memory.  It is not related to the batch size or
142    /// compute units in any way.
143    fn schedule_ranges(
144        &self,
145        ranges: &[Range<u64>],
146        io: &Arc<dyn EncodingsIo>,
147    ) -> Result<Vec<PageLoadTask>>;
148}
149
150/// Metadata describing the decoded size of a mini-block
151#[derive(Debug)]
152struct ChunkMeta {
153    num_values: u64,
154    chunk_size_bytes: u64,
155    offset_bytes: u64,
156}
157
158/// A mini-block chunk that has been decoded and decompressed
159#[derive(Debug, Clone)]
160struct DecodedMiniBlockChunk {
161    rep: Option<ScalarBuffer<u16>>,
162    def: Option<ScalarBuffer<u16>>,
163    values: DataBlock,
164}
165
166/// A task to decode a one or more mini-blocks of data into an output batch
167///
168/// Note: Two batches might share the same mini-block of data.  When this happens
169/// then each batch gets a copy of the block and each batch decodes the block independently.
170///
171/// This means we have duplicated work but it is necessary to avoid having to synchronize
172/// the decoding of the block. (TODO: test this theory)
173#[derive(Debug)]
174struct DecodeMiniBlockTask {
175    rep_decompressor: Option<Arc<dyn BlockDecompressor>>,
176    def_decompressor: Option<Arc<dyn BlockDecompressor>>,
177    value_decompressor: Arc<dyn MiniBlockDecompressor>,
178    dictionary_data: Option<Arc<DataBlock>>,
179    def_meaning: Arc<[DefinitionInterpretation]>,
180    num_buffers: u64,
181    max_visible_level: u16,
182    instructions: Vec<(ChunkDrainInstructions, LoadedChunk)>,
183    has_large_chunk: bool,
184}
185
186impl DecodeMiniBlockTask {
187    fn decoded_size_bytes(&self) -> Option<u64> {
188        if self.rep_decompressor.is_some() || self.def_decompressor.is_some() {
189            return None;
190        }
191        let num_values = self
192            .instructions
193            .iter()
194            .try_fold(0_u64, |total, (instruction, _)| {
195                total.checked_add(instruction.rows_to_take)
196            })?;
197        self.value_decompressor.decoded_size_bytes(num_values)
198    }
199
200    fn resolve_num_levels(
201        rep_decompressor: Option<&dyn BlockDecompressor>,
202        rep_levels: Option<&LanceBuffer>,
203        def_decompressor: Option<&dyn BlockDecompressor>,
204        def_levels: Option<&LanceBuffer>,
205        declared_num_levels: u16,
206    ) -> Result<u64> {
207        let rep_num_levels = match (rep_decompressor, rep_levels) {
208            (Some(decompressor), Some(levels)) => decompressor.infer_num_values(levels)?,
209            (None, None) => None,
210            _ => {
211                return Err(Error::invalid_input_source(
212                    "miniblock repetition codec and payload presence disagree".into(),
213                ));
214            }
215        };
216        let def_num_levels = match (def_decompressor, def_levels) {
217            (Some(decompressor), Some(levels)) => decompressor.infer_num_values(levels)?,
218            (None, None) => None,
219            _ => {
220                return Err(Error::invalid_input_source(
221                    "miniblock definition codec and payload presence disagree".into(),
222                ));
223            }
224        };
225
226        let inferred_num_levels = match (rep_num_levels, def_num_levels) {
227            (Some(rep_num_levels), Some(def_num_levels)) if rep_num_levels != def_num_levels => {
228                return Err(Error::invalid_input_source(
229                    format!(
230                        "miniblock structural streams disagree on the level count: repetition inferred {rep_num_levels}, definition inferred {def_num_levels}"
231                    )
232                    .into(),
233                ));
234            }
235            (Some(num_levels), _) | (_, Some(num_levels)) => num_levels,
236            (None, None) => return Ok(u64::from(declared_num_levels)),
237        };
238
239        let declared_num_levels = u64::from(declared_num_levels);
240        if inferred_num_levels == declared_num_levels {
241            return Ok(inferred_num_levels);
242        }
243        let u16_modulus = u64::from(u16::MAX) + 1;
244        if inferred_num_levels <= declared_num_levels
245            || inferred_num_levels % u16_modulus != declared_num_levels
246        {
247            return Err(Error::invalid_input_source(
248                format!(
249                    "miniblock payload proves {inferred_num_levels} levels but the header declared {declared_num_levels}; the counts are not congruent modulo 65536"
250                )
251                .into(),
252            ));
253        }
254        if inferred_num_levels > MAX_LEGACY_RLE_LEVELS {
255            return Err(Error::invalid_input_source(
256                format!(
257                    "miniblock payload proves {inferred_num_levels} levels, exceeding the legacy RLE payload bound of {MAX_LEGACY_RLE_LEVELS}"
258                )
259                .into(),
260            ));
261        }
262        Ok(inferred_num_levels)
263    }
264
265    fn decode_levels(
266        decompressor: &dyn BlockDecompressor,
267        levels: LanceBuffer,
268        expected_num_levels: u64,
269    ) -> Result<ScalarBuffer<u16>> {
270        let levels = decompressor.decompress(Some(levels), expected_num_levels)?;
271        let levels = levels.as_fixed_width().ok_or_else(|| {
272            Error::invalid_input_source(
273                "miniblock levels did not decode to fixed-width data".into(),
274            )
275        })?;
276        if levels.num_values != expected_num_levels {
277            return Err(Error::invalid_input_source(
278                format!(
279                    "miniblock levels decoded {} values, expected {expected_num_levels}",
280                    levels.num_values
281                )
282                .into(),
283            ));
284        }
285        if levels.bits_per_value != 16 {
286            return Err(Error::invalid_input_source(
287                format!(
288                    "miniblock levels decoded {} bits per value, expected 16",
289                    levels.bits_per_value
290                )
291                .into(),
292            ));
293        }
294        let expected_bytes = expected_num_levels.checked_mul(2).ok_or_else(|| {
295            Error::invalid_input_source(
296                format!("miniblock level byte count overflowed for {expected_num_levels} levels")
297                    .into(),
298            )
299        })?;
300        if levels.data.len() as u64 != expected_bytes {
301            return Err(Error::invalid_input_source(
302                format!(
303                    "miniblock levels decoded {} bytes, expected {expected_bytes}",
304                    levels.data.len()
305                )
306                .into(),
307            ));
308        }
309        Ok(levels.data.borrow_to_typed_slice::<u16>())
310    }
311
312    // We are building a LevelBuffer (levels) and want to copy into it `total_len`
313    // values from `level_buf` starting at `offset`.
314    //
315    // We need to handle both the case where `levels` is None (no nulls encountered
316    // yet) and the case where `level_buf` is None (the input we are copying from has
317    // no nulls)
318    fn extend_levels(
319        range: Range<u64>,
320        levels: &mut Option<LevelBuffer>,
321        level_buf: &Option<impl AsRef<[u16]>>,
322        dest_offset: usize,
323    ) {
324        if let Some(level_buf) = level_buf {
325            if levels.is_none() {
326                // This is the first non-empty def buf we've hit, fill in the past
327                // with 0 (valid)
328                let mut new_levels_vec =
329                    LevelBuffer::with_capacity(dest_offset + (range.end - range.start) as usize);
330                new_levels_vec.extend(iter::repeat_n(0, dest_offset));
331                *levels = Some(new_levels_vec);
332            }
333            levels.as_mut().unwrap().extend(
334                level_buf.as_ref()[range.start as usize..range.end as usize]
335                    .iter()
336                    .copied(),
337            );
338        } else if let Some(levels) = levels {
339            let num_values = (range.end - range.start) as usize;
340            // This is an all-valid level_buf but we had nulls earlier and so we
341            // need to materialize it
342            levels.extend(iter::repeat_n(0, num_values));
343        }
344    }
345
346    /// Maps a range of rows to a range of items and a range of levels
347    ///
348    /// If there is no repetition information this just returns the range as-is.
349    ///
350    /// If there is repetition information then we need to do some work to figure out what
351    /// range of items corresponds to the requested range of rows.
352    ///
353    /// For example, if the data is [[1, 2, 3], [4, 5], [6, 7]] and the range is 1..2 (i.e. just row
354    /// 1) then the user actually wants items 3..5.  In the above case the rep levels would be:
355    ///
356    /// Idx: 0 1 2 3 4 5 6
357    /// Rep: 1 0 0 1 0 1 0
358    ///
359    /// So the start (1) maps to the second 1 (idx=3) and the end (2) maps to the third 1 (idx=5)
360    ///
361    /// If there are invisible items then we don't count them when calculating the range of items we
362    /// are interested in but we do count them when calculating the range of levels we are interested
363    /// in.  As a result we have to return both the item range (first return value) and the level range
364    /// (second return value).
365    ///
366    /// For example, if the data is [[1, 2, 3], [4, 5], NULL, [6, 7, 8]] and the range is 2..4 then the
367    /// user wants items 5..8 but they want levels 5..9.  In the above case the rep/def levels would be:
368    ///
369    /// Idx: 0 1 2 3 4 5 6 7 8
370    /// Rep: 1 0 0 1 0 1 1 0 0
371    /// Def: 0 0 0 0 0 1 0 0 0
372    /// Itm: 1 2 3 4 5 6 7 8
373    ///
374    /// Finally, we have to contend with the fact that chunks may or may not start with a "preamble" of
375    /// trailing values that finish up a list from the previous chunk.  In this case the first item does
376    /// not start at max_rep because it is a continuation of the previous chunk.  For our purposes we do
377    /// not consider this a "row" and so the range 0..1 will refer to the first row AFTER the preamble.
378    ///
379    /// We have a separate parameter (`preamble_action`) to control whether we want the preamble or not.
380    ///
381    /// Note that the "trailer" is considered a "row" and if we want it we should include it in the range.
382    fn map_range(
383        range: Range<u64>,
384        rep: Option<&impl AsRef<[u16]>>,
385        def: Option<&impl AsRef<[u16]>>,
386        max_rep: u16,
387        max_visible_def: u16,
388        // The total number of items (not rows) in the chunk.  This is not quite the same as
389        // rep.len() / def.len() because it doesn't count invisible items
390        total_items: u64,
391        preamble_action: PreambleAction,
392    ) -> (Range<u64>, Range<u64>) {
393        if let Some(rep) = rep {
394            let mut rep = rep.as_ref();
395            // If there is a preamble and we need to skip it then do that first.  The work is the same
396            // whether there is def information or not
397            let mut items_in_preamble = 0_u64;
398            let first_row_start = match preamble_action {
399                PreambleAction::Skip | PreambleAction::Take => {
400                    let first_row_start = if let Some(def) = def.as_ref() {
401                        let mut first_row_start = None;
402                        for (idx, (rep, def)) in rep.iter().zip(def.as_ref()).enumerate() {
403                            if *rep == max_rep {
404                                first_row_start = Some(idx as u64);
405                                break;
406                            }
407                            if *def <= max_visible_def {
408                                items_in_preamble += 1;
409                            }
410                        }
411                        first_row_start
412                    } else {
413                        let first_row_start =
414                            rep.iter().position(|&r| r == max_rep).map(|r| r as u64);
415                        items_in_preamble = first_row_start.unwrap_or(rep.len() as u64);
416                        first_row_start
417                    };
418                    // It is possible for a chunk to be entirely partial values but if it is then it
419                    // should never show up as a preamble to skip
420                    if first_row_start.is_none() {
421                        assert!(preamble_action == PreambleAction::Take);
422                        return (0..total_items, 0..rep.len() as u64);
423                    }
424                    let first_row_start = first_row_start.unwrap();
425                    rep = &rep[first_row_start as usize..];
426                    first_row_start
427                }
428                PreambleAction::Absent => {
429                    debug_assert!(rep[0] == max_rep);
430                    0
431                }
432            };
433
434            // We hit this case when all we needed was the preamble
435            if range.start == range.end {
436                debug_assert!(preamble_action == PreambleAction::Take);
437                debug_assert!(items_in_preamble <= total_items);
438                return (0..items_in_preamble, 0..first_row_start);
439            }
440            assert!(range.start < range.end);
441
442            let mut rows_seen = 0;
443            let mut new_start = 0;
444            let mut new_levels_start = 0;
445
446            if let Some(def) = def {
447                let def = &def.as_ref()[first_row_start as usize..];
448
449                // range.start == 0 always maps to 0 (even with invis items), otherwise we need to walk
450                let mut lead_invis_seen = 0;
451
452                if range.start > 0 {
453                    if def[0] > max_visible_def {
454                        lead_invis_seen += 1;
455                    }
456                    for (idx, (rep, def)) in rep.iter().zip(def).skip(1).enumerate() {
457                        if *rep == max_rep {
458                            rows_seen += 1;
459                            if rows_seen == range.start {
460                                new_start = idx as u64 + 1 - lead_invis_seen;
461                                new_levels_start = idx as u64 + 1;
462                                break;
463                            }
464                        }
465                        if *def > max_visible_def {
466                            lead_invis_seen += 1;
467                        }
468                    }
469                }
470
471                rows_seen += 1;
472
473                let mut new_end = u64::MAX;
474                let mut new_levels_end = rep.len() as u64;
475                let new_start_is_visible = def[new_levels_start as usize] <= max_visible_def;
476                let mut tail_invis_seen = if new_start_is_visible { 0 } else { 1 };
477                for (idx, (rep, def)) in rep[(new_levels_start + 1) as usize..]
478                    .iter()
479                    .zip(&def[(new_levels_start + 1) as usize..])
480                    .enumerate()
481                {
482                    if *rep == max_rep {
483                        rows_seen += 1;
484                        if rows_seen == range.end + 1 {
485                            new_end = idx as u64 + new_start + 1 - tail_invis_seen;
486                            new_levels_end = idx as u64 + new_levels_start + 1;
487                            break;
488                        }
489                    }
490                    if *def > max_visible_def {
491                        tail_invis_seen += 1;
492                    }
493                }
494
495                if new_end == u64::MAX {
496                    new_levels_end = rep.len() as u64;
497                    let total_invis_seen = lead_invis_seen + tail_invis_seen;
498                    new_end = rep.len() as u64 - total_invis_seen;
499                }
500
501                assert_ne!(new_end, u64::MAX);
502
503                // Adjust for any skipped preamble
504                if preamble_action == PreambleAction::Skip {
505                    new_start += items_in_preamble;
506                    new_end += items_in_preamble;
507                    new_levels_start += first_row_start;
508                    new_levels_end += first_row_start;
509                } else if preamble_action == PreambleAction::Take {
510                    debug_assert_eq!(new_start, 0);
511                    debug_assert_eq!(new_levels_start, 0);
512                    new_end += items_in_preamble;
513                    new_levels_end += first_row_start;
514                }
515
516                debug_assert!(new_end <= total_items);
517                (new_start..new_end, new_levels_start..new_levels_end)
518            } else {
519                // Easy case, there are no invisible items, so we don't need to check for them
520                // The items range and levels range will be the same.  We do still need to walk
521                // the rep levels to find the row boundaries
522
523                // range.start == 0 always maps to 0, otherwise we need to walk
524                if range.start > 0 {
525                    for (idx, rep) in rep.iter().skip(1).enumerate() {
526                        if *rep == max_rep {
527                            rows_seen += 1;
528                            if rows_seen == range.start {
529                                new_start = idx as u64 + 1;
530                                break;
531                            }
532                        }
533                    }
534                }
535                let mut new_end = rep.len() as u64;
536                // range.end == max_items always maps to rep.len(), otherwise we need to walk
537                if range.end < total_items {
538                    for (idx, rep) in rep[(new_start + 1) as usize..].iter().enumerate() {
539                        if *rep == max_rep {
540                            rows_seen += 1;
541                            if rows_seen == range.end {
542                                new_end = idx as u64 + new_start + 1;
543                                break;
544                            }
545                        }
546                    }
547                }
548
549                // Adjust for any skipped preamble
550                if preamble_action == PreambleAction::Skip {
551                    new_start += first_row_start;
552                    new_end += first_row_start;
553                } else if preamble_action == PreambleAction::Take {
554                    debug_assert_eq!(new_start, 0);
555                    new_end += first_row_start;
556                }
557
558                debug_assert!(new_end <= total_items);
559                (new_start..new_end, new_start..new_end)
560            }
561        } else {
562            // No repetition info, easy case, just use the range as-is and the item
563            // and level ranges are the same
564            (range.clone(), range)
565        }
566    }
567
568    // read `num_buffers` buffer sizes from `buf` starting at `offset`
569    fn read_buffer_sizes<const LARGE: bool>(
570        buf: &[u8],
571        offset: &mut usize,
572        num_buffers: u64,
573    ) -> Vec<u32> {
574        let read_size = if LARGE { 4 } else { 2 };
575        (0..num_buffers)
576            .map(|_| {
577                let bytes = &buf[*offset..*offset + read_size];
578                let size = if LARGE {
579                    u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
580                } else {
581                    // the buffer size is read from u16 but is stored as u32 after decoding for consistency
582                    u16::from_le_bytes([bytes[0], bytes[1]]) as u32
583                };
584                *offset += read_size;
585                size
586            })
587            .collect()
588    }
589
590    // Unserialize a miniblock into a collection of vectors
591    fn decode_miniblock_chunk(
592        &self,
593        buf: &LanceBuffer,
594        items_in_chunk: u64,
595    ) -> Result<DecodedMiniBlockChunk> {
596        let mut offset = 0;
597        let num_levels = u16::from_le_bytes([buf[offset], buf[offset + 1]]);
598        offset += 2;
599
600        let rep_size = if self.rep_decompressor.is_some() {
601            let rep_size = u16::from_le_bytes([buf[offset], buf[offset + 1]]);
602            offset += 2;
603            Some(rep_size)
604        } else {
605            None
606        };
607        let def_size = if self.def_decompressor.is_some() {
608            let def_size = u16::from_le_bytes([buf[offset], buf[offset + 1]]);
609            offset += 2;
610            Some(def_size)
611        } else {
612            None
613        };
614
615        let buffer_sizes = if self.has_large_chunk {
616            Self::read_buffer_sizes::<true>(buf, &mut offset, self.num_buffers)
617        } else {
618            Self::read_buffer_sizes::<false>(buf, &mut offset, self.num_buffers)
619        };
620
621        offset += pad_bytes::<MINIBLOCK_ALIGNMENT>(offset);
622
623        let rep = rep_size.map(|rep_size| {
624            let rep = buf.slice_with_length(offset, rep_size as usize);
625            offset += rep_size as usize;
626            offset += pad_bytes::<MINIBLOCK_ALIGNMENT>(offset);
627            rep
628        });
629
630        let def = def_size.map(|def_size| {
631            let def = buf.slice_with_length(offset, def_size as usize);
632            offset += def_size as usize;
633            offset += pad_bytes::<MINIBLOCK_ALIGNMENT>(offset);
634            def
635        });
636
637        let num_levels = Self::resolve_num_levels(
638            self.rep_decompressor.as_deref(),
639            rep.as_ref(),
640            self.def_decompressor.as_deref(),
641            def.as_ref(),
642            num_levels,
643        )?;
644
645        let buffers = buffer_sizes
646            .into_iter()
647            .map(|buf_size| {
648                let buf = buf.slice_with_length(offset, buf_size as usize);
649                offset += buf_size as usize;
650                offset += pad_bytes::<MINIBLOCK_ALIGNMENT>(offset);
651                buf
652            })
653            .collect::<Vec<_>>();
654
655        let values = self
656            .value_decompressor
657            .decompress(buffers, items_in_chunk)?;
658
659        let rep = rep
660            .map(|rep| {
661                Self::decode_levels(
662                    self.rep_decompressor.as_ref().unwrap().as_ref(),
663                    rep,
664                    num_levels,
665                )
666            })
667            .transpose()?;
668        let def = def
669            .map(|def| {
670                Self::decode_levels(
671                    self.def_decompressor.as_ref().unwrap().as_ref(),
672                    def,
673                    num_levels,
674                )
675            })
676            .transpose()?;
677
678        if let (Some(rep), Some(def)) = (&rep, &def)
679            && rep.len() != def.len()
680        {
681            return Err(Error::invalid_input_source(
682                format!(
683                    "miniblock structural streams decoded different level counts: repetition {}, definition {}",
684                    rep.len(),
685                    def.len()
686                )
687                .into(),
688            ));
689        }
690
691        Ok(DecodedMiniBlockChunk { rep, def, values })
692    }
693}
694
695impl DecodePageTask for DecodeMiniBlockTask {
696    fn decode(self: Box<Self>) -> Result<DecodedPage> {
697        // First, we create output buffers for the rep and def and data
698        let mut repbuf: Option<LevelBuffer> = None;
699        let mut defbuf: Option<LevelBuffer> = None;
700
701        let max_rep = self.def_meaning.iter().filter(|l| l.is_list()).count() as u16;
702
703        let estimated_size_bytes = self.decoded_size_bytes().unwrap_or_else(|| {
704            // Variable-width and rep/def encoded output sizes are not known before decoding.
705            self.instructions
706                .iter()
707                .map(|(_, chunk)| chunk.data.len() as u64)
708                .sum::<u64>()
709                * 2
710        });
711        let mut data_builder = DataBlockBuilder::with_capacity_estimate(estimated_size_bytes);
712
713        // We need to keep track of the offset into repbuf/defbuf that we are building up
714        let mut level_offset = 0;
715
716        // Pre-compute caching needs for each chunk by checking if the next chunk is the same
717        let needs_caching: Vec<bool> = self
718            .instructions
719            .windows(2)
720            .map(|w| w[0].1.chunk_idx == w[1].1.chunk_idx)
721            .chain(std::iter::once(false)) // the last one never needs caching
722            .collect();
723
724        // Cache for storing decoded chunks when beneficial
725        let mut chunk_cache: Option<(usize, DecodedMiniBlockChunk)> = None;
726
727        // Now we iterate through each instruction and process it
728        for (idx, (instructions, chunk)) in self.instructions.iter().enumerate() {
729            let should_cache_this_chunk = needs_caching[idx];
730
731            let decoded_chunk = match &chunk_cache {
732                Some((cached_chunk_idx, cached_chunk)) if *cached_chunk_idx == chunk.chunk_idx => {
733                    // Clone only when we have a cache hit (much cheaper than decoding)
734                    cached_chunk.clone()
735                }
736                _ => {
737                    // Cache miss, need to decode
738                    let decoded = self.decode_miniblock_chunk(&chunk.data, chunk.items_in_chunk)?;
739
740                    // Only update cache if this chunk will benefit the next access
741                    if should_cache_this_chunk {
742                        chunk_cache = Some((chunk.chunk_idx, decoded.clone()));
743                    }
744                    decoded
745                }
746            };
747
748            let DecodedMiniBlockChunk { rep, def, values } = decoded_chunk;
749
750            // Our instructions tell us which rows we want to take from this chunk
751            let row_range_start =
752                instructions.rows_to_skip + instructions.chunk_instructions.rows_to_skip;
753            let row_range_end = row_range_start + instructions.rows_to_take;
754
755            // We use the rep info to map the row range to an item range / levels range
756            let (item_range, level_range) = Self::map_range(
757                row_range_start..row_range_end,
758                rep.as_ref(),
759                def.as_ref(),
760                max_rep,
761                self.max_visible_level,
762                chunk.items_in_chunk,
763                instructions.preamble_action,
764            );
765            if item_range.end - item_range.start > chunk.items_in_chunk {
766                return Err(lance_core::Error::internal(format!(
767                    "Item range {:?} is greater than chunk items in chunk {:?}",
768                    item_range, chunk.items_in_chunk
769                )));
770            }
771
772            // Now we append the data to the output buffers
773            Self::extend_levels(level_range.clone(), &mut repbuf, &rep, level_offset);
774            Self::extend_levels(level_range.clone(), &mut defbuf, &def, level_offset);
775            level_offset += (level_range.end - level_range.start) as usize;
776            data_builder.append(&values, item_range)?;
777        }
778
779        let mut data = data_builder.finish();
780
781        let unraveler =
782            RepDefUnraveler::new(repbuf, defbuf, self.def_meaning.clone(), data.num_values());
783
784        if let Some(dictionary) = &self.dictionary_data {
785            // Don't decode here, that happens later (if needed)
786            let DataBlock::FixedWidth(indices) = data else {
787                return Err(lance_core::Error::internal(format!(
788                    "Expected FixedWidth DataBlock for dictionary indices, got {:?}",
789                    data
790                )));
791            };
792            data = DataBlock::Dictionary(DictionaryDataBlock::from_parts(
793                indices,
794                dictionary.as_ref().clone(),
795            ));
796        }
797
798        Ok(DecodedPage {
799            data,
800            repdef: unraveler,
801        })
802    }
803}
804
805/// A chunk that has been loaded by the miniblock scheduler (but not
806/// yet decoded)
807#[derive(Debug)]
808struct LoadedChunk {
809    data: LanceBuffer,
810    items_in_chunk: u64,
811    byte_range: Range<u64>,
812    chunk_idx: usize,
813}
814
815impl Clone for LoadedChunk {
816    fn clone(&self) -> Self {
817        Self {
818            // Safe as we always create borrowed buffers here
819            data: self.data.clone(),
820            items_in_chunk: self.items_in_chunk,
821            byte_range: self.byte_range.clone(),
822            chunk_idx: self.chunk_idx,
823        }
824    }
825}
826
827/// Decodes mini-block formatted data.  See [`PrimitiveStructuralEncoder`] for more
828/// details on the different layouts.
829#[derive(Debug)]
830struct MiniBlockDecoder {
831    rep_decompressor: Option<Arc<dyn BlockDecompressor>>,
832    def_decompressor: Option<Arc<dyn BlockDecompressor>>,
833    value_decompressor: Arc<dyn MiniBlockDecompressor>,
834    def_meaning: Arc<[DefinitionInterpretation]>,
835    loaded_chunks: VecDeque<LoadedChunk>,
836    instructions: VecDeque<ChunkInstructions>,
837    offset_in_current_chunk: u64,
838    num_rows: u64,
839    num_buffers: u64,
840    dictionary: Option<Arc<DataBlock>>,
841    has_large_chunk: bool,
842}
843
844/// See [`MiniBlockScheduler`] for more details on the scheduling and decoding
845/// process for miniblock encoded data.
846impl StructuralPageDecoder for MiniBlockDecoder {
847    fn drain(&mut self, num_rows: u64) -> Result<Box<dyn DecodePageTask>> {
848        let mut items_desired = num_rows;
849        let mut need_preamble = false;
850        let mut skip_in_chunk = self.offset_in_current_chunk;
851        let mut drain_instructions = Vec::new();
852        while items_desired > 0 || need_preamble {
853            let (instructions, consumed) = self
854                .instructions
855                .front()
856                .unwrap()
857                .drain_from_instruction(&mut items_desired, &mut need_preamble, &mut skip_in_chunk);
858
859            while self.loaded_chunks.front().unwrap().chunk_idx
860                != instructions.chunk_instructions.chunk_idx
861            {
862                self.loaded_chunks.pop_front();
863            }
864            drain_instructions.push((instructions, self.loaded_chunks.front().unwrap().clone()));
865            if consumed {
866                self.instructions.pop_front();
867            }
868        }
869        // We can throw away need_preamble here because it must be false.  If it were true it would mean
870        // we were still in the middle of loading rows.  We do need to latch skip_in_chunk though.
871        self.offset_in_current_chunk = skip_in_chunk;
872
873        let max_visible_level = self
874            .def_meaning
875            .iter()
876            .take_while(|l| !l.is_list())
877            .map(|l| l.num_def_levels())
878            .sum::<u16>();
879
880        Ok(Box::new(DecodeMiniBlockTask {
881            instructions: drain_instructions,
882            def_decompressor: self.def_decompressor.clone(),
883            rep_decompressor: self.rep_decompressor.clone(),
884            value_decompressor: self.value_decompressor.clone(),
885            dictionary_data: self.dictionary.clone(),
886            def_meaning: self.def_meaning.clone(),
887            num_buffers: self.num_buffers,
888            max_visible_level,
889            has_large_chunk: self.has_large_chunk,
890        }))
891    }
892
893    fn num_rows(&self) -> u64 {
894        self.num_rows
895    }
896}
897
898/// How a complex-all-null page's rep/def level buffer is compressed on disk.
899/// Captured at scheduler construction so `initialize` can keep RLE levels in run
900/// form instead of expanding them.
901#[derive(Debug, Clone)]
902pub(crate) enum LevelCodec {
903    /// Raw little-endian u16 levels (no block compression).
904    Uncompressed,
905    /// RLE-compressed levels; the validated physical runs select their cached representation.
906    Rle(Arc<RleDecompressor>),
907    /// Any other block compression; decoded eagerly into [`LazyLevels::Dense`]
908    /// (these encodings don't expand, so laziness buys nothing).
909    Block(Arc<dyn BlockDecompressor>),
910}
911
912impl LevelCodec {
913    fn try_new(
914        encoding: Option<&CompressiveEncoding>,
915        decompression_strategy: &dyn DecompressionStrategy,
916    ) -> Result<Self> {
917        match encoding {
918            None => Ok(Self::Uncompressed),
919            Some(encoding) => match encoding.compression.as_ref() {
920                Some(Compression::Rle(rle)) => Ok(Self::Rle(Arc::new(create_rle_decompressor(
921                    rle,
922                    decompression_strategy,
923                )?))),
924                _ => Ok(Self::Block(Arc::from(
925                    decompression_strategy.create_block_decompressor(encoding)?,
926                ))),
927            },
928        }
929    }
930}
931
932#[derive(Debug)]
933enum RunEnds {
934    U16(Box<[u16]>),
935    U32(Box<[u32]>),
936    U64(Box<[u64]>),
937}
938
939impl RunEnds {
940    fn width_for(num_values: usize) -> usize {
941        if u16::try_from(num_values).is_ok() {
942            std::mem::size_of::<u16>()
943        } else if u32::try_from(num_values).is_ok() {
944            std::mem::size_of::<u32>()
945        } else {
946            std::mem::size_of::<u64>()
947        }
948    }
949
950    fn len(&self) -> usize {
951        match self {
952            Self::U16(ends) => ends.len(),
953            Self::U32(ends) => ends.len(),
954            Self::U64(ends) => ends.len(),
955        }
956    }
957
958    fn get(&self, run: usize) -> usize {
959        match self {
960            Self::U16(ends) => ends[run] as usize,
961            Self::U32(ends) => ends[run] as usize,
962            Self::U64(ends) => ends[run] as usize,
963        }
964    }
965
966    fn partition_point(&self, logical_index: usize) -> usize {
967        match self {
968            Self::U16(ends) => ends.partition_point(|&end| end as usize <= logical_index),
969            Self::U32(ends) => ends.partition_point(|&end| end as usize <= logical_index),
970            Self::U64(ends) => ends.partition_point(|&end| end as usize <= logical_index),
971        }
972    }
973
974    fn deep_size(&self) -> usize {
975        match self {
976            Self::U16(ends) => std::mem::size_of_val(ends.as_ref()),
977            Self::U32(ends) => std::mem::size_of_val(ends.as_ref()),
978            Self::U64(ends) => std::mem::size_of_val(ends.as_ref()),
979        }
980    }
981}
982
983enum RunEndsBuilder {
984    U16(Vec<u16>),
985    U32(Vec<u32>),
986    U64(Vec<u64>),
987}
988
989impl RunEndsBuilder {
990    fn with_capacity(num_values: usize, capacity: usize) -> Self {
991        if u16::try_from(num_values).is_ok() {
992            Self::U16(Vec::with_capacity(capacity))
993        } else if u32::try_from(num_values).is_ok() {
994            Self::U32(Vec::with_capacity(capacity))
995        } else {
996            Self::U64(Vec::with_capacity(capacity))
997        }
998    }
999
1000    fn push(&mut self, end: usize) -> Result<()> {
1001        match self {
1002            Self::U16(ends) => ends.push(
1003                u16::try_from(end)
1004                    .map_err(|_| Error::internal(format!("Run end {end} does not fit in u16")))?,
1005            ),
1006            Self::U32(ends) => ends.push(
1007                u32::try_from(end)
1008                    .map_err(|_| Error::internal(format!("Run end {end} does not fit in u32")))?,
1009            ),
1010            Self::U64(ends) => ends.push(end as u64),
1011        }
1012        Ok(())
1013    }
1014
1015    fn set_last(&mut self, end: usize) -> Result<()> {
1016        match self {
1017            Self::U16(ends) => {
1018                let last = ends.last_mut().ok_or_else(|| {
1019                    Error::internal("Cannot extend an empty coalesced run buffer")
1020                })?;
1021                *last = u16::try_from(end)
1022                    .map_err(|_| Error::internal(format!("Run end {end} does not fit in u16")))?;
1023            }
1024            Self::U32(ends) => {
1025                let last = ends.last_mut().ok_or_else(|| {
1026                    Error::internal("Cannot extend an empty coalesced run buffer")
1027                })?;
1028                *last = u32::try_from(end)
1029                    .map_err(|_| Error::internal(format!("Run end {end} does not fit in u32")))?;
1030            }
1031            Self::U64(ends) => {
1032                let last = ends.last_mut().ok_or_else(|| {
1033                    Error::internal("Cannot extend an empty coalesced run buffer")
1034                })?;
1035                *last = end as u64;
1036            }
1037        }
1038        Ok(())
1039    }
1040
1041    fn finish(self) -> RunEnds {
1042        match self {
1043            Self::U16(ends) => RunEnds::U16(ends.into_boxed_slice()),
1044            Self::U32(ends) => RunEnds::U32(ends.into_boxed_slice()),
1045            Self::U64(ends) => RunEnds::U64(ends.into_boxed_slice()),
1046        }
1047    }
1048}
1049
1050#[derive(Debug)]
1051enum RunStorage {
1052    Physical(RleRuns),
1053    Coalesced { values: Box<[u16]>, ends: RunEnds },
1054}
1055
1056impl RunStorage {
1057    fn len(&self) -> usize {
1058        match self {
1059            Self::Physical(runs) => runs.num_values(),
1060            Self::Coalesced { ends, .. } => ends.get(ends.len() - 1),
1061        }
1062    }
1063
1064    fn num_runs(&self) -> usize {
1065        match self {
1066            Self::Physical(runs) => runs.num_runs(),
1067            Self::Coalesced { values, .. } => values.len(),
1068        }
1069    }
1070
1071    fn value(&self, run: usize) -> u16 {
1072        match self {
1073            Self::Physical(runs) => runs.value(run),
1074            Self::Coalesced { values, .. } => values[run],
1075        }
1076    }
1077
1078    fn first_value_above(&self, max: u16) -> Option<(usize, u16)> {
1079        (0..self.num_runs()).find_map(|run| {
1080            let value = self.value(run);
1081            (value > max).then_some((run, value))
1082        })
1083    }
1084
1085    fn seek(&self, position: &mut RunPosition, logical_index: usize) {
1086        if logical_index >= self.len() {
1087            *position = RunPosition {
1088                run: self.num_runs(),
1089                start: self.len(),
1090                end: self.len(),
1091            };
1092            return;
1093        }
1094
1095        match self {
1096            Self::Physical(runs) => {
1097                if position.run >= runs.num_runs()
1098                    || position.end == 0
1099                    || logical_index < position.start
1100                {
1101                    *position = RunPosition {
1102                        run: 0,
1103                        start: 0,
1104                        end: runs.length(0),
1105                    };
1106                }
1107                while position.end <= logical_index {
1108                    self.advance(position);
1109                }
1110            }
1111            Self::Coalesced { ends, .. } => {
1112                if logical_index < position.start || logical_index >= position.end {
1113                    let run = ends.partition_point(logical_index);
1114                    *position = RunPosition {
1115                        run,
1116                        start: if run == 0 { 0 } else { ends.get(run - 1) },
1117                        end: ends.get(run),
1118                    };
1119                }
1120            }
1121        }
1122    }
1123
1124    fn advance(&self, position: &mut RunPosition) {
1125        let next_run = position.run + 1;
1126        if next_run >= self.num_runs() {
1127            *position = RunPosition {
1128                run: self.num_runs(),
1129                start: self.len(),
1130                end: self.len(),
1131            };
1132            return;
1133        }
1134
1135        let start = position.end;
1136        position.run = next_run;
1137        position.start = start;
1138        position.end = match self {
1139            Self::Physical(runs) => start + runs.length(next_run),
1140            Self::Coalesced { ends, .. } => ends.get(next_run),
1141        };
1142    }
1143
1144    fn deep_size(&self) -> usize {
1145        match self {
1146            Self::Physical(runs) => runs.deep_size(),
1147            Self::Coalesced { values, ends } => {
1148                std::mem::size_of_val(values.as_ref()) + ends.deep_size()
1149            }
1150        }
1151    }
1152}
1153
1154/// Rep/def levels for a complex-all-null page.
1155///
1156/// RLE pages retain the smallest of their validated physical runs, coalesced
1157/// runs, and dense values. The decoder materializes only the per-drain slices
1158/// it touches.
1159#[derive(Debug, Clone)]
1160enum LazyLevels {
1161    Dense(ScalarBuffer<u16>),
1162    Runs(Arc<RunStorage>),
1163}
1164
1165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1166enum LevelPlan {
1167    Physical,
1168    Coalesced,
1169    Dense,
1170}
1171
1172#[derive(Debug, Default, Clone, Copy)]
1173struct RunPosition {
1174    run: usize,
1175    start: usize,
1176    end: usize,
1177}
1178
1179/// Monotonic forward cursor into a [`LazyLevels`] sequence.
1180///
1181/// Drains seek to strictly increasing rows, so each [`LazyLevels::seek_row_start`]
1182/// resumes from the last position instead of rescanning — every run is visited at
1183/// most once per page while locating and counting level ranges.
1184#[derive(Debug, Default, Clone, Copy)]
1185struct LevelCursor {
1186    /// Logical level index where the current row begins.
1187    level: usize,
1188    /// Row index at `level` (the number of `max_rep` occurrences before it).
1189    row: u64,
1190    /// Run containing `level`. Unused for [`LazyLevels::Dense`].
1191    run: RunPosition,
1192}
1193
1194impl LazyLevels {
1195    fn from_rle_runs(runs: RleRuns) -> Result<Self> {
1196        let plan = Self::select_plan(&runs);
1197        match plan {
1198            LevelPlan::Physical => Ok(Self::Runs(Arc::new(RunStorage::Physical(
1199                runs.into_owned(),
1200            )))),
1201            LevelPlan::Coalesced => Self::build_coalesced(runs),
1202            LevelPlan::Dense => Self::build_dense(runs),
1203        }
1204    }
1205
1206    /// Minimize retained payload bytes first, then expected traversal work.
1207    /// If both are equal, keep the physical runs and avoid another allocation.
1208    fn select_plan(runs: &RleRuns) -> LevelPlan {
1209        if runs.num_values() == 0 {
1210            return LevelPlan::Dense;
1211        }
1212
1213        let run_storage_size = std::mem::size_of::<RunStorage>() as u128;
1214        let physical_size = run_storage_size + runs.owned_size() as u128;
1215        let coalesced_size = run_storage_size
1216            + (runs.coalesced_runs() as u128)
1217                * (std::mem::size_of::<u16>() + RunEnds::width_for(runs.num_values())) as u128;
1218        let dense_size = (runs.num_values() as u128) * std::mem::size_of::<u16>() as u128;
1219        [
1220            (physical_size, runs.num_runs(), 0usize, LevelPlan::Physical),
1221            (
1222                coalesced_size,
1223                runs.coalesced_runs(),
1224                1usize,
1225                LevelPlan::Coalesced,
1226            ),
1227            (dense_size, runs.num_values(), 2usize, LevelPlan::Dense),
1228        ]
1229        .into_iter()
1230        .min_by_key(|(size, traversal, priority, _)| (*size, *traversal, *priority))
1231        .map(|(_, _, _, plan)| plan)
1232        .unwrap_or(LevelPlan::Dense)
1233    }
1234
1235    fn build_coalesced(runs: RleRuns) -> Result<Self> {
1236        let mut values = Vec::with_capacity(runs.coalesced_runs());
1237        let mut ends = RunEndsBuilder::with_capacity(runs.num_values(), runs.coalesced_runs());
1238        let mut logical_end = 0usize;
1239        for (value, length) in runs.iter() {
1240            logical_end = logical_end
1241                .checked_add(length)
1242                .ok_or_else(|| Error::internal("Validated RLE run length sum overflowed usize"))?;
1243            if values.last().copied() == Some(value) {
1244                ends.set_last(logical_end)?;
1245            } else {
1246                values.push(value);
1247                ends.push(logical_end)?;
1248            }
1249        }
1250        Ok(Self::Runs(Arc::new(RunStorage::Coalesced {
1251            values: values.into_boxed_slice(),
1252            ends: ends.finish(),
1253        })))
1254    }
1255
1256    fn build_dense(runs: RleRuns) -> Result<Self> {
1257        let mut values = Vec::new();
1258        values.try_reserve_exact(runs.num_values()).map_err(|_| {
1259            Error::internal(format!(
1260                "Cannot allocate {} dense repetition/definition levels",
1261                runs.num_values()
1262            ))
1263        })?;
1264        for (value, length) in runs.iter() {
1265            values.resize(values.len() + length, value);
1266        }
1267        Ok(Self::Dense(ScalarBuffer::from(values)))
1268    }
1269
1270    fn len(&self) -> usize {
1271        match self {
1272            Self::Dense(buf) => buf.len(),
1273            Self::Runs(runs) => runs.len(),
1274        }
1275    }
1276
1277    fn validate_max(&self, level_type: &str, max: u16) -> Result<()> {
1278        let invalid = match self {
1279            Self::Dense(levels) => levels
1280                .iter()
1281                .enumerate()
1282                .find_map(|(index, &value)| (value > max).then_some(("index", index, value))),
1283            Self::Runs(runs) => runs
1284                .first_value_above(max)
1285                .map(|(run, value)| ("run", run, value)),
1286        };
1287        if let Some((position_type, position, value)) = invalid {
1288            return Err(Error::invalid_input_source(
1289                format!(
1290                    "Invalid {level_type} level {value} at {position_type} {position}: maximum is {max}"
1291                )
1292                .into(),
1293            ));
1294        }
1295        Ok(())
1296    }
1297
1298    /// Advance `cursor` to the start of row `target_row`, returning that row's
1299    /// starting level index.
1300    ///
1301    /// Rows begin at `max_rep` positions, so this finds the `target_row`-th one.
1302    /// `target_row` must be `>= cursor.row`: the cursor only moves forward, which
1303    /// is what keeps a full page decode O(runs) rather than O(rows).
1304    fn seek_row_start(
1305        &self,
1306        cursor: &mut LevelCursor,
1307        target_row: u64,
1308        max_rep: u16,
1309    ) -> Result<usize> {
1310        let mut need = target_row.checked_sub(cursor.row).ok_or_else(|| {
1311            Error::internal(format!(
1312                "Complex all-null row ranges are not sorted: target row {target_row} follows {}",
1313                cursor.row
1314            ))
1315        })?;
1316        if need == 0 {
1317            return Ok(cursor.level);
1318        }
1319        match self {
1320            Self::Dense(buf) => {
1321                let mut level = cursor.level;
1322                while need > 0 {
1323                    if level >= buf.len() {
1324                        return Err(Error::internal(
1325                            "Invalid complex all-null layout: repetition buffer too short",
1326                        ));
1327                    }
1328                    if buf[level] != max_rep {
1329                        return Err(Error::internal(
1330                            "Invalid complex all-null layout: row did not start at max repetition level",
1331                        ));
1332                    }
1333                    level += 1;
1334                    while level < buf.len() && buf[level] != max_rep {
1335                        level += 1;
1336                    }
1337                    need -= 1;
1338                }
1339                cursor.level = level;
1340                cursor.row = target_row;
1341                Ok(level)
1342            }
1343            Self::Runs(runs) => {
1344                let mut level = cursor.level;
1345                let mut run = cursor.run;
1346                runs.seek(&mut run, level);
1347                while need > 0 {
1348                    if run.run >= runs.num_runs() {
1349                        return Err(Error::internal(
1350                            "Invalid complex all-null layout: repetition buffer too short",
1351                        ));
1352                    }
1353                    if runs.value(run.run) != max_rep {
1354                        return Err(Error::internal(
1355                            "Invalid complex all-null layout: row did not start at max repetition level",
1356                        ));
1357                    }
1358                    let avail = (run.end - level) as u64;
1359                    if need < avail {
1360                        // Target lands inside this max-rep run.
1361                        level += need as usize;
1362                        need = 0;
1363                    } else {
1364                        // Consume every row start in this run, then skip the
1365                        // trailing non-max-rep runs to reach the next row start.
1366                        need -= avail;
1367                        runs.advance(&mut run);
1368                        while run.run < runs.num_runs() && runs.value(run.run) != max_rep {
1369                            runs.advance(&mut run);
1370                        }
1371                        level = if run.run < runs.num_runs() {
1372                            run.start
1373                        } else {
1374                            self.len()
1375                        };
1376                    }
1377                }
1378                cursor.level = level;
1379                cursor.row = target_row;
1380                cursor.run = run;
1381                Ok(level)
1382            }
1383        }
1384    }
1385
1386    /// Count of levels in `range` that are `<= max`, resuming from `*run_cursor`
1387    /// and leaving it on the last run that overlaps `range`.
1388    ///
1389    /// Successive calls must pass ascending, non-overlapping ranges (`range.start
1390    /// >=` the previous `range.end`) so runs are swept at most once per page.
1391    fn count_le_cursor(
1392        &self,
1393        run_cursor: &mut RunPosition,
1394        range: Range<usize>,
1395        max: u16,
1396    ) -> (u64, RunPosition) {
1397        if range.is_empty() {
1398            return (0, *run_cursor);
1399        }
1400        match self {
1401            Self::Dense(buf) => (
1402                buf[range].iter().filter(|&&d| d <= max).count() as u64,
1403                RunPosition::default(),
1404            ),
1405            Self::Runs(runs) => {
1406                // Advance to the first run overlapping the range.
1407                runs.seek(run_cursor, range.start);
1408                let start = *run_cursor;
1409                let mut count = 0u64;
1410                let mut current = *run_cursor;
1411                while current.run < runs.num_runs() && current.start < range.end {
1412                    if runs.value(current.run) <= max {
1413                        let lo = current.start.max(range.start);
1414                        let hi = current.end.min(range.end);
1415                        count += (hi - lo) as u64;
1416                    }
1417                    if current.end >= range.end {
1418                        break;
1419                    }
1420                    runs.advance(&mut current);
1421                }
1422                // Resume the next (ascending) range from the last overlapping run;
1423                // `current` remains valid because `range` is non-empty.
1424                *run_cursor = current;
1425                (count, start)
1426            }
1427        }
1428    }
1429
1430    fn extend_into(&self, range: Range<usize>, run: RunPosition, out: &mut Vec<u16>) {
1431        if range.is_empty() {
1432            return;
1433        }
1434        match self {
1435            Self::Dense(buf) => out.extend_from_slice(&buf[range]),
1436            Self::Runs(runs) => {
1437                let mut current = run;
1438                runs.seek(&mut current, range.start);
1439                while current.run < runs.num_runs() && current.start < range.end {
1440                    let lo = current.start.max(range.start);
1441                    let hi = current.end.min(range.end);
1442                    if hi > lo {
1443                        out.resize(out.len() + (hi - lo), runs.value(current.run));
1444                    }
1445                    runs.advance(&mut current);
1446                }
1447            }
1448        }
1449    }
1450
1451    #[cfg(test)]
1452    fn deep_size(&self) -> usize {
1453        self.deep_size_of_children(&mut Context::new())
1454    }
1455}
1456
1457impl DeepSizeOf for LazyLevels {
1458    fn deep_size_of_children(&self, ctx: &mut Context) -> usize {
1459        match self {
1460            Self::Dense(buf) => buf.deep_size_of_children(ctx),
1461            Self::Runs(runs) => {
1462                let pointer = Arc::as_ptr(runs) as *const () as usize;
1463                if ctx.mark_seen(pointer) {
1464                    std::mem::size_of_val(runs.as_ref()) + runs.deep_size()
1465                } else {
1466                    0
1467                }
1468            }
1469        }
1470    }
1471}
1472
1473fn validate_complex_all_null_levels(
1474    rep: &Option<LazyLevels>,
1475    def: &Option<LazyLevels>,
1476    max_rep: u16,
1477    max_def: u16,
1478) -> Result<()> {
1479    if let Some(rep) = rep {
1480        rep.validate_max("repetition", max_rep)?;
1481    }
1482    if let Some(def) = def {
1483        def.validate_max("definition", max_def)?;
1484    }
1485    if let (Some(rep), Some(def)) = (rep, def)
1486        && rep.len() != def.len()
1487    {
1488        return Err(Error::invalid_input_source(
1489            format!(
1490                "Mismatched complex all-null level counts: repetition has {}, definition has {}",
1491                rep.len(),
1492                def.len()
1493            )
1494            .into(),
1495        ));
1496    }
1497    Ok(())
1498}
1499
1500fn expected_level_bytes(num_values: u64, level_type: &str) -> Result<usize> {
1501    usize::try_from(num_values)
1502        .ok()
1503        .and_then(|num_values| num_values.checked_mul(std::mem::size_of::<u16>()))
1504        .ok_or_else(|| {
1505            Error::invalid_input_source(
1506                format!("{level_type} level count {num_values} does not fit in memory").into(),
1507            )
1508        })
1509}
1510
1511fn dense_levels_from_block(
1512    decompressed: DataBlock,
1513    num_values: u64,
1514    level_type: &str,
1515) -> Result<LazyLevels> {
1516    let DataBlock::FixedWidth(block) = decompressed else {
1517        return Err(Error::invalid_input_source(
1518            format!("Expected fixed-width data block for {level_type} levels").into(),
1519        ));
1520    };
1521    if block.num_values != num_values {
1522        return Err(Error::invalid_input_source(
1523            format!(
1524                "Unexpected {level_type} level count after decompression: expected {num_values}, got {}",
1525                block.num_values
1526            )
1527            .into(),
1528        ));
1529    }
1530    if block.bits_per_value != 16 {
1531        return Err(Error::invalid_input_source(
1532            format!(
1533                "Unexpected {level_type} level bit width after decompression: expected 16, got {}",
1534                block.bits_per_value
1535            )
1536            .into(),
1537        ));
1538    }
1539    let expected_bytes = expected_level_bytes(num_values, level_type)?;
1540    if block.data.len() != expected_bytes {
1541        return Err(Error::invalid_input_source(
1542            format!(
1543                "Unexpected decompressed {level_type} level size: expected {expected_bytes} bytes for {num_values} values, got {}",
1544                block.data.len()
1545            )
1546            .into(),
1547        ));
1548    }
1549    Ok(LazyLevels::Dense(block.data.borrow_to_typed_slice::<u16>()))
1550}
1551
1552#[derive(Debug)]
1553struct CachedComplexAllNullState {
1554    rep: Option<LazyLevels>,
1555    def: Option<LazyLevels>,
1556}
1557
1558impl DeepSizeOf for CachedComplexAllNullState {
1559    fn deep_size_of_children(&self, ctx: &mut Context) -> usize {
1560        self.rep.deep_size_of_children(ctx) + self.def.deep_size_of_children(ctx)
1561    }
1562}
1563
1564impl CachedPageData for CachedComplexAllNullState {
1565    fn as_arc_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync + 'static> {
1566        self
1567    }
1568}
1569
1570/// A scheduler for all-null data that has repetition and definition levels
1571///
1572/// We still need to do some I/O in this case because we need to figure out what kind of null we
1573/// are dealing with (null list, null struct, what level null struct, etc.)
1574///
1575/// TODO: Right now we just load the entire rep/def at initialization time and cache it.  This is a touch
1576/// RAM aggressive and maybe we want something more lazy in the future.  On the other hand, it's simple
1577/// and fast so...maybe not :)
1578#[derive(Debug)]
1579pub struct ComplexAllNullScheduler {
1580    // Set from protobuf
1581    buffer_offsets_and_sizes: Arc<[(u64, u64)]>,
1582    def_meaning: Arc<[DefinitionInterpretation]>,
1583    repdef: Option<Arc<CachedComplexAllNullState>>,
1584    max_rep: u16,
1585    max_def: u16,
1586    max_visible_level: u16,
1587    rep_codec: LevelCodec,
1588    def_codec: LevelCodec,
1589    num_rep_values: u64,
1590    num_def_values: u64,
1591}
1592
1593impl ComplexAllNullScheduler {
1594    pub(crate) fn new(
1595        buffer_offsets_and_sizes: Arc<[(u64, u64)]>,
1596        def_meaning: Arc<[DefinitionInterpretation]>,
1597        rep_codec: LevelCodec,
1598        def_codec: LevelCodec,
1599        num_rep_values: u64,
1600        num_def_values: u64,
1601    ) -> Self {
1602        let max_rep = def_meaning.iter().filter(|l| l.is_list()).count() as u16;
1603        let max_def = def_meaning
1604            .iter()
1605            .map(|meaning| meaning.num_def_levels())
1606            .sum::<u16>();
1607        let max_visible_level = def_meaning
1608            .iter()
1609            .take_while(|l| !l.is_list())
1610            .map(|l| l.num_def_levels())
1611            .sum::<u16>();
1612        Self {
1613            buffer_offsets_and_sizes,
1614            def_meaning,
1615            repdef: None,
1616            max_rep,
1617            max_def,
1618            max_visible_level,
1619            rep_codec,
1620            def_codec,
1621            num_rep_values,
1622            num_def_values,
1623        }
1624    }
1625}
1626
1627impl StructuralPageScheduler for ComplexAllNullScheduler {
1628    fn initialize<'a>(
1629        &'a mut self,
1630        io: &Arc<dyn EncodingsIo>,
1631    ) -> BoxFuture<'a, Result<Arc<dyn CachedPageData>>> {
1632        // Fully load the rep & def buffers, as needed
1633        let (rep_pos, rep_size) = self.buffer_offsets_and_sizes[0];
1634        let (def_pos, def_size) = self.buffer_offsets_and_sizes[1];
1635        let has_rep = rep_size > 0;
1636        let has_def = def_size > 0;
1637
1638        let mut reads = Vec::with_capacity(2);
1639        if has_rep {
1640            reads.push(rep_pos..rep_pos + rep_size);
1641        }
1642        if has_def {
1643            reads.push(def_pos..def_pos + def_size);
1644        }
1645
1646        let data = io.submit_request(reads, 0);
1647        let rep_codec = self.rep_codec.clone();
1648        let def_codec = self.def_codec.clone();
1649        let num_rep_values = self.num_rep_values;
1650        let num_def_values = self.num_def_values;
1651        let max_rep = self.max_rep;
1652        let max_def = self.max_def;
1653
1654        async move {
1655            let data = data.await?;
1656            let mut data_iter = data.into_iter();
1657
1658            // RLE levels select the smallest validated cache representation;
1659            // everything else expands eagerly to `LazyLevels::Dense`.
1660            let build_levels = |compressed_bytes: Bytes,
1661                                codec: &LevelCodec,
1662                                num_values: u64,
1663                                level_type: &str|
1664             -> Result<LazyLevels> {
1665                match codec {
1666                    LevelCodec::Uncompressed => {
1667                        if num_values == 0 {
1668                            if !compressed_bytes
1669                                .len()
1670                                .is_multiple_of(std::mem::size_of::<u16>())
1671                            {
1672                                return Err(Error::invalid_input_source(
1673                                    format!(
1674                                        "Unexpected uncompressed {level_type} level size: {} bytes is not divisible by {}",
1675                                        compressed_bytes.len(),
1676                                        std::mem::size_of::<u16>()
1677                                    )
1678                                    .into(),
1679                                ));
1680                            }
1681                        } else {
1682                            let expected_bytes = expected_level_bytes(num_values, level_type)?;
1683                            if compressed_bytes.len() != expected_bytes {
1684                                return Err(Error::invalid_input_source(
1685                                    format!(
1686                                        "Unexpected uncompressed {level_type} level size: expected {expected_bytes} bytes for {num_values} values, got {}",
1687                                        compressed_bytes.len()
1688                                    )
1689                                    .into(),
1690                                ));
1691                            }
1692                        }
1693                        let buffer = LanceBuffer::from_bytes(compressed_bytes, 2);
1694                        Ok(LazyLevels::Dense(buffer.borrow_to_typed_slice::<u16>()))
1695                    }
1696                    LevelCodec::Rle(decompressor) => {
1697                        let frame = LanceBuffer::from_bytes(compressed_bytes, 1);
1698                        let runs = decompressor.decode_u16_runs(frame, num_values)?;
1699                        LazyLevels::from_rle_runs(runs)
1700                    }
1701                    LevelCodec::Block(decompressor) => {
1702                        let frame = LanceBuffer::from_bytes(compressed_bytes, 1);
1703                        let decompressed = decompressor.decompress(Some(frame), num_values)?;
1704                        dense_levels_from_block(decompressed, num_values, level_type)
1705                    }
1706                }
1707            };
1708
1709            let rep = if has_rep {
1710                let rep = data_iter.next().unwrap();
1711                Some(build_levels(rep, &rep_codec, num_rep_values, "repetition")?)
1712            } else {
1713                None
1714            };
1715
1716            let def = if has_def {
1717                let def = data_iter.next().unwrap();
1718                Some(build_levels(def, &def_codec, num_def_values, "definition")?)
1719            } else {
1720                None
1721            };
1722
1723            validate_complex_all_null_levels(&rep, &def, max_rep, max_def)?;
1724            let repdef = Arc::new(CachedComplexAllNullState { rep, def });
1725
1726            self.repdef = Some(repdef.clone());
1727
1728            Ok(repdef as Arc<dyn CachedPageData>)
1729        }
1730        .boxed()
1731    }
1732
1733    fn load(&mut self, data: &Arc<dyn CachedPageData>) {
1734        self.repdef = Some(
1735            data.clone()
1736                .as_arc_any()
1737                .downcast::<CachedComplexAllNullState>()
1738                .unwrap(),
1739        );
1740    }
1741
1742    fn schedule_ranges(
1743        &self,
1744        ranges: &[Range<u64>],
1745        _io: &Arc<dyn EncodingsIo>,
1746    ) -> Result<Vec<PageLoadTask>> {
1747        let ranges = VecDeque::from_iter(ranges.iter().cloned());
1748        let num_rows = ranges.iter().map(|r| r.end - r.start).sum::<u64>();
1749        let decoder = Box::new(ComplexAllNullPageDecoder {
1750            ranges,
1751            rep: self.repdef.as_ref().unwrap().rep.clone(),
1752            def: self.repdef.as_ref().unwrap().def.clone(),
1753            num_rows,
1754            def_meaning: self.def_meaning.clone(),
1755            max_rep: self.max_rep,
1756            max_visible_level: self.max_visible_level,
1757            rep_cursor: LevelCursor::default(),
1758            def_run_cursor: RunPosition::default(),
1759        }) as Box<dyn StructuralPageDecoder>;
1760        let page_load_task = PageLoadTask {
1761            decoder_fut: std::future::ready(Ok(decoder)).boxed(),
1762            num_rows,
1763        };
1764        Ok(vec![page_load_task])
1765    }
1766}
1767
1768#[derive(Debug)]
1769pub struct ComplexAllNullPageDecoder {
1770    ranges: VecDeque<Range<u64>>,
1771    rep: Option<LazyLevels>,
1772    def: Option<LazyLevels>,
1773    num_rows: u64,
1774    def_meaning: Arc<[DefinitionInterpretation]>,
1775    max_rep: u16,
1776    max_visible_level: u16,
1777    /// Monotonic cursor into `rep` tracking the current row's level start.
1778    rep_cursor: LevelCursor,
1779    /// Monotonic run cursor into `def` for `count_le_cursor`.
1780    def_run_cursor: RunPosition,
1781}
1782
1783impl ComplexAllNullPageDecoder {
1784    fn drain_ranges(&mut self, num_rows: u64) -> Vec<Range<u64>> {
1785        let mut rows_desired = num_rows;
1786        let mut ranges = Vec::with_capacity(self.ranges.len());
1787        while rows_desired > 0 {
1788            let front = self.ranges.front_mut().unwrap();
1789            let avail = front.end - front.start;
1790            if avail > rows_desired {
1791                ranges.push(front.start..front.start + rows_desired);
1792                front.start += rows_desired;
1793                rows_desired = 0;
1794            } else {
1795                ranges.push(self.ranges.pop_front().unwrap());
1796                rows_desired -= avail;
1797            }
1798        }
1799        ranges
1800    }
1801
1802    /// Level index at which row `target_row` starts, advancing the monotonic
1803    /// repetition cursor. Callers must request non-decreasing `target_row`.
1804    fn seek_row_start(&mut self, target_row: u64) -> Result<usize> {
1805        match &self.rep {
1806            Some(rep) => rep.seek_row_start(&mut self.rep_cursor, target_row, self.max_rep),
1807            None => {
1808                // Without repetition every level is its own row.
1809                self.rep_cursor.row = target_row;
1810                self.rep_cursor.level = target_row as usize;
1811                Ok(target_row as usize)
1812            }
1813        }
1814    }
1815
1816    /// Number of visible items in the level range `levels` (definition levels
1817    /// `<= max_visible_level`), advancing the monotonic definition cursor.
1818    fn count_visible(&mut self, levels: Range<usize>) -> Result<(u64, RunPosition)> {
1819        match &self.def {
1820            Some(def) => {
1821                if levels.end > def.len() {
1822                    return Err(Error::internal(
1823                        "Invalid complex all-null layout: definition buffer too short",
1824                    ));
1825                }
1826                Ok(def.count_le_cursor(&mut self.def_run_cursor, levels, self.max_visible_level))
1827            }
1828            None => Ok(((levels.end - levels.start) as u64, RunPosition::default())),
1829        }
1830    }
1831}
1832
1833impl StructuralPageDecoder for ComplexAllNullPageDecoder {
1834    fn drain(&mut self, num_rows: u64) -> Result<Box<dyn DecodePageTask>> {
1835        let drained_ranges = self.drain_ranges(num_rows);
1836        let mut level_slices: Vec<LevelSlice> = Vec::with_capacity(drained_ranges.len());
1837        let mut visible_items_total = 0;
1838
1839        // Each row range is one contiguous level slice `[start_row_level,
1840        // end_row_level)`, so we seek both boundaries and count its visibility at
1841        // once rather than per row. The cursors only move forward, so locating and
1842        // counting all requested ranges visits each intervening run at most once.
1843        for range in drained_ranges {
1844            let level_start = self.seek_row_start(range.start)?;
1845            let rep_run = self.rep_cursor.run;
1846            let level_end = self.seek_row_start(range.end)?;
1847            let (visible_items, def_run) = self.count_visible(level_start..level_end)?;
1848            visible_items_total += visible_items;
1849            if let Some(last) = level_slices.last_mut()
1850                && last.range.end == level_start
1851            {
1852                last.range.end = level_end;
1853            } else {
1854                level_slices.push(LevelSlice {
1855                    range: level_start..level_end,
1856                    rep_run,
1857                    def_run,
1858                });
1859            }
1860        }
1861
1862        Ok(Box::new(DecodeComplexAllNullTask {
1863            level_slices,
1864            visible_items_total,
1865            rep: self.rep.clone(),
1866            def: self.def.clone(),
1867            def_meaning: self.def_meaning.clone(),
1868            max_visible_level: self.max_visible_level,
1869        }))
1870    }
1871
1872    fn num_rows(&self) -> u64 {
1873        self.num_rows
1874    }
1875}
1876
1877/// We use `level_slices` to slice into `rep` and `def` and create rep/def buffers
1878/// for the null data.
1879#[derive(Debug, Clone)]
1880struct LevelSlice {
1881    range: Range<usize>,
1882    rep_run: RunPosition,
1883    def_run: RunPosition,
1884}
1885
1886#[derive(Clone, Copy)]
1887enum LevelKind {
1888    Repetition,
1889    Definition,
1890}
1891
1892impl LevelSlice {
1893    fn run(&self, kind: LevelKind) -> RunPosition {
1894        match kind {
1895            LevelKind::Repetition => self.rep_run,
1896            LevelKind::Definition => self.def_run,
1897        }
1898    }
1899}
1900
1901#[derive(Debug)]
1902pub struct DecodeComplexAllNullTask {
1903    level_slices: Vec<LevelSlice>,
1904    visible_items_total: u64,
1905    rep: Option<LazyLevels>,
1906    def: Option<LazyLevels>,
1907    def_meaning: Arc<[DefinitionInterpretation]>,
1908    max_visible_level: u16,
1909}
1910
1911impl DecodeComplexAllNullTask {
1912    fn decode_level(&self, levels: &Option<LazyLevels>, kind: LevelKind) -> Option<Vec<u16>> {
1913        levels.as_ref().map(|levels| {
1914            let num_levels = self
1915                .level_slices
1916                .iter()
1917                .map(|slice| slice.range.end - slice.range.start)
1918                .sum();
1919            let mut referenced_levels = Vec::with_capacity(num_levels);
1920            for slice in &self.level_slices {
1921                levels.extend_into(slice.range.clone(), slice.run(kind), &mut referenced_levels);
1922            }
1923            referenced_levels
1924        })
1925    }
1926}
1927
1928impl DecodePageTask for DecodeComplexAllNullTask {
1929    fn decode(self: Box<Self>) -> Result<DecodedPage> {
1930        let rep = self.decode_level(&self.rep, LevelKind::Repetition);
1931        let def = self.decode_level(&self.def, LevelKind::Definition);
1932
1933        // If there are definition levels there may be empty / null lists which are not visible
1934        // in the items array.  We need to account for that here to figure out how many values
1935        // should be in the items array.
1936        let num_values = if let Some(def) = &def {
1937            def.iter().filter(|&d| *d <= self.max_visible_level).count() as u64
1938        } else {
1939            self.visible_items_total
1940        };
1941
1942        let data = DataBlock::AllNull(AllNullDataBlock { num_values });
1943        let unraveler = RepDefUnraveler::new(rep, def, self.def_meaning, num_values);
1944        Ok(DecodedPage {
1945            data,
1946            repdef: unraveler,
1947        })
1948    }
1949}
1950
1951/// A scheduler for simple all-null data
1952///
1953/// "simple" all-null data is data that is all null and only has a single level of definition and
1954/// no repetition.  We don't need to read any data at all in this case.
1955#[derive(Debug, Default)]
1956pub struct SimpleAllNullScheduler {}
1957
1958impl StructuralPageScheduler for SimpleAllNullScheduler {
1959    fn initialize<'a>(
1960        &'a mut self,
1961        _io: &Arc<dyn EncodingsIo>,
1962    ) -> BoxFuture<'a, Result<Arc<dyn CachedPageData>>> {
1963        std::future::ready(Ok(Arc::new(NoCachedPageData) as Arc<dyn CachedPageData>)).boxed()
1964    }
1965
1966    fn load(&mut self, _cache: &Arc<dyn CachedPageData>) {}
1967
1968    fn schedule_ranges(
1969        &self,
1970        ranges: &[Range<u64>],
1971        _io: &Arc<dyn EncodingsIo>,
1972    ) -> Result<Vec<PageLoadTask>> {
1973        let num_rows = ranges.iter().map(|r| r.end - r.start).sum::<u64>();
1974        let decoder =
1975            Box::new(SimpleAllNullPageDecoder { num_rows }) as Box<dyn StructuralPageDecoder>;
1976        let page_load_task = PageLoadTask {
1977            decoder_fut: std::future::ready(Ok(decoder)).boxed(),
1978            num_rows,
1979        };
1980        Ok(vec![page_load_task])
1981    }
1982}
1983
1984/// A page decode task for all-null data without any
1985/// repetition and only a single level of definition
1986#[derive(Debug)]
1987struct SimpleAllNullDecodePageTask {
1988    num_values: u64,
1989}
1990impl DecodePageTask for SimpleAllNullDecodePageTask {
1991    fn decode(self: Box<Self>) -> Result<DecodedPage> {
1992        let unraveler = RepDefUnraveler::new(
1993            None,
1994            Some(vec![1; self.num_values as usize]),
1995            Arc::new([DefinitionInterpretation::NullableItem]),
1996            self.num_values,
1997        );
1998        Ok(DecodedPage {
1999            data: DataBlock::AllNull(AllNullDataBlock {
2000                num_values: self.num_values,
2001            }),
2002            repdef: unraveler,
2003        })
2004    }
2005}
2006
2007#[derive(Debug)]
2008pub struct SimpleAllNullPageDecoder {
2009    num_rows: u64,
2010}
2011
2012impl StructuralPageDecoder for SimpleAllNullPageDecoder {
2013    fn drain(&mut self, num_rows: u64) -> Result<Box<dyn DecodePageTask>> {
2014        Ok(Box::new(SimpleAllNullDecodePageTask {
2015            num_values: num_rows,
2016        }))
2017    }
2018
2019    fn num_rows(&self) -> u64 {
2020        self.num_rows
2021    }
2022}
2023
2024#[derive(Debug, Clone)]
2025struct MiniBlockSchedulerDictionary {
2026    // These come from the protobuf
2027    dictionary_decompressor: Arc<dyn BlockDecompressor>,
2028    dictionary_buf_position_and_size: (u64, u64),
2029    dictionary_data_alignment: u64,
2030    num_dictionary_items: u64,
2031}
2032
2033/// State that is loaded once and cached for future lookups
2034#[derive(Debug)]
2035struct MiniBlockCacheableState {
2036    /// Compact per-chunk index (byte ranges + row/item mapping) for the page
2037    chunk_index: MiniBlockChunkIndex,
2038    /// The dictionary for the page, if any
2039    dictionary: Option<Arc<DataBlock>>,
2040}
2041
2042impl DeepSizeOf for MiniBlockCacheableState {
2043    fn deep_size_of_children(&self, context: &mut Context) -> usize {
2044        self.chunk_index.deep_size_of_children(context)
2045            + self
2046                .dictionary
2047                .as_ref()
2048                .map(|dict| dict.data_size() as usize)
2049                .unwrap_or(0)
2050    }
2051}
2052
2053impl CachedPageData for MiniBlockCacheableState {
2054    fn as_arc_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync + 'static> {
2055        self
2056    }
2057}
2058
2059/// A scheduler for a page that has been encoded with the mini-block layout
2060///
2061/// Scheduling mini-block encoded data is simple in concept and somewhat complex
2062/// in practice.
2063///
2064/// First, during initialization, we load the chunk metadata, the repetition index,
2065/// and the dictionary (these last two may not be present)
2066///
2067/// Then, during scheduling, we use the user's requested row ranges and the repetition
2068/// index to determine which chunks we need and which rows we need from those chunks.
2069///
2070/// For example, if the repetition index is: [50, 3], [50, 0], [10, 0] and the range
2071/// from the user is 40..60 then we need to:
2072///
2073///  - Read the first chunk and skip the first 40 rows, then read 10 full rows, and
2074///    then read 3 items for the 11th row of our range.
2075///  - Read the second chunk and read the remaining items in our 11th row and then read
2076///    the remaining 9 full rows.
2077///
2078/// Then, if we are going to decode that in batches of 5, we need to make decode tasks.
2079/// The first two decode tasks will just need the first chunk.  The third decode task will
2080/// need the first chunk (for the trailer which has the 11th row in our range) and the second
2081/// chunk.  The final decode task will just need the second chunk.
2082///
2083/// The above prose descriptions are what are represented by `ChunkInstructions` and
2084/// `ChunkDrainInstructions`.
2085#[derive(Debug)]
2086pub struct MiniBlockScheduler {
2087    // These come from the protobuf
2088    buffer_offsets_and_sizes: Vec<(u64, u64)>,
2089    priority: u64,
2090    items_in_page: u64,
2091    repetition_index_depth: u16,
2092    num_buffers: u64,
2093    rep_decompressor: Option<Arc<dyn BlockDecompressor>>,
2094    def_decompressor: Option<Arc<dyn BlockDecompressor>>,
2095    value_decompressor: Arc<dyn MiniBlockDecompressor>,
2096    def_meaning: Arc<[DefinitionInterpretation]>,
2097    dictionary: Option<MiniBlockSchedulerDictionary>,
2098    // This is set after initialization
2099    page_meta: Option<Arc<MiniBlockCacheableState>>,
2100    has_large_chunk: bool,
2101}
2102
2103impl MiniBlockScheduler {
2104    fn try_new(
2105        buffer_offsets_and_sizes: &[(u64, u64)],
2106        priority: u64,
2107        items_in_page: u64,
2108        layout: &pb21::MiniBlockLayout,
2109        decompressors: &dyn DecompressionStrategy,
2110    ) -> Result<Self> {
2111        let rep_decompressor = layout
2112            .rep_compression
2113            .as_ref()
2114            .map(|rep_compression| {
2115                decompressors
2116                    .create_block_decompressor(rep_compression)
2117                    .map(Arc::from)
2118            })
2119            .transpose()?;
2120        let def_decompressor = layout
2121            .def_compression
2122            .as_ref()
2123            .map(|def_compression| {
2124                decompressors
2125                    .create_block_decompressor(def_compression)
2126                    .map(Arc::from)
2127            })
2128            .transpose()?;
2129        let def_meaning = layout
2130            .layers
2131            .iter()
2132            .map(|l| ProtobufUtils21::repdef_layer_to_def_interp(*l))
2133            .collect::<Vec<_>>();
2134        let value_decompressor = decompressors.create_miniblock_decompressor(
2135            layout.value_compression.as_ref().unwrap(),
2136            decompressors,
2137        )?;
2138
2139        let dictionary = if let Some(dictionary_encoding) = layout.dictionary.as_ref() {
2140            let num_dictionary_items = layout.num_dictionary_items;
2141            let dictionary_decompressor = decompressors
2142                .create_block_decompressor(dictionary_encoding)?
2143                .into();
2144            let dictionary_data_alignment = match dictionary_encoding.compression.as_ref().unwrap()
2145            {
2146                Compression::Variable(_) => 4,
2147                Compression::Flat(_) => 16,
2148                Compression::General(_) => 1,
2149                Compression::InlineBitpacking(_) | Compression::OutOfLineBitpacking(_) => {
2150                    crate::encoder::MIN_PAGE_BUFFER_ALIGNMENT
2151                }
2152                _ => {
2153                    return Err(Error::invalid_input_source(
2154                        format!(
2155                            "Unsupported mini-block dictionary encoding: {:?}",
2156                            dictionary_encoding.compression.as_ref().unwrap()
2157                        )
2158                        .into(),
2159                    ));
2160                }
2161            };
2162            Some(MiniBlockSchedulerDictionary {
2163                dictionary_decompressor,
2164                dictionary_buf_position_and_size: buffer_offsets_and_sizes[2],
2165                dictionary_data_alignment,
2166                num_dictionary_items,
2167            })
2168        } else {
2169            None
2170        };
2171
2172        Ok(Self {
2173            buffer_offsets_and_sizes: buffer_offsets_and_sizes.to_vec(),
2174            rep_decompressor,
2175            def_decompressor,
2176            value_decompressor: value_decompressor.into(),
2177            repetition_index_depth: layout.repetition_index_depth as u16,
2178            num_buffers: layout.num_buffers,
2179            priority,
2180            items_in_page,
2181            dictionary,
2182            def_meaning: def_meaning.into(),
2183            page_meta: None,
2184            has_large_chunk: layout.has_large_chunk,
2185        })
2186    }
2187
2188    fn lookup_chunks(&self, chunk_indices: &[usize]) -> Vec<LoadedChunk> {
2189        let chunk_index = &self.page_meta.as_ref().unwrap().chunk_index;
2190        chunk_indices
2191            .iter()
2192            .map(|&chunk_idx| LoadedChunk {
2193                byte_range: chunk_index.byte_range(chunk_idx),
2194                items_in_chunk: chunk_index.items_in_chunk(chunk_idx),
2195                chunk_idx,
2196                data: LanceBuffer::empty(),
2197            })
2198            .collect()
2199    }
2200}
2201
2202#[derive(Debug, PartialEq, Eq, Clone, Copy)]
2203enum PreambleAction {
2204    Take,
2205    Skip,
2206    Absent,
2207}
2208
2209// When we schedule a chunk we use the repetition index (or, if none exists, just the # of items
2210// in each chunk) to map a user requested range into a set of ChunkInstruction objects which tell
2211// us how exactly to read from the chunk.
2212//
2213// Examples:
2214//
2215// | Chunk 0     | Chunk 1   | Chunk 2   | Chunk 3 |
2216// | xxxxyyyyzzz | zzzzzzzzz | zzzzzzzzz | aaabbcc |
2217//
2218// Full read (0..6)
2219//
2220// Chunk 0: (several rows, ends with trailer)
2221//   preamble: absent
2222//   rows_to_skip: 0
2223//   rows_to_take: 3 (x, y, z)
2224//   take_trailer: true
2225//
2226// Chunk 1: (all preamble, ends with trailer)
2227//   preamble: take
2228//   rows_to_skip: 0
2229//   rows_to_take: 0
2230//   take_trailer: true
2231//
2232// Chunk 2: (all preamble, no trailer)
2233//   preamble: take
2234//   rows_to_skip: 0
2235//   rows_to_take: 0
2236//   take_trailer: false
2237//
2238// Chunk 3: (several rows, no trailer or preamble)
2239//   preamble: absent
2240//   rows_to_skip: 0
2241//   rows_to_take: 3 (a, b, c)
2242//   take_trailer: false
2243#[derive(Clone, Debug, PartialEq, Eq)]
2244struct ChunkInstructions {
2245    // The index of the chunk to read
2246    chunk_idx: usize,
2247    // A "preamble" is when a chunk begins with a continuation of the previous chunk's list.  If there
2248    // is no repetition index there is never a preamble.
2249    //
2250    // It's possible for a chunk to be entirely premable.  For example, if there is a really large list
2251    // that spans several chunks.
2252    preamble: PreambleAction,
2253    // How many complete rows (not including the preamble or trailer) to skip
2254    //
2255    // If this is non-zero then premable must not be Take
2256    rows_to_skip: u64,
2257    // How many rows to take.  If a row splits across chunks then we will count the row in the first
2258    // chunk that contains the row.
2259    rows_to_take: u64,
2260    // A "trailer" is when a chunk ends with a partial list.  If there is no repetition index there is
2261    // never a trailer.
2262    //
2263    // A chunk that is all preamble may or may not have a trailer.
2264    //
2265    // If this is true then we want to include the trailer
2266    take_trailer: bool,
2267}
2268
2269// First, we schedule a bunch of [`ChunkInstructions`] based on the users ranges.  Then we
2270// start decoding them, based on a batch size, which might not align with what we scheduled.
2271//
2272// This results in `ChunkDrainInstructions` which targets a contiguous slice of a `ChunkInstructions`
2273//
2274// So if `ChunkInstructions` is "skip preamble, skip 10, take 50, take trailer" and we are decoding in
2275// batches of size 10 we might have a `ChunkDrainInstructions` that targets that chunk and has its own
2276// skip of 17 and take of 10.  This would mean we decode the chunk, skip the preamble and 27 rows, and
2277// then take 10 rows.
2278//
2279// One very confusing bit is that `rows_to_take` includes the trailer.  So if we have two chunks:
2280//  -no preamble, skip 5, take 10, take trailer
2281//  -take preamble, skip 0, take 50, no trailer
2282//
2283// and we are draining 20 rows then the drain instructions for the first batch will be:
2284//  - no preamble, skip 0 (from chunk 0), take 11 (from chunk 0)
2285//  - take preamble (from chunk 1), skip 0 (from chunk 1), take 9 (from chunk 1)
2286#[derive(Debug, PartialEq, Eq)]
2287struct ChunkDrainInstructions {
2288    chunk_instructions: ChunkInstructions,
2289    rows_to_skip: u64,
2290    rows_to_take: u64,
2291    preamble_action: PreambleAction,
2292}
2293
2294impl ChunkInstructions {
2295    // Given a repetition index and a set of user ranges we need to figure out how to read from the chunks
2296    //
2297    // We assume that `user_ranges` are in sorted order and non-overlapping
2298    //
2299    // The output will be a set of `ChunkInstructions` which tell us how to read from the chunks
2300    fn schedule_instructions(
2301        chunk_index: &MiniBlockChunkIndex,
2302        user_ranges: &[Range<u64>],
2303    ) -> Vec<Self> {
2304        // Bind the per-page chunk count once; re-deriving it each iteration
2305        // costs a width match plus a length read.
2306        let num_chunks = chunk_index.num_chunks();
2307        // This is an in-exact capacity guess but pretty good.  The actual capacity can be
2308        // smaller if instructions are merged.  It can be larger if there are multiple instructions
2309        // per row which can happen with lists.
2310        let mut chunk_instructions = Vec::with_capacity(user_ranges.len());
2311
2312        for user_range in user_ranges {
2313            let mut rows_needed = user_range.end - user_range.start;
2314            let mut need_preamble = false;
2315
2316            // Need to find the first chunk with a first row >= user_range.start.  If there are
2317            // multiple chunks with the same first row we need to take the first one.
2318            let mut block_index = chunk_index.find_chunk(user_range.start);
2319
2320            let mut to_skip = user_range.start - chunk_index.first_row(block_index);
2321
2322            while rows_needed > 0 || need_preamble {
2323                // Check if we've gone past the last block (should not happen)
2324                if block_index >= num_chunks {
2325                    log::warn!(
2326                        "schedule_instructions inconsistency: block_index >= num_chunks, exiting early"
2327                    );
2328                    break;
2329                }
2330
2331                let starts_including_trailer = chunk_index.rows_in_chunk(block_index);
2332                let has_preamble = chunk_index.has_preamble(block_index);
2333                let has_trailer = chunk_index.has_trailer(block_index);
2334                let rows_avail = starts_including_trailer.saturating_sub(to_skip);
2335
2336                // Handle blocks that are entirely preamble (rows_avail = 0)
2337                // These blocks have no rows to take but may have a preamble we need
2338                // We only look for preamble if to_skip == 0 (we're not skipping rows)
2339                if rows_avail == 0 && to_skip == 0 {
2340                    // Only process if this chunk has a preamble we need
2341                    if has_preamble && need_preamble {
2342                        chunk_instructions.push(Self {
2343                            chunk_idx: block_index,
2344                            preamble: PreambleAction::Take,
2345                            rows_to_skip: 0,
2346                            rows_to_take: 0,
2347                            // We still need to look at has_trailer to distinguish between "all preamble
2348                            // and row ends at end of chunk" and "all preamble and row bleeds into next
2349                            // chunk".  Both cases will have 0 rows available.
2350                            take_trailer: has_trailer,
2351                        });
2352                        // Only set need_preamble = false if the chunk has at least one row,
2353                        // Or we are reaching the last block,
2354                        // Otherwise, the chunk is entirely preamble and we need the next chunk's preamble too
2355                        if starts_including_trailer > 0 || block_index == num_chunks - 1 {
2356                            need_preamble = false;
2357                        }
2358                    }
2359                    // Move to next block
2360                    block_index += 1;
2361                    continue;
2362                }
2363
2364                // Edge case: if rows_avail == 0 but to_skip > 0
2365                // This theoretically shouldn't happen (binary search should avoid it)
2366                // but handle it for safety
2367                if rows_avail == 0 && to_skip > 0 {
2368                    // This block doesn't have enough rows to skip, move to next block
2369                    // Adjust to_skip by the number of rows in this block
2370                    to_skip -= starts_including_trailer;
2371                    block_index += 1;
2372                    continue;
2373                }
2374
2375                let rows_to_take = rows_avail.min(rows_needed);
2376                rows_needed -= rows_to_take;
2377
2378                let mut take_trailer = false;
2379                let preamble = if has_preamble {
2380                    if need_preamble {
2381                        PreambleAction::Take
2382                    } else {
2383                        PreambleAction::Skip
2384                    }
2385                } else {
2386                    PreambleAction::Absent
2387                };
2388
2389                // Are we taking the trailer?  If so, make sure we mark that we need the preamble
2390                if rows_to_take == rows_avail && has_trailer {
2391                    take_trailer = true;
2392                    need_preamble = true;
2393                } else {
2394                    need_preamble = false;
2395                };
2396
2397                chunk_instructions.push(Self {
2398                    preamble,
2399                    chunk_idx: block_index,
2400                    rows_to_skip: to_skip,
2401                    rows_to_take,
2402                    take_trailer,
2403                });
2404
2405                to_skip = 0;
2406                block_index += 1;
2407            }
2408        }
2409
2410        // If there were multiple ranges we may have multiple instructions for a single chunk.  Merge them now if they
2411        // are _adjacent_ (i.e. don't merge "take first row of chunk 0" and "take third row of chunk 0" into "take 2
2412        // rows of chunk 0 starting at 0")
2413        if user_ranges.len() > 1 {
2414            // Merge adjacent instructions in place.  `write` indexes the last
2415            // retained instruction; each following instruction is either folded
2416            // into it (contiguous within the same chunk) or compacted forward.
2417            let mut write = 0;
2418            for read in 1..chunk_instructions.len() {
2419                let merges = {
2420                    let last = &chunk_instructions[write];
2421                    let candidate = &chunk_instructions[read];
2422                    last.chunk_idx == candidate.chunk_idx
2423                        && last.rows_to_take + last.rows_to_skip == candidate.rows_to_skip
2424                };
2425                if merges {
2426                    let rows_to_take = chunk_instructions[read].rows_to_take;
2427                    let take_trailer = chunk_instructions[read].take_trailer;
2428                    let last = &mut chunk_instructions[write];
2429                    last.rows_to_take += rows_to_take;
2430                    last.take_trailer |= take_trailer;
2431                } else {
2432                    write += 1;
2433                    if write != read {
2434                        chunk_instructions.swap(write, read);
2435                    }
2436                }
2437            }
2438            chunk_instructions.truncate(write + 1);
2439        }
2440        chunk_instructions
2441    }
2442
2443    fn drain_from_instruction(
2444        &self,
2445        rows_desired: &mut u64,
2446        need_preamble: &mut bool,
2447        skip_in_chunk: &mut u64,
2448    ) -> (ChunkDrainInstructions, bool) {
2449        // If we need the premable then we shouldn't be skipping anything
2450        debug_assert!(!*need_preamble || *skip_in_chunk == 0);
2451        let rows_avail = self.rows_to_take - *skip_in_chunk;
2452        let has_preamble = self.preamble != PreambleAction::Absent;
2453        let preamble_action = match (*need_preamble, has_preamble) {
2454            (true, true) => PreambleAction::Take,
2455            (true, false) => panic!("Need preamble but there isn't one"),
2456            (false, true) => PreambleAction::Skip,
2457            (false, false) => PreambleAction::Absent,
2458        };
2459
2460        // How many rows are we actually taking in this take step (including the preamble
2461        // and trailer both as individual rows)
2462        let rows_taking = if *rows_desired >= rows_avail {
2463            // We want all the rows.  If there is a trailer we are grabbing it and will need
2464            // the preamble of the next chunk
2465            // If there is a trailer and we are taking all the rows then we need the preamble
2466            // of the next chunk.
2467            //
2468            // Also, if this chunk is entirely preamble (rows_avail == 0 && !take_trailer) then we
2469            // need the preamble of the next chunk.
2470            *need_preamble = self.take_trailer;
2471            rows_avail
2472        } else {
2473            // We aren't taking all the rows.  Even if there is a trailer we aren't taking
2474            // it so we will not need the preamble
2475            *need_preamble = false;
2476            *rows_desired
2477        };
2478        let rows_skipped = *skip_in_chunk;
2479
2480        // Update the state for the next iteration
2481        let consumed_chunk = if *rows_desired >= rows_avail {
2482            *rows_desired -= rows_avail;
2483            *skip_in_chunk = 0;
2484            true
2485        } else {
2486            *skip_in_chunk += *rows_desired;
2487            *rows_desired = 0;
2488            false
2489        };
2490
2491        (
2492            ChunkDrainInstructions {
2493                chunk_instructions: self.clone(),
2494                rows_to_skip: rows_skipped,
2495                rows_to_take: rows_taking,
2496                preamble_action,
2497            },
2498            consumed_chunk,
2499        )
2500    }
2501}
2502
2503enum Words {
2504    U16(ScalarBuffer<u16>),
2505    U32(ScalarBuffer<u32>),
2506}
2507
2508struct WordsIter<'a> {
2509    iter: Box<dyn Iterator<Item = u32> + 'a>,
2510}
2511
2512impl Words {
2513    pub fn len(&self) -> usize {
2514        match self {
2515            Self::U16(b) => b.len(),
2516            Self::U32(b) => b.len(),
2517        }
2518    }
2519
2520    pub fn iter(&self) -> WordsIter<'_> {
2521        match self {
2522            Self::U16(buf) => WordsIter {
2523                iter: Box::new(buf.iter().map(|&x| x as u32)),
2524            },
2525            Self::U32(buf) => WordsIter {
2526                iter: Box::new(buf.iter().copied()),
2527            },
2528        }
2529    }
2530
2531    pub fn from_bytes(bytes: Bytes, has_large_chunk: bool) -> Result<Self> {
2532        let bytes_per_value = if has_large_chunk { 4 } else { 2 };
2533        assert_eq!(bytes.len() % bytes_per_value, 0);
2534        let buffer = LanceBuffer::from_bytes(bytes, bytes_per_value as u64);
2535        if has_large_chunk {
2536            Ok(Self::U32(buffer.borrow_to_typed_slice::<u32>()))
2537        } else {
2538            Ok(Self::U16(buffer.borrow_to_typed_slice::<u16>()))
2539        }
2540    }
2541}
2542
2543impl<'a> Iterator for WordsIter<'a> {
2544    type Item = u32;
2545
2546    fn next(&mut self) -> Option<Self::Item> {
2547        self.iter.next()
2548    }
2549}
2550
2551/// Per-chunk leaf value-count analysis derived from the metadata words.
2552///
2553/// `values_per_chunk` is the count shared by every non-last chunk (meaningful
2554/// when `uniform`), and `last_chunk_values` is the final chunk's count.
2555struct FlatValueCounts {
2556    logs: Vec<u8>,
2557    uniform: bool,
2558    values_per_chunk: u64,
2559    last_chunk_values: u64,
2560}
2561
2562fn analyze_value_counts(words: &Words, items_in_page: u64) -> Result<FlatValueCounts> {
2563    let num_chunks = words.len();
2564    let logs = words.iter().map(|w| (w & 0x0F) as u8).collect::<Vec<_>>();
2565    let mut counted = 0u64;
2566    for (chunk_index, &log) in logs.iter().take(num_chunks.saturating_sub(1)).enumerate() {
2567        if log == 0 {
2568            return Err(Error::corrupt_file_named(
2569                "miniblock_metadata",
2570                format!(
2571                    "non-final chunk {chunk_index} of {num_chunks} has invalid log_num_values=0"
2572                ),
2573            ));
2574        }
2575        counted = counted.checked_add(1u64 << log).ok_or_else(|| {
2576            Error::corrupt_file_named(
2577                "miniblock_metadata",
2578                format!(
2579                    "value count overflow at chunk {chunk_index}: counted_values={counted}, \
2580                     log_num_values={log}, items_in_page={items_in_page}"
2581                ),
2582            )
2583        })?;
2584    }
2585    let last_chunk_values = items_in_page.checked_sub(counted).ok_or_else(|| {
2586        Error::corrupt_file_named(
2587            "miniblock_metadata",
2588            format!(
2589                "non-final chunks account for counted_values={counted}, exceeding \
2590                 items_in_page={items_in_page}"
2591            ),
2592        )
2593    })?;
2594    if let Some(&last_log) = logs.last()
2595        && last_log != 0
2596        && (1u64 << last_log) != last_chunk_values
2597    {
2598        return Err(Error::corrupt_file_named(
2599            "miniblock_metadata",
2600            format!(
2601                "final chunk log_num_values={last_log} does not match \
2602                 last_chunk_values={last_chunk_values}: counted_values={counted}, \
2603                 items_in_page={items_in_page}"
2604            ),
2605        ));
2606    }
2607    let uniform = num_chunks <= 1 || logs[..num_chunks - 1].iter().all(|&log| log == logs[0]);
2608    // A single-chunk page has no "non-last" chunk to derive a stride from; use the
2609    // page item count (min 1 so it stays a valid divisor in `find_chunk`).
2610    let values_per_chunk = if num_chunks <= 1 {
2611        items_in_page.max(1)
2612    } else {
2613        1u64 << logs[0]
2614    };
2615    Ok(FlatValueCounts {
2616        logs,
2617        uniform,
2618        values_per_chunk,
2619        last_chunk_values,
2620    })
2621}
2622
2623/// Iterator over per-chunk value counts for a non-uniform flat page.  Non-last
2624/// chunks yield `1 << log`; the last yields the validated remaining item count.
2625fn flat_value_counts_iter(logs: &[u8], last_chunk_values: u64) -> impl Iterator<Item = u64> + '_ {
2626    let num_chunks = logs.len();
2627    (0..num_chunks).map(move |i| {
2628        if i + 1 < num_chunks {
2629            1u64 << logs[i]
2630        } else {
2631            last_chunk_values
2632        }
2633    })
2634}
2635
2636/// Builds the compact per-chunk index from the metadata words and, for nested
2637/// pages, the raw repetition-index bytes.  The row axis is picked by page shape:
2638/// `UniformFlat` when all non-last chunks share a value count (fixed-width /
2639/// bitpacking), `Flat` for non-uniform flat pages (RLE / FSST), else `Nested`.
2640fn build_chunk_index(
2641    words: &Words,
2642    items_in_page: u64,
2643    base: u64,
2644    data_buf_size: u64,
2645    rep_index_bytes: Option<&[u8]>,
2646    repetition_index_depth: u16,
2647) -> Result<MiniBlockChunkIndex> {
2648    let num_chunks = words.len();
2649    // Validate item counts before byte sizes because both share a metadata word,
2650    // and an invalid count must not reach the final-chunk subtraction.
2651    let value_counts = analyze_value_counts(words, items_in_page)?;
2652
2653    // Each chunk stores `(divided_bytes + 1) * MINIBLOCK_ALIGNMENT` bytes, so the
2654    // deltas are the chunk sizes and their grand total is the data buffer size.
2655    let byte_starts = PrefixSums::from_deltas(
2656        words
2657            .iter()
2658            .map(|word| ((word >> 4) as u64 + 1) * MINIBLOCK_ALIGNMENT as u64),
2659        num_chunks,
2660        data_buf_size,
2661    );
2662
2663    // Nested pages track rows via the repetition index and keep leaf item counts
2664    // separately; flat pages have row == value index, so value counts are rows.
2665    let rows = if let Some(rep_index_data) = rep_index_bytes {
2666        assert!(rep_index_data.len() % 8 == 0);
2667        let stride = repetition_index_depth as usize + 1;
2668        let (row_starts, has_trailer) = parse_nested_rep(rep_index_data, stride);
2669        let item_counts = if value_counts.uniform {
2670            ItemCounts::Uniform {
2671                values_per_chunk: value_counts.values_per_chunk,
2672                last_chunk_values: value_counts.last_chunk_values,
2673            }
2674        } else {
2675            ItemCounts::PerChunkLog {
2676                logs: value_counts.logs,
2677                last_chunk_values: value_counts.last_chunk_values,
2678            }
2679        };
2680        RowMapping::Nested {
2681            row_starts,
2682            has_trailer,
2683            item_counts,
2684        }
2685    } else {
2686        if value_counts.uniform {
2687            RowMapping::UniformFlat {
2688                values_per_chunk: value_counts.values_per_chunk,
2689                last_chunk_values: value_counts.last_chunk_values,
2690                num_chunks,
2691            }
2692        } else {
2693            let value_starts = PrefixSums::from_deltas(
2694                flat_value_counts_iter(&value_counts.logs, value_counts.last_chunk_values),
2695                num_chunks,
2696                items_in_page,
2697            );
2698            RowMapping::Flat { value_starts }
2699        }
2700    };
2701
2702    Ok(MiniBlockChunkIndex::new(base, byte_starts, rows))
2703}
2704
2705impl StructuralPageScheduler for MiniBlockScheduler {
2706    fn initialize<'a>(
2707        &'a mut self,
2708        io: &Arc<dyn EncodingsIo>,
2709    ) -> BoxFuture<'a, Result<Arc<dyn CachedPageData>>> {
2710        // We always need to fetch chunk metadata.  We may also need to fetch a dictionary and
2711        // we may also need to fetch the repetition index.  Here, we gather what buffers we
2712        // need.
2713        let (meta_buf_position, meta_buf_size) = self.buffer_offsets_and_sizes[0];
2714        let base = self.buffer_offsets_and_sizes[1].0;
2715        let data_buf_size = self.buffer_offsets_and_sizes[1].1;
2716        let mut bufs_needed = 1;
2717        if self.dictionary.is_some() {
2718            bufs_needed += 1;
2719        }
2720        if self.repetition_index_depth > 0 {
2721            bufs_needed += 1;
2722        }
2723        let mut required_ranges = Vec::with_capacity(bufs_needed);
2724        required_ranges.push(meta_buf_position..meta_buf_position + meta_buf_size);
2725        if let Some(ref dictionary) = self.dictionary {
2726            required_ranges.push(
2727                dictionary.dictionary_buf_position_and_size.0
2728                    ..dictionary.dictionary_buf_position_and_size.0
2729                        + dictionary.dictionary_buf_position_and_size.1,
2730            );
2731        }
2732        if self.repetition_index_depth > 0 {
2733            let (rep_index_pos, rep_index_size) = self.buffer_offsets_and_sizes.last().unwrap();
2734            required_ranges.push(*rep_index_pos..*rep_index_pos + *rep_index_size);
2735        }
2736        let io_req = io.submit_request(required_ranges, 0);
2737
2738        async move {
2739            let mut buffers = io_req.await?.into_iter().fuse();
2740            let meta_bytes = buffers.next().unwrap();
2741            let dictionary_bytes = self.dictionary.as_ref().and_then(|_| buffers.next());
2742            let rep_index_bytes = buffers.next();
2743
2744            let words = Words::from_bytes(meta_bytes, self.has_large_chunk)?;
2745            let chunk_index = build_chunk_index(
2746                &words,
2747                self.items_in_page,
2748                base,
2749                data_buf_size,
2750                rep_index_bytes.as_deref(),
2751                self.repetition_index_depth,
2752            )?;
2753
2754            // decode dictionary
2755            let dictionary = if let Some(ref mut dictionary) = self.dictionary {
2756                let dictionary_data = dictionary_bytes.unwrap();
2757                Some(Arc::new(dictionary.dictionary_decompressor.decompress(
2758                    Some(LanceBuffer::from_bytes(
2759                        dictionary_data,
2760                        dictionary.dictionary_data_alignment,
2761                    )),
2762                    dictionary.num_dictionary_items,
2763                )?))
2764            } else {
2765                None
2766            };
2767
2768            let page_meta = Arc::new(MiniBlockCacheableState {
2769                chunk_index,
2770                dictionary,
2771            });
2772            self.page_meta = Some(page_meta.clone());
2773            Ok(page_meta as Arc<dyn CachedPageData>)
2774        }
2775        .boxed()
2776    }
2777
2778    fn load(&mut self, data: &Arc<dyn CachedPageData>) {
2779        self.page_meta = Some(
2780            data.clone()
2781                .as_arc_any()
2782                .downcast::<MiniBlockCacheableState>()
2783                .unwrap(),
2784        );
2785    }
2786
2787    fn schedule_ranges(
2788        &self,
2789        ranges: &[Range<u64>],
2790        io: &Arc<dyn EncodingsIo>,
2791    ) -> Result<Vec<PageLoadTask>> {
2792        let num_rows = ranges.iter().map(|r| r.end - r.start).sum();
2793
2794        let page_meta = self.page_meta.as_ref().unwrap();
2795
2796        let chunk_instructions =
2797            ChunkInstructions::schedule_instructions(&page_meta.chunk_index, ranges);
2798
2799        debug_assert_eq!(
2800            num_rows,
2801            chunk_instructions
2802                .iter()
2803                .map(|ci| ci.rows_to_take)
2804                .sum::<u64>()
2805        );
2806
2807        let chunks_needed = chunk_instructions
2808            .iter()
2809            .map(|ci| ci.chunk_idx)
2810            .unique()
2811            .collect::<Vec<_>>();
2812
2813        let mut loaded_chunks = self.lookup_chunks(&chunks_needed);
2814        let chunk_ranges = loaded_chunks
2815            .iter()
2816            .map(|c| c.byte_range.clone())
2817            .collect::<Vec<_>>();
2818        let loaded_chunk_data = io.submit_request(chunk_ranges, self.priority);
2819
2820        let rep_decompressor = self.rep_decompressor.clone();
2821        let def_decompressor = self.def_decompressor.clone();
2822        let value_decompressor = self.value_decompressor.clone();
2823        let num_buffers = self.num_buffers;
2824        let has_large_chunk = self.has_large_chunk;
2825        let dictionary = page_meta
2826            .dictionary
2827            .as_ref()
2828            .map(|dictionary| dictionary.clone());
2829        let def_meaning = self.def_meaning.clone();
2830
2831        let res = async move {
2832            let loaded_chunk_data = loaded_chunk_data.await?;
2833            for (loaded_chunk, chunk_data) in loaded_chunks.iter_mut().zip(loaded_chunk_data) {
2834                loaded_chunk.data = LanceBuffer::from_bytes(chunk_data, 1);
2835            }
2836
2837            Ok(Box::new(MiniBlockDecoder {
2838                rep_decompressor,
2839                def_decompressor,
2840                value_decompressor,
2841                def_meaning,
2842                loaded_chunks: VecDeque::from_iter(loaded_chunks),
2843                instructions: VecDeque::from(chunk_instructions),
2844                offset_in_current_chunk: 0,
2845                dictionary,
2846                num_rows,
2847                num_buffers,
2848                has_large_chunk,
2849            }) as Box<dyn StructuralPageDecoder>)
2850        }
2851        .boxed();
2852        let page_load_task = PageLoadTask {
2853            decoder_fut: res,
2854            num_rows,
2855        };
2856        Ok(vec![page_load_task])
2857    }
2858}
2859
2860#[derive(Debug, Clone, Copy)]
2861struct FullZipRepIndexDetails {
2862    buf_position: u64,
2863    bytes_per_value: u64, // Will be 1, 2, 4, or 8
2864}
2865
2866#[derive(Debug)]
2867enum PerValueDecompressor {
2868    Fixed(Arc<dyn FixedPerValueDecompressor>),
2869    Variable(Arc<dyn VariablePerValueDecompressor>),
2870}
2871
2872#[derive(Debug)]
2873struct FullZipDecodeDetails {
2874    value_decompressor: PerValueDecompressor,
2875    def_meaning: Arc<[DefinitionInterpretation]>,
2876    ctrl_word_parser: ControlWordParser,
2877    max_rep: u16,
2878    max_visible_def: u16,
2879}
2880
2881/// Describes where FullZip byte ranges should be read from.
2882///
2883/// FullZip decoding always needs a list of byte ranges, but those bytes can come
2884/// from two different places:
2885/// - Remote I/O (normal path): ranges are fetched from the underlying `EncodingsIo`.
2886/// - A prefetched full page (full scan fast path): the entire page has already been
2887///   loaded once and ranges should be sliced from memory.
2888///
2889/// This abstraction keeps scheduling code focused on "which ranges are needed"
2890/// instead of "how bytes are fetched", and it lets full-page scans avoid the
2891/// two-stage rep-index -> data I/O pipeline.
2892#[derive(Debug, Clone)]
2893enum FullZipReadSource {
2894    /// Fetch ranges from the storage backend through the encoding I/O interface.
2895    Remote(Arc<dyn EncodingsIo>),
2896    /// Slice ranges from an already-loaded FullZip page buffer.
2897    PrefetchedPage { base_offset: u64, data: LanceBuffer },
2898}
2899
2900impl FullZipReadSource {
2901    /// Materialize the requested ranges as decode-ready `LanceBuffer`s.
2902    ///
2903    /// The returned buffers preserve the input range order.
2904    fn fetch(
2905        &self,
2906        ranges: &[Range<u64>],
2907        priority: u64,
2908    ) -> BoxFuture<'static, Result<VecDeque<LanceBuffer>>> {
2909        match self {
2910            Self::Remote(io) => {
2911                let io = io.clone();
2912                let ranges = ranges.to_vec();
2913                async move {
2914                    let data = io.submit_request(ranges, priority).await?;
2915                    Ok(data
2916                        .into_iter()
2917                        .map(|bytes| LanceBuffer::from_bytes(bytes, 1))
2918                        .collect::<VecDeque<_>>())
2919                }
2920                .boxed()
2921            }
2922            Self::PrefetchedPage { base_offset, data } => {
2923                let base_offset = *base_offset;
2924                let data = data.clone();
2925                let page_end = base_offset + data.len() as u64;
2926                std::future::ready(
2927                    ranges
2928                        .iter()
2929                        .map(|range| {
2930                            if range.start > range.end
2931                                || range.start < base_offset
2932                                || range.end > page_end
2933                            {
2934                                return Err(Error::internal(format!(
2935                                    "Requested range {:?} is outside page range {}..{}",
2936                                    range, base_offset, page_end
2937                                )));
2938                            }
2939                            let start = (range.start - base_offset) as usize;
2940                            let len = (range.end - range.start) as usize;
2941                            Ok(data.slice_with_length(start, len))
2942                        })
2943                        .collect::<Result<VecDeque<_>>>(),
2944                )
2945                .boxed()
2946            }
2947        }
2948    }
2949}
2950
2951/// A scheduler for full-zip encoded data
2952///
2953/// When the data type has a fixed-width then we simply need to map from
2954/// row ranges to byte ranges using the fixed-width of the data type.
2955///
2956/// When the data type is variable-width or has any repetition then a
2957/// repetition index is required.
2958#[derive(Debug)]
2959pub struct FullZipScheduler {
2960    data_buf_position: u64,
2961    data_buf_size: u64,
2962    rep_index: Option<FullZipRepIndexDetails>,
2963    priority: u64,
2964    rows_in_page: u64,
2965    bits_per_offset: u8,
2966    details: Arc<FullZipDecodeDetails>,
2967    /// Cached state containing the decoded repetition index
2968    cached_state: Option<Arc<FullZipCacheableState>>,
2969    /// Whether repetition index metadata should be cached during initialize.
2970    enable_cache: bool,
2971}
2972
2973impl FullZipScheduler {
2974    fn try_new(
2975        buffer_offsets_and_sizes: &[(u64, u64)],
2976        priority: u64,
2977        rows_in_page: u64,
2978        layout: &pb21::FullZipLayout,
2979        decompressors: &dyn DecompressionStrategy,
2980    ) -> Result<Self> {
2981        let (data_buf_position, data_buf_size) = buffer_offsets_and_sizes[0];
2982        let rep_index = buffer_offsets_and_sizes.get(1).map(|(pos, len)| {
2983            let num_reps = rows_in_page + 1;
2984            let bytes_per_rep = len / num_reps;
2985            debug_assert_eq!(len % num_reps, 0);
2986            debug_assert!(
2987                bytes_per_rep == 1
2988                    || bytes_per_rep == 2
2989                    || bytes_per_rep == 4
2990                    || bytes_per_rep == 8
2991            );
2992            FullZipRepIndexDetails {
2993                buf_position: *pos,
2994                bytes_per_value: bytes_per_rep,
2995            }
2996        });
2997
2998        let value_decompressor = match layout.details {
2999            Some(pb21::full_zip_layout::Details::BitsPerValue(_)) => {
3000                let decompressor = decompressors.create_fixed_per_value_decompressor(
3001                    layout.value_compression.as_ref().unwrap(),
3002                )?;
3003                PerValueDecompressor::Fixed(decompressor.into())
3004            }
3005            Some(pb21::full_zip_layout::Details::BitsPerOffset(_)) => {
3006                let decompressor = decompressors.create_variable_per_value_decompressor(
3007                    layout.value_compression.as_ref().unwrap(),
3008                )?;
3009                PerValueDecompressor::Variable(decompressor.into())
3010            }
3011            None => {
3012                panic!("Full-zip layout must have a `details` field");
3013            }
3014        };
3015        let ctrl_word_parser = ControlWordParser::new(
3016            layout.bits_rep.try_into().unwrap(),
3017            layout.bits_def.try_into().unwrap(),
3018        );
3019        let def_meaning = layout
3020            .layers
3021            .iter()
3022            .map(|l| ProtobufUtils21::repdef_layer_to_def_interp(*l))
3023            .collect::<Vec<_>>();
3024
3025        let max_rep = def_meaning.iter().filter(|d| d.is_list()).count() as u16;
3026        let max_visible_def = def_meaning
3027            .iter()
3028            .filter(|d| !d.is_list())
3029            .map(|d| d.num_def_levels())
3030            .sum();
3031
3032        let bits_per_offset = match layout.details {
3033            Some(pb21::full_zip_layout::Details::BitsPerValue(_)) => 32,
3034            Some(pb21::full_zip_layout::Details::BitsPerOffset(bits_per_offset)) => {
3035                bits_per_offset as u8
3036            }
3037            None => panic!("Full-zip layout must have a `details` field"),
3038        };
3039
3040        let details = Arc::new(FullZipDecodeDetails {
3041            value_decompressor,
3042            def_meaning: def_meaning.into(),
3043            ctrl_word_parser,
3044            max_rep,
3045            max_visible_def,
3046        });
3047        Ok(Self {
3048            data_buf_position,
3049            data_buf_size,
3050            rep_index,
3051            details,
3052            priority,
3053            rows_in_page,
3054            bits_per_offset,
3055            cached_state: None,
3056            enable_cache: false,
3057        })
3058    }
3059
3060    fn covers_entire_page(ranges: &[Range<u64>], rows_in_page: u64) -> bool {
3061        if ranges.is_empty() {
3062            return false;
3063        }
3064        let mut expected_start = 0;
3065        for range in ranges {
3066            if range.start != expected_start || range.end > rows_in_page || range.end < range.start
3067            {
3068                return false;
3069            }
3070            expected_start = range.end;
3071        }
3072        expected_start == rows_in_page
3073    }
3074
3075    fn create_page_load_task(
3076        io_future: BoxFuture<'static, Result<Vec<Bytes>>>,
3077        num_rows: u64,
3078        details: Arc<FullZipDecodeDetails>,
3079        bits_per_offset: u8,
3080    ) -> PageLoadTask {
3081        let load_task = async move {
3082            let buffers = io_future.await?;
3083            let data = buffers
3084                .into_iter()
3085                .map(|bytes| LanceBuffer::from_bytes(bytes, 1))
3086                .collect::<VecDeque<_>>();
3087            Self::create_decoder(details, data, num_rows, bits_per_offset)
3088        }
3089        .boxed();
3090        PageLoadTask {
3091            decoder_fut: load_task,
3092            num_rows,
3093        }
3094    }
3095
3096    /// Creates a decoder from the loaded data
3097    fn create_decoder(
3098        details: Arc<FullZipDecodeDetails>,
3099        data: VecDeque<LanceBuffer>,
3100        num_rows: u64,
3101        bits_per_offset: u8,
3102    ) -> Result<Box<dyn StructuralPageDecoder>> {
3103        match &details.value_decompressor {
3104            PerValueDecompressor::Fixed(decompressor) => {
3105                let bits_per_value = decompressor.bits_per_value();
3106                if bits_per_value % 8 != 0 {
3107                    return Err(lance_core::Error::not_supported_source("Bit-packed full-zip encoding (non-byte-aligned values) is not yet implemented".into()));
3108                }
3109                let bytes_per_value = bits_per_value / 8;
3110                let total_bytes_per_value =
3111                    bytes_per_value as usize + details.ctrl_word_parser.bytes_per_word();
3112                // total_bytes_per_value == 0 is valid for constant-null FSL pages written by
3113                // earlier encoders that produced bits_per_value=0 with no ctrl-word bytes.
3114                // FixedFullZipDecoder::drain handles this case by producing AllNull output
3115                // without touching the (empty) data buffer.
3116                Ok(Box::new(FixedFullZipDecoder {
3117                    details,
3118                    data,
3119                    num_rows,
3120                    offset_in_current: 0,
3121                    bytes_per_value: bytes_per_value as usize,
3122                    total_bytes_per_value,
3123                }) as Box<dyn StructuralPageDecoder>)
3124            }
3125            PerValueDecompressor::Variable(_decompressor) => {
3126                Ok(Box::new(VariableFullZipDecoder::new(
3127                    details,
3128                    data,
3129                    num_rows,
3130                    bits_per_offset,
3131                    bits_per_offset,
3132                )?))
3133            }
3134        }
3135    }
3136
3137    /// Extracts byte ranges from a repetition index buffer
3138    /// The buffer contains pairs of (start, end) values for each range
3139    fn extract_byte_ranges_from_pairs(
3140        buffer: LanceBuffer,
3141        bytes_per_value: u64,
3142        data_buf_position: u64,
3143    ) -> Vec<Range<u64>> {
3144        ByteUnpacker::new(buffer, bytes_per_value as usize)
3145            .chunks(2)
3146            .into_iter()
3147            .map(|mut c| {
3148                let start = c.next().unwrap() + data_buf_position;
3149                let end = c.next().unwrap() + data_buf_position;
3150                start..end
3151            })
3152            .collect::<Vec<_>>()
3153    }
3154
3155    /// Extracts byte ranges from a cached repetition index buffer
3156    /// The buffer contains all values and we need to extract specific ranges
3157    fn extract_byte_ranges_from_cached(
3158        buffer: &LanceBuffer,
3159        ranges: &[Range<u64>],
3160        bytes_per_value: u64,
3161        data_buf_position: u64,
3162    ) -> Vec<Range<u64>> {
3163        ranges
3164            .iter()
3165            .map(|r| {
3166                let start_offset = (r.start * bytes_per_value) as usize;
3167                let end_offset = (r.end * bytes_per_value) as usize;
3168
3169                let start_slice = &buffer[start_offset..start_offset + bytes_per_value as usize];
3170                let start_val =
3171                    ByteUnpacker::new(start_slice.iter().copied(), bytes_per_value as usize)
3172                        .next()
3173                        .unwrap();
3174
3175                let end_slice = &buffer[end_offset..end_offset + bytes_per_value as usize];
3176                let end_val =
3177                    ByteUnpacker::new(end_slice.iter().copied(), bytes_per_value as usize)
3178                        .next()
3179                        .unwrap();
3180
3181                (data_buf_position + start_val)..(data_buf_position + end_val)
3182            })
3183            .collect()
3184    }
3185
3186    /// Computes the ranges in the repetition index that need to be loaded
3187    fn compute_rep_index_ranges(
3188        ranges: &[Range<u64>],
3189        rep_index: &FullZipRepIndexDetails,
3190    ) -> Vec<Range<u64>> {
3191        ranges
3192            .iter()
3193            .flat_map(|r| {
3194                let first_val_start =
3195                    rep_index.buf_position + (r.start * rep_index.bytes_per_value);
3196                let first_val_end = first_val_start + rep_index.bytes_per_value;
3197                let last_val_start = rep_index.buf_position + (r.end * rep_index.bytes_per_value);
3198                let last_val_end = last_val_start + rep_index.bytes_per_value;
3199                [first_val_start..first_val_end, last_val_start..last_val_end]
3200            })
3201            .collect()
3202    }
3203
3204    /// Schedules ranges in the presence of a repetition index
3205    fn schedule_ranges_rep(
3206        &self,
3207        ranges: &[Range<u64>],
3208        io: &Arc<dyn EncodingsIo>,
3209        rep_index: FullZipRepIndexDetails,
3210    ) -> Result<Vec<PageLoadTask>> {
3211        let num_rows = ranges.iter().map(|r| r.end - r.start).sum();
3212        let data_buf_position = self.data_buf_position;
3213        let priority = self.priority;
3214        let details = self.details.clone();
3215        let bits_per_offset = self.bits_per_offset;
3216
3217        if Self::covers_entire_page(ranges, self.rows_in_page) {
3218            let full_range = self.data_buf_position..(self.data_buf_position + self.data_buf_size);
3219            let page_data = io.submit_single(full_range.clone(), priority);
3220            let load_task = async move {
3221                let page_data = page_data.await?;
3222                let source = FullZipReadSource::PrefetchedPage {
3223                    base_offset: full_range.start,
3224                    data: LanceBuffer::from_bytes(page_data, 1),
3225                };
3226                let read_ranges = vec![full_range];
3227                let data = source.fetch(&read_ranges, priority).await?;
3228                Self::create_decoder(details, data, num_rows, bits_per_offset)
3229            }
3230            .boxed();
3231            let page_load_task = PageLoadTask {
3232                decoder_fut: load_task,
3233                num_rows,
3234            };
3235            return Ok(vec![page_load_task]);
3236        }
3237
3238        if let Some(cached_state) = &self.cached_state {
3239            let byte_ranges = Self::extract_byte_ranges_from_cached(
3240                &cached_state.rep_index_buffer,
3241                ranges,
3242                rep_index.bytes_per_value,
3243                data_buf_position,
3244            );
3245            let io_future = io.submit_request(byte_ranges, priority);
3246            let page_load_task =
3247                Self::create_page_load_task(io_future, num_rows, details, bits_per_offset);
3248            return Ok(vec![page_load_task]);
3249        }
3250
3251        let rep_ranges = Self::compute_rep_index_ranges(ranges, &rep_index);
3252        let rep_data = io.submit_request(rep_ranges, priority);
3253        let io_clone = io.clone();
3254        let load_task = async move {
3255            let rep_data = rep_data.await?;
3256            let rep_buffer = LanceBuffer::concat(
3257                &rep_data
3258                    .into_iter()
3259                    .map(|d| LanceBuffer::from_bytes(d, 1))
3260                    .collect::<Vec<_>>(),
3261            );
3262            let byte_ranges = Self::extract_byte_ranges_from_pairs(
3263                rep_buffer,
3264                rep_index.bytes_per_value,
3265                data_buf_position,
3266            );
3267            let source = FullZipReadSource::Remote(io_clone);
3268            let data = source.fetch(&byte_ranges, priority).await?;
3269            Self::create_decoder(details, data, num_rows, bits_per_offset)
3270        }
3271        .boxed();
3272        let page_load_task = PageLoadTask {
3273            decoder_fut: load_task,
3274            num_rows,
3275        };
3276        Ok(vec![page_load_task])
3277    }
3278
3279    // In the simple case there is no repetition and we just have large fixed-width
3280    // rows of data.  We can just map row ranges to byte ranges directly using the
3281    // fixed-width of the data type.
3282    fn schedule_ranges_simple(
3283        &self,
3284        ranges: &[Range<u64>],
3285        io: &Arc<dyn EncodingsIo>,
3286    ) -> Result<Vec<PageLoadTask>> {
3287        // Convert row ranges to item ranges (i.e. multiply by items per row)
3288        let num_rows = ranges.iter().map(|r| r.end - r.start).sum();
3289
3290        let PerValueDecompressor::Fixed(decompressor) = &self.details.value_decompressor else {
3291            unreachable!()
3292        };
3293
3294        // Convert item ranges to byte ranges (i.e. multiply by bytes per item)
3295        let bits_per_value = decompressor.bits_per_value();
3296        if !bits_per_value.is_multiple_of(8) {
3297            return Err(Error::invalid_input_source(
3298                format!(
3299                    "Full-zip fixed-width values must be byte aligned, got {} bits per value",
3300                    bits_per_value
3301                )
3302                .into(),
3303            ));
3304        }
3305        let bytes_per_value = bits_per_value / 8;
3306        let bytes_per_cw = self.details.ctrl_word_parser.bytes_per_word();
3307        let total_bytes_per_value = bytes_per_value + bytes_per_cw as u64;
3308        let byte_ranges = ranges
3309            .iter()
3310            .map(|r| {
3311                debug_assert!(r.end <= self.rows_in_page);
3312                let start = self.data_buf_position + r.start * total_bytes_per_value;
3313                let end = self.data_buf_position + r.end * total_bytes_per_value;
3314                start..end
3315            })
3316            .collect::<Vec<_>>();
3317
3318        let io_future = io.submit_request(byte_ranges, self.priority);
3319        let page_load_task = Self::create_page_load_task(
3320            io_future,
3321            num_rows,
3322            self.details.clone(),
3323            self.bits_per_offset,
3324        );
3325        Ok(vec![page_load_task])
3326    }
3327}
3328
3329/// Cacheable state for FullZip encoding, storing the decoded repetition index
3330#[derive(Debug)]
3331struct FullZipCacheableState {
3332    /// The raw repetition index buffer for future decoding
3333    rep_index_buffer: LanceBuffer,
3334}
3335
3336impl DeepSizeOf for FullZipCacheableState {
3337    fn deep_size_of_children(&self, _context: &mut Context) -> usize {
3338        self.rep_index_buffer.len()
3339    }
3340}
3341
3342impl CachedPageData for FullZipCacheableState {
3343    fn as_arc_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync + 'static> {
3344        self
3345    }
3346}
3347
3348impl StructuralPageScheduler for FullZipScheduler {
3349    fn initialize<'a>(
3350        &'a mut self,
3351        io: &Arc<dyn EncodingsIo>,
3352    ) -> BoxFuture<'a, Result<Arc<dyn CachedPageData>>> {
3353        if self.enable_cache
3354            && let Some(rep_index) = self.rep_index
3355        {
3356            let total_size = (self.rows_in_page + 1) * rep_index.bytes_per_value;
3357            let rep_index_range = rep_index.buf_position..(rep_index.buf_position + total_size);
3358            let io_clone = io.clone();
3359            return async move {
3360                let rep_index_data = io_clone.submit_request(vec![rep_index_range], 0).await?;
3361                let state = Arc::new(FullZipCacheableState {
3362                    rep_index_buffer: LanceBuffer::from_bytes(rep_index_data[0].clone(), 1),
3363                });
3364                self.cached_state = Some(state.clone());
3365                Ok(state as Arc<dyn CachedPageData>)
3366            }
3367            .boxed();
3368        }
3369        std::future::ready(Ok(Arc::new(NoCachedPageData) as Arc<dyn CachedPageData>)).boxed()
3370    }
3371
3372    /// Loads previously cached repetition index data from the cache system.
3373    /// This method is called when a scheduler instance needs to use cached data
3374    /// that was initialized by another instance or in a previous operation.
3375    fn load(&mut self, cache: &Arc<dyn CachedPageData>) {
3376        // Try to downcast to our specific cache type
3377        if let Ok(cached_state) = cache
3378            .clone()
3379            .as_arc_any()
3380            .downcast::<FullZipCacheableState>()
3381        {
3382            // Store the cached state for use in schedule_ranges
3383            self.cached_state = Some(cached_state);
3384        }
3385    }
3386
3387    fn schedule_ranges(
3388        &self,
3389        ranges: &[Range<u64>],
3390        io: &Arc<dyn EncodingsIo>,
3391    ) -> Result<Vec<PageLoadTask>> {
3392        if let Some(rep_index) = self.rep_index {
3393            self.schedule_ranges_rep(ranges, io, rep_index)
3394        } else {
3395            self.schedule_ranges_simple(ranges, io)
3396        }
3397    }
3398}
3399
3400/// A decoder for full-zip encoded data when the data has a fixed-width
3401///
3402/// Here we need to unzip the control words from the values themselves and
3403/// then decompress the requested values.
3404///
3405/// We use a PerValueDecompressor because we will only be decompressing the
3406/// requested data.  This decoder / scheduler does not do any read amplification.
3407#[derive(Debug)]
3408struct FixedFullZipDecoder {
3409    details: Arc<FullZipDecodeDetails>,
3410    data: VecDeque<LanceBuffer>,
3411    offset_in_current: usize,
3412    bytes_per_value: usize,
3413    total_bytes_per_value: usize,
3414    num_rows: u64,
3415}
3416
3417impl FixedFullZipDecoder {
3418    fn slice_next_task(&mut self, num_rows: u64) -> FullZipDecodeTaskItem {
3419        debug_assert!(num_rows > 0);
3420        let cur_buf = self.data.front_mut().unwrap();
3421        let start = self.offset_in_current;
3422        if self.details.ctrl_word_parser.has_rep() {
3423            // This is a slightly slower path.  In order to figure out where to split we need to
3424            // examine the rep index so we can convert num_lists to num_rows
3425            let mut rows_started = 0;
3426            // We always need at least one value.  Now loop through until we have passed num_rows
3427            // values
3428            let mut num_items = 0;
3429            while self.offset_in_current < cur_buf.len() {
3430                let control = self.details.ctrl_word_parser.parse_desc(
3431                    &cur_buf[self.offset_in_current..],
3432                    self.details.max_rep,
3433                    self.details.max_visible_def,
3434                );
3435                if control.is_new_row {
3436                    if rows_started == num_rows {
3437                        break;
3438                    }
3439                    rows_started += 1;
3440                }
3441                num_items += 1;
3442                if control.is_visible {
3443                    self.offset_in_current += self.total_bytes_per_value;
3444                } else {
3445                    self.offset_in_current += self.details.ctrl_word_parser.bytes_per_word();
3446                }
3447            }
3448
3449            let task_slice = cur_buf.slice_with_length(start, self.offset_in_current - start);
3450            if self.offset_in_current == cur_buf.len() {
3451                self.data.pop_front();
3452                self.offset_in_current = 0;
3453            }
3454
3455            FullZipDecodeTaskItem {
3456                data: PerValueDataBlock::Fixed(FixedWidthDataBlock {
3457                    data: task_slice,
3458                    bits_per_value: self.bytes_per_value as u64 * 8,
3459                    num_values: num_items,
3460                    block_info: BlockInfo::new(),
3461                }),
3462                rows_in_buf: rows_started,
3463            }
3464        } else {
3465            // If there's no repetition we can calculate the slicing point by just multiplying
3466            // the number of rows by the total bytes per value
3467            let cur_buf = self.data.front_mut().unwrap();
3468            let bytes_avail = cur_buf.len() - self.offset_in_current;
3469            let offset_in_cur = self.offset_in_current;
3470
3471            let bytes_needed = num_rows as usize * self.total_bytes_per_value;
3472            let mut rows_taken = num_rows;
3473            let task_slice = if bytes_needed >= bytes_avail {
3474                self.offset_in_current = 0;
3475                rows_taken = bytes_avail as u64 / self.total_bytes_per_value as u64;
3476                self.data
3477                    .pop_front()
3478                    .unwrap()
3479                    .slice_with_length(offset_in_cur, bytes_avail)
3480            } else {
3481                self.offset_in_current += bytes_needed;
3482                cur_buf.slice_with_length(offset_in_cur, bytes_needed)
3483            };
3484            FullZipDecodeTaskItem {
3485                data: PerValueDataBlock::Fixed(FixedWidthDataBlock {
3486                    data: task_slice,
3487                    bits_per_value: self.bytes_per_value as u64 * 8,
3488                    num_values: rows_taken,
3489                    block_info: BlockInfo::new(),
3490                }),
3491                rows_in_buf: rows_taken,
3492            }
3493        }
3494    }
3495}
3496
3497impl StructuralPageDecoder for FixedFullZipDecoder {
3498    fn drain(&mut self, num_rows: u64) -> Result<Box<dyn DecodePageTask>> {
3499        if self.total_bytes_per_value == 0 {
3500            // No bytes per row: constant-null page with no ctrl-word bytes.
3501            // The decompressor (ConstantDecompressor) ignores its input and returns AllNull.
3502            return Ok(Box::new(FixedFullZipDecodeTask {
3503                details: self.details.clone(),
3504                data: vec![FullZipDecodeTaskItem {
3505                    data: PerValueDataBlock::Fixed(FixedWidthDataBlock {
3506                        data: LanceBuffer::empty(),
3507                        bits_per_value: 0,
3508                        num_values: num_rows,
3509                        block_info: BlockInfo::new(),
3510                    }),
3511                    rows_in_buf: num_rows,
3512                }],
3513                bytes_per_value: 0,
3514                num_rows: num_rows as usize,
3515            }));
3516        }
3517        let mut task_data = Vec::with_capacity(self.data.len());
3518        let mut remaining = num_rows;
3519        while remaining > 0 {
3520            let task_item = self.slice_next_task(remaining);
3521            remaining -= task_item.rows_in_buf;
3522            task_data.push(task_item);
3523        }
3524        Ok(Box::new(FixedFullZipDecodeTask {
3525            details: self.details.clone(),
3526            data: task_data,
3527            bytes_per_value: self.bytes_per_value,
3528            num_rows: num_rows as usize,
3529        }))
3530    }
3531
3532    fn num_rows(&self) -> u64 {
3533        self.num_rows
3534    }
3535}
3536
3537/// A decoder for full-zip encoded data when the data has a variable-width
3538///
3539/// Here we need to unzip the control words AND lengths from the values and
3540/// then decompress the requested values.
3541#[derive(Debug)]
3542struct VariableFullZipDecoder {
3543    details: Arc<FullZipDecodeDetails>,
3544    decompressor: Arc<dyn VariablePerValueDecompressor>,
3545    data: LanceBuffer,
3546    offsets: LanceBuffer,
3547    rep: ScalarBuffer<u16>,
3548    def: ScalarBuffer<u16>,
3549    repdef_starts: Vec<usize>,
3550    data_starts: Vec<usize>,
3551    offset_starts: Vec<usize>,
3552    visible_item_counts: Vec<u64>,
3553    bits_per_offset: u8,
3554    current_idx: usize,
3555    num_rows: u64,
3556}
3557
3558impl VariableFullZipDecoder {
3559    fn new(
3560        details: Arc<FullZipDecodeDetails>,
3561        data: VecDeque<LanceBuffer>,
3562        num_rows: u64,
3563        in_bits_per_length: u8,
3564        out_bits_per_offset: u8,
3565    ) -> Result<Self> {
3566        let decompressor = match details.value_decompressor {
3567            PerValueDecompressor::Variable(ref d) => d.clone(),
3568            _ => unreachable!(),
3569        };
3570
3571        assert_eq!(in_bits_per_length % 8, 0);
3572        assert!(out_bits_per_offset == 32 || out_bits_per_offset == 64);
3573
3574        let mut decoder = Self {
3575            details,
3576            decompressor,
3577            data: LanceBuffer::empty(),
3578            offsets: LanceBuffer::empty(),
3579            rep: LanceBuffer::empty().borrow_to_typed_slice(),
3580            def: LanceBuffer::empty().borrow_to_typed_slice(),
3581            bits_per_offset: out_bits_per_offset,
3582            repdef_starts: Vec::with_capacity(num_rows as usize + 1),
3583            data_starts: Vec::with_capacity(num_rows as usize + 1),
3584            offset_starts: Vec::with_capacity(num_rows as usize + 1),
3585            visible_item_counts: Vec::with_capacity(num_rows as usize + 1),
3586            current_idx: 0,
3587            num_rows,
3588        };
3589
3590        // There's no great time to do this and this is the least worst time.  If we don't unzip then
3591        // we can't slice the data during the decode phase.  This is because we need the offsets to be
3592        // unpacked to know where the values start and end.
3593        //
3594        // We don't want to unzip on the decode thread because that is a single-threaded path
3595        // We don't want to unzip on the scheduling thread because that is a single-threaded path
3596        //
3597        // Fortunately, we know variable length data will always be read indirectly and so we can do it
3598        // here, which should be on the indirect thread.  The primary disadvantage to doing it here is that
3599        // we load all the data into memory and then throw it away only to load it all into memory again during
3600        // the decode.
3601        //
3602        // There are some alternatives to investigate:
3603        //   - Instead of just reading the beginning and end of the rep index we could read the entire
3604        //     range in between.  This will give us the break points that we need for slicing and won't increase
3605        //     the number of IOPs but it will mean we are doing more total I/O and we need to load the rep index
3606        //     even when doing a full scan.
3607        //   - We could force each decode task to do a full unzip of all the data.  Each decode task now
3608        //     has to do more work but the work is all fused.
3609        //   - We could just try doing this work on the decode thread and see if it is a problem.
3610        decoder.unzip(data, in_bits_per_length, out_bits_per_offset, num_rows)?;
3611
3612        Ok(decoder)
3613    }
3614
3615    fn slice_batch_data_and_rebase_offsets_typed<T>(
3616        data: &LanceBuffer,
3617        offsets: &LanceBuffer,
3618    ) -> Result<(LanceBuffer, LanceBuffer)>
3619    where
3620        T: arrow_buffer::ArrowNativeType
3621            + Copy
3622            + PartialOrd
3623            + std::ops::Sub<Output = T>
3624            + std::fmt::Display
3625            + TryInto<usize>,
3626    {
3627        let offsets_slice = offsets.borrow_to_typed_slice::<T>();
3628        let offsets_slice = offsets_slice.as_ref();
3629        if offsets_slice.is_empty() {
3630            return Err(Error::internal(
3631                "Variable offsets cannot be empty".to_string(),
3632            ));
3633        }
3634
3635        let base = offsets_slice[0];
3636        let end = *offsets_slice.last().unwrap();
3637        if end < base {
3638            return Err(Error::internal(format!(
3639                "Invalid variable offsets: end ({end}) is less than base ({base})"
3640            )));
3641        }
3642
3643        let data_start = base.try_into().map_err(|_| {
3644            Error::internal(format!("Variable offset ({base}) does not fit into usize"))
3645        })?;
3646        let data_end = end.try_into().map_err(|_| {
3647            Error::internal(format!("Variable offset ({end}) does not fit into usize"))
3648        })?;
3649        if data_end > data.len() {
3650            return Err(Error::internal(format!(
3651                "Invalid variable offsets: end ({data_end}) exceeds data len ({})",
3652                data.len()
3653            )));
3654        }
3655
3656        let mut rebased_offsets = Vec::with_capacity(offsets_slice.len());
3657        for &offset in offsets_slice {
3658            if offset < base {
3659                return Err(Error::internal(format!(
3660                    "Invalid variable offsets: offset ({offset}) is less than base ({base})"
3661                )));
3662            }
3663            rebased_offsets.push(offset - base);
3664        }
3665
3666        let sliced_data = data.slice_with_length(data_start, data_end - data_start);
3667        // Copy into a compact buffer so each output batch owns only what it references.
3668        let sliced_data = LanceBuffer::copy_slice(&sliced_data);
3669        let rebased_offsets = LanceBuffer::reinterpret_vec(rebased_offsets);
3670        Ok((sliced_data, rebased_offsets))
3671    }
3672
3673    fn slice_batch_data_and_rebase_offsets(
3674        data: &LanceBuffer,
3675        offsets: &LanceBuffer,
3676        bits_per_offset: u8,
3677    ) -> Result<(LanceBuffer, LanceBuffer)> {
3678        match bits_per_offset {
3679            32 => Self::slice_batch_data_and_rebase_offsets_typed::<u32>(data, offsets),
3680            64 => Self::slice_batch_data_and_rebase_offsets_typed::<u64>(data, offsets),
3681            _ => Err(Error::internal(format!(
3682                "Unsupported bits_per_offset={bits_per_offset}"
3683            ))),
3684        }
3685    }
3686
3687    /// Reads a single length prefix from the front of `data`.
3688    ///
3689    /// The bytes come from the file. A page whose item walk ends with a partial
3690    /// trailing item leaves fewer than `bits_per_offset / 8` bytes here, so this
3691    /// is bounds checked and reports a corrupt file rather than reading past the
3692    /// end of the buffer.
3693    fn parse_length(data: &[u8], bits_per_offset: u8) -> Result<u64> {
3694        let width = bits_per_offset as usize / 8;
3695        if data.len() < width {
3696            return Err(Error::corrupt_file_named(
3697                "variable_full_zip",
3698                format!(
3699                    "truncated length prefix: {} byte(s) remain in the page buffer but a \
3700                     {}-bit length prefix requires {}",
3701                    data.len(),
3702                    bits_per_offset,
3703                    width
3704                ),
3705            ));
3706        }
3707        Ok(match bits_per_offset {
3708            8 => data[0] as u64,
3709            16 => u16::from_le_bytes(data[..2].try_into().unwrap()) as u64,
3710            32 => u32::from_le_bytes(data[..4].try_into().unwrap()) as u64,
3711            64 => u64::from_le_bytes(data[..8].try_into().unwrap()),
3712            _ => unreachable!(),
3713        })
3714    }
3715
3716    fn unzip(
3717        &mut self,
3718        data: VecDeque<LanceBuffer>,
3719        in_bits_per_length: u8,
3720        out_bits_per_offset: u8,
3721        num_rows: u64,
3722    ) -> Result<()> {
3723        // This undercounts if there are lists but, at this point, we don't really know how many items we have
3724        let mut rep = Vec::with_capacity(num_rows as usize);
3725        let mut def = Vec::with_capacity(num_rows as usize);
3726        let bytes_cw = self.details.ctrl_word_parser.bytes_per_word() * num_rows as usize;
3727
3728        // This undercounts if there are lists
3729        // It can also overcount if there are invisible items
3730        let bytes_per_offset = out_bits_per_offset as usize / 8;
3731        let bytes_offsets = bytes_per_offset * (num_rows as usize + 1);
3732        let mut offsets_data = Vec::with_capacity(bytes_offsets);
3733
3734        let bytes_per_length = in_bits_per_length as usize / 8;
3735        let bytes_lengths = bytes_per_length * num_rows as usize;
3736
3737        let bytes_data = data.iter().map(|d| d.len()).sum::<usize>();
3738        // This overcounts since bytes_lengths and bytes_cw are undercounts
3739        // It can also undercount if there are invisible items (hence the saturating_sub)
3740        let mut unzipped_data =
3741            Vec::with_capacity((bytes_data - bytes_cw).saturating_sub(bytes_lengths));
3742
3743        let mut current_offset = 0_u64;
3744        let mut visible_item_count = 0_u64;
3745        for databuf in data.into_iter() {
3746            let mut databuf = databuf.as_ref();
3747            while !databuf.is_empty() {
3748                let data_start = unzipped_data.len();
3749                let offset_start = offsets_data.len();
3750                // We might have only-rep or only-def, neither, or both.  They move at the same
3751                // speed though so we only need one index into it
3752                let repdef_start = rep.len().max(def.len());
3753                // TODO: Kind of inefficient we parse the control word twice here
3754                let ctrl_desc = self.details.ctrl_word_parser.parse_desc(
3755                    databuf,
3756                    self.details.max_rep,
3757                    self.details.max_visible_def,
3758                );
3759                self.details
3760                    .ctrl_word_parser
3761                    .parse(databuf, &mut rep, &mut def);
3762                databuf = &databuf[self.details.ctrl_word_parser.bytes_per_word()..];
3763
3764                if ctrl_desc.is_new_row {
3765                    self.repdef_starts.push(repdef_start);
3766                    self.data_starts.push(data_start);
3767                    self.offset_starts.push(offset_start);
3768                    self.visible_item_counts.push(visible_item_count);
3769                }
3770                if ctrl_desc.is_visible {
3771                    visible_item_count += 1;
3772                    if ctrl_desc.is_valid_item {
3773                        let length = Self::parse_length(databuf, in_bits_per_length)?;
3774                        match out_bits_per_offset {
3775                            32 => offsets_data
3776                                .extend_from_slice(&(current_offset as u32).to_le_bytes()),
3777                            64 => offsets_data.extend_from_slice(&current_offset.to_le_bytes()),
3778                            _ => unreachable!(),
3779                        };
3780                        databuf = &databuf[bytes_per_offset..];
3781                        unzipped_data.extend_from_slice(&databuf[..length as usize]);
3782                        databuf = &databuf[length as usize..];
3783                        current_offset += length;
3784                    } else {
3785                        // Null items still get an offset
3786                        match out_bits_per_offset {
3787                            32 => offsets_data
3788                                .extend_from_slice(&(current_offset as u32).to_le_bytes()),
3789                            64 => offsets_data.extend_from_slice(&current_offset.to_le_bytes()),
3790                            _ => unreachable!(),
3791                        }
3792                    }
3793                }
3794            }
3795        }
3796        self.repdef_starts.push(rep.len().max(def.len()));
3797        self.data_starts.push(unzipped_data.len());
3798        self.offset_starts.push(offsets_data.len());
3799        self.visible_item_counts.push(visible_item_count);
3800        match out_bits_per_offset {
3801            32 => offsets_data.extend_from_slice(&(current_offset as u32).to_le_bytes()),
3802            64 => offsets_data.extend_from_slice(&current_offset.to_le_bytes()),
3803            _ => unreachable!(),
3804        };
3805        self.rep = ScalarBuffer::from(rep);
3806        self.def = ScalarBuffer::from(def);
3807        self.data = LanceBuffer::from(unzipped_data);
3808        self.offsets = LanceBuffer::from(offsets_data);
3809        Ok(())
3810    }
3811}
3812
3813impl StructuralPageDecoder for VariableFullZipDecoder {
3814    fn drain(&mut self, num_rows: u64) -> Result<Box<dyn DecodePageTask>> {
3815        let start = self.current_idx;
3816        let end = start + num_rows as usize;
3817
3818        let offset_start = self.offset_starts[start];
3819        let offset_end = self.offset_starts[end] + (self.bits_per_offset as usize / 8);
3820        let offsets = self
3821            .offsets
3822            .slice_with_length(offset_start, offset_end - offset_start);
3823        // Keep each batch's variable data buffer bounded to the selected rows.
3824        let (data, offsets) =
3825            Self::slice_batch_data_and_rebase_offsets(&self.data, &offsets, self.bits_per_offset)?;
3826
3827        let repdef_start = self.repdef_starts[start];
3828        let repdef_end = self.repdef_starts[end];
3829        let rep = if self.rep.is_empty() {
3830            self.rep.clone()
3831        } else {
3832            self.rep.slice(repdef_start, repdef_end - repdef_start)
3833        };
3834        let def = if self.def.is_empty() {
3835            self.def.clone()
3836        } else {
3837            self.def.slice(repdef_start, repdef_end - repdef_start)
3838        };
3839
3840        let visible_item_counts_start = self.visible_item_counts[start];
3841        let visible_item_counts_end = self.visible_item_counts[end];
3842        let num_visible_items = visible_item_counts_end - visible_item_counts_start;
3843
3844        self.current_idx += num_rows as usize;
3845
3846        Ok(Box::new(VariableFullZipDecodeTask {
3847            details: self.details.clone(),
3848            decompressor: self.decompressor.clone(),
3849            data,
3850            offsets,
3851            bits_per_offset: self.bits_per_offset,
3852            num_visible_items,
3853            rep,
3854            def,
3855        }))
3856    }
3857
3858    fn num_rows(&self) -> u64 {
3859        self.num_rows
3860    }
3861}
3862
3863#[derive(Debug)]
3864struct VariableFullZipDecodeTask {
3865    details: Arc<FullZipDecodeDetails>,
3866    decompressor: Arc<dyn VariablePerValueDecompressor>,
3867    data: LanceBuffer,
3868    offsets: LanceBuffer,
3869    bits_per_offset: u8,
3870    num_visible_items: u64,
3871    rep: ScalarBuffer<u16>,
3872    def: ScalarBuffer<u16>,
3873}
3874
3875impl DecodePageTask for VariableFullZipDecodeTask {
3876    fn decode(self: Box<Self>) -> Result<DecodedPage> {
3877        let block = VariableWidthBlock {
3878            data: self.data,
3879            offsets: self.offsets,
3880            bits_per_offset: self.bits_per_offset,
3881            num_values: self.num_visible_items,
3882            block_info: BlockInfo::new(),
3883        };
3884        let decomopressed = self.decompressor.decompress(block)?;
3885        let rep = if self.rep.is_empty() {
3886            None
3887        } else {
3888            Some(self.rep.to_vec())
3889        };
3890        let def = if self.def.is_empty() {
3891            None
3892        } else {
3893            Some(self.def.to_vec())
3894        };
3895        let unraveler = RepDefUnraveler::new(
3896            rep,
3897            def,
3898            self.details.def_meaning.clone(),
3899            self.num_visible_items,
3900        );
3901        Ok(DecodedPage {
3902            data: decomopressed,
3903            repdef: unraveler,
3904        })
3905    }
3906}
3907
3908#[derive(Debug)]
3909struct FullZipDecodeTaskItem {
3910    data: PerValueDataBlock,
3911    rows_in_buf: u64,
3912}
3913
3914/// A task to unzip and decompress full-zip encoded data when that data
3915/// has a fixed-width.
3916#[derive(Debug)]
3917struct FixedFullZipDecodeTask {
3918    details: Arc<FullZipDecodeDetails>,
3919    data: Vec<FullZipDecodeTaskItem>,
3920    num_rows: usize,
3921    bytes_per_value: usize,
3922}
3923
3924impl DecodePageTask for FixedFullZipDecodeTask {
3925    fn decode(self: Box<Self>) -> Result<DecodedPage> {
3926        let estimated_size_bytes = if self.details.ctrl_word_parser.bytes_per_word() == 0 {
3927            let PerValueDecompressor::Fixed(decompressor) = &self.details.value_decompressor else {
3928                return Err(Error::internal(
3929                    "FixedFullZipDecodeTask requires a fixed-width decompressor",
3930                ));
3931            };
3932            decompressor
3933                .decoded_size_bytes(self.num_rows as u64)
3934                .unwrap_or_else(|| {
3935                    self.data
3936                        .iter()
3937                        .map(|task_item| task_item.data.data_size())
3938                        .sum::<u64>()
3939                        * 2
3940                })
3941        } else {
3942            // Rep/def levels can suppress values, so the exact output size is not known
3943            // until they are decoded. Keep the existing conservative estimate.
3944            self.data
3945                .iter()
3946                .map(|task_item| task_item.data.data_size())
3947                .sum::<u64>()
3948                * 2
3949        };
3950        let mut data_builder = DataBlockBuilder::with_capacity_estimate(estimated_size_bytes);
3951
3952        if self.details.ctrl_word_parser.bytes_per_word() == 0 {
3953            // Fast path, no need to unzip because there is no rep/def
3954            //
3955            // We decompress each buffer and add it to our output buffer
3956            for task_item in self.data.into_iter() {
3957                let PerValueDataBlock::Fixed(fixed_data) = task_item.data else {
3958                    unreachable!()
3959                };
3960                let PerValueDecompressor::Fixed(decompressor) = &self.details.value_decompressor
3961                else {
3962                    unreachable!()
3963                };
3964                debug_assert_eq!(fixed_data.num_values, task_item.rows_in_buf);
3965                let decompressed = decompressor.decompress(fixed_data, task_item.rows_in_buf)?;
3966                data_builder.append(&decompressed, 0..task_item.rows_in_buf)?;
3967            }
3968
3969            let unraveler = RepDefUnraveler::new(
3970                None,
3971                None,
3972                self.details.def_meaning.clone(),
3973                self.num_rows as u64,
3974            );
3975
3976            Ok(DecodedPage {
3977                data: data_builder.finish(),
3978                repdef: unraveler,
3979            })
3980        } else {
3981            // Slow path, unzipping needed
3982            let mut rep = Vec::with_capacity(self.num_rows);
3983            let mut def = Vec::with_capacity(self.num_rows);
3984
3985            for task_item in self.data.into_iter() {
3986                let PerValueDataBlock::Fixed(fixed_data) = task_item.data else {
3987                    unreachable!()
3988                };
3989                let mut buf_slice = fixed_data.data.as_ref();
3990                let num_values = fixed_data.num_values as usize;
3991                // We will be unzipping repdef in to `rep` and `def` and the
3992                // values into `values` (which contains the compressed values)
3993                let mut values = Vec::with_capacity(
3994                    fixed_data.data.len()
3995                        - (self.details.ctrl_word_parser.bytes_per_word() * num_values),
3996                );
3997                let mut visible_items = 0;
3998                for _ in 0..num_values {
3999                    // Extract rep/def
4000                    self.details
4001                        .ctrl_word_parser
4002                        .parse(buf_slice, &mut rep, &mut def);
4003                    buf_slice = &buf_slice[self.details.ctrl_word_parser.bytes_per_word()..];
4004
4005                    let is_visible = def
4006                        .last()
4007                        .map(|d| *d <= self.details.max_visible_def)
4008                        .unwrap_or(true);
4009                    if is_visible {
4010                        // Extract value
4011                        values.extend_from_slice(buf_slice[..self.bytes_per_value].as_ref());
4012                        buf_slice = &buf_slice[self.bytes_per_value..];
4013                        visible_items += 1;
4014                    }
4015                }
4016
4017                // Finally, we decompress the values and add them to our output buffer
4018                let values_buf = LanceBuffer::from(values);
4019                let fixed_data = FixedWidthDataBlock {
4020                    bits_per_value: self.bytes_per_value as u64 * 8,
4021                    block_info: BlockInfo::new(),
4022                    data: values_buf,
4023                    num_values: visible_items,
4024                };
4025                let PerValueDecompressor::Fixed(decompressor) = &self.details.value_decompressor
4026                else {
4027                    unreachable!()
4028                };
4029                let decompressed = decompressor.decompress(fixed_data, visible_items)?;
4030                data_builder.append(&decompressed, 0..visible_items)?;
4031            }
4032
4033            let repetition = if rep.is_empty() { None } else { Some(rep) };
4034            let definition = if def.is_empty() { None } else { Some(def) };
4035
4036            let unraveler = RepDefUnraveler::new(
4037                repetition,
4038                definition,
4039                self.details.def_meaning.clone(),
4040                self.num_rows as u64,
4041            );
4042            let data = data_builder.finish();
4043
4044            Ok(DecodedPage {
4045                data,
4046                repdef: unraveler,
4047            })
4048        }
4049    }
4050}
4051
4052#[derive(Debug)]
4053struct StructuralPrimitiveFieldSchedulingJob<'a> {
4054    scheduler: &'a StructuralPrimitiveFieldScheduler,
4055    ranges: Vec<Range<u64>>,
4056    page_idx: usize,
4057    range_idx: usize,
4058    global_row_offset: u64,
4059}
4060
4061impl<'a> StructuralPrimitiveFieldSchedulingJob<'a> {
4062    pub fn new(scheduler: &'a StructuralPrimitiveFieldScheduler, ranges: Vec<Range<u64>>) -> Self {
4063        Self {
4064            scheduler,
4065            ranges,
4066            page_idx: 0,
4067            range_idx: 0,
4068            global_row_offset: 0,
4069        }
4070    }
4071}
4072
4073impl StructuralSchedulingJob for StructuralPrimitiveFieldSchedulingJob<'_> {
4074    fn schedule_next(&mut self, context: &mut SchedulerContext) -> Result<Vec<ScheduledScanLine>> {
4075        if self.range_idx >= self.ranges.len() {
4076            return Ok(Vec::new());
4077        }
4078        // Get our current range
4079        let mut range = self.ranges[self.range_idx].clone();
4080        let priority = range.start;
4081
4082        let mut cur_page = &self.scheduler.page_schedulers[self.page_idx];
4083        trace!(
4084            "Current range is {:?} and current page has {} rows",
4085            range, cur_page.num_rows
4086        );
4087        // Skip entire pages until we have some overlap with our next range
4088        while cur_page.num_rows + self.global_row_offset <= range.start {
4089            self.global_row_offset += cur_page.num_rows;
4090            self.page_idx += 1;
4091            trace!("Skipping entire page of {} rows", cur_page.num_rows);
4092            cur_page = &self.scheduler.page_schedulers[self.page_idx];
4093        }
4094
4095        // Now the cur_page has overlap with range.  Continue looping through ranges
4096        // until we find a range that exceeds the current page
4097
4098        let mut ranges_in_page = Vec::new();
4099        while cur_page.num_rows + self.global_row_offset > range.start {
4100            range.start = range.start.max(self.global_row_offset);
4101            let start_in_page = range.start - self.global_row_offset;
4102            let end_in_page = start_in_page + (range.end - range.start);
4103            let end_in_page = end_in_page.min(cur_page.num_rows);
4104            let last_in_range = (end_in_page + self.global_row_offset) >= range.end;
4105
4106            ranges_in_page.push(start_in_page..end_in_page);
4107            if last_in_range {
4108                self.range_idx += 1;
4109                if self.range_idx == self.ranges.len() {
4110                    break;
4111                }
4112                range = self.ranges[self.range_idx].clone();
4113            } else {
4114                break;
4115            }
4116        }
4117
4118        trace!(
4119            "Scheduling {} rows across {} ranges from page with {} rows (priority={}, column_index={}, page_index={})",
4120            ranges_in_page.iter().map(|r| r.end - r.start).sum::<u64>(),
4121            ranges_in_page.len(),
4122            cur_page.num_rows,
4123            priority,
4124            self.scheduler.column_index,
4125            cur_page.page_index,
4126        );
4127
4128        self.global_row_offset += cur_page.num_rows;
4129        self.page_idx += 1;
4130
4131        let page_decoders = cur_page
4132            .scheduler
4133            .schedule_ranges(&ranges_in_page, context.io())?;
4134
4135        let cur_path = context.current_path();
4136        page_decoders
4137            .into_iter()
4138            .map(|page_load_task| {
4139                let cur_path = cur_path.clone();
4140                let page_decoder = page_load_task.decoder_fut;
4141                let unloaded_page = async move {
4142                    let page_decoder = page_decoder.await?;
4143                    Ok(LoadedPageShard {
4144                        decoder: page_decoder,
4145                        path: cur_path,
4146                    })
4147                }
4148                .boxed();
4149                Ok(ScheduledScanLine {
4150                    decoders: vec![MessageType::UnloadedPage(UnloadedPageShard(unloaded_page))],
4151                    rows_scheduled: page_load_task.num_rows,
4152                })
4153            })
4154            .collect::<Result<Vec<_>>>()
4155    }
4156}
4157
4158#[derive(Debug)]
4159struct PageInfoAndScheduler {
4160    page_index: usize,
4161    num_rows: u64,
4162    scheduler: Box<dyn StructuralPageScheduler>,
4163}
4164
4165/// A scheduler for a leaf node
4166///
4167/// Here we look at the layout of the various pages and delegate scheduling to a scheduler
4168/// appropriate for the layout of the page.
4169#[derive(Debug)]
4170pub struct StructuralPrimitiveFieldScheduler {
4171    page_schedulers: Vec<PageInfoAndScheduler>,
4172    column_index: u32,
4173    // Identifies the requested decode shape (e.g. blob descriptor struct vs
4174    // raw bytes). Blob columns can produce multiple page scheduler variants
4175    // for the same physical column depending on the target field's data type,
4176    // and the cached page state types differ per variant. The view tag is
4177    // mixed into the cache key so different variants do not collide.
4178    view_tag: String,
4179}
4180
4181impl StructuralPrimitiveFieldScheduler {
4182    pub fn try_new(
4183        column_info: &ColumnInfo,
4184        decompressors: &dyn DecompressionStrategy,
4185        cache_repetition_index: bool,
4186        target_field: &Field,
4187    ) -> Result<Self> {
4188        let page_schedulers = column_info
4189            .page_infos
4190            .iter()
4191            .enumerate()
4192            .map(|(page_index, page_info)| {
4193                Self::page_info_to_scheduler(
4194                    page_info,
4195                    page_index,
4196                    decompressors,
4197                    cache_repetition_index,
4198                    target_field,
4199                )
4200            })
4201            .collect::<Result<Vec<_>>>()?;
4202        Ok(Self {
4203            page_schedulers,
4204            column_index: column_info.index,
4205            view_tag: format!("{:?}", target_field.data_type()),
4206        })
4207    }
4208
4209    fn page_layout_to_scheduler(
4210        page_info: &PageInfo,
4211        page_layout: &PageLayout,
4212        decompressors: &dyn DecompressionStrategy,
4213        cache_repetition_index: bool,
4214        target_field: &Field,
4215    ) -> Result<Box<dyn StructuralPageScheduler>> {
4216        use pb21::page_layout::Layout;
4217        Ok(match page_layout.layout.as_ref().expect_ok()? {
4218            Layout::MiniBlockLayout(mini_block) => Box::new(MiniBlockScheduler::try_new(
4219                &page_info.buffer_offsets_and_sizes,
4220                page_info.priority,
4221                mini_block.num_items,
4222                mini_block,
4223                decompressors,
4224            )?),
4225            Layout::SparseLayout(sparse_layout) => {
4226                Box::new(sparse::SparseStructuralScheduler::try_new(
4227                    &page_info.buffer_offsets_and_sizes,
4228                    page_info.priority,
4229                    page_info.num_rows,
4230                    target_field.data_type(),
4231                    sparse_layout,
4232                    decompressors,
4233                )?)
4234            }
4235            Layout::FullZipLayout(full_zip) => {
4236                let mut scheduler = FullZipScheduler::try_new(
4237                    &page_info.buffer_offsets_and_sizes,
4238                    page_info.priority,
4239                    page_info.num_rows,
4240                    full_zip,
4241                    decompressors,
4242                )?;
4243                scheduler.enable_cache = cache_repetition_index;
4244                Box::new(scheduler)
4245            }
4246            Layout::ConstantLayout(constant_layout) => {
4247                let def_meaning = constant_layout
4248                    .layers
4249                    .iter()
4250                    .map(|l| ProtobufUtils21::repdef_layer_to_def_interp(*l))
4251                    .collect::<Vec<_>>();
4252                let has_scalar_value = constant_layout.inline_value.is_some()
4253                    || page_info.buffer_offsets_and_sizes.len() == 1
4254                    || page_info.buffer_offsets_and_sizes.len() == 3;
4255                if has_scalar_value {
4256                    Box::new(constant::ConstantPageScheduler::try_new(
4257                        page_info.buffer_offsets_and_sizes.clone(),
4258                        constant_layout.inline_value.clone(),
4259                        target_field.data_type(),
4260                        def_meaning.into(),
4261                    )?) as Box<dyn StructuralPageScheduler>
4262                } else if def_meaning.len() == 1
4263                    && def_meaning[0] == DefinitionInterpretation::NullableItem
4264                {
4265                    Box::new(SimpleAllNullScheduler::default()) as Box<dyn StructuralPageScheduler>
4266                } else {
4267                    // RLE levels select a validated cache representation; other
4268                    // block compressions keep flowing through the eager decompressor.
4269                    let rep_codec = LevelCodec::try_new(
4270                        constant_layout.rep_compression.as_ref(),
4271                        decompressors,
4272                    )?;
4273                    let def_codec = LevelCodec::try_new(
4274                        constant_layout.def_compression.as_ref(),
4275                        decompressors,
4276                    )?;
4277
4278                    Box::new(ComplexAllNullScheduler::new(
4279                        page_info.buffer_offsets_and_sizes.clone(),
4280                        def_meaning.into(),
4281                        rep_codec,
4282                        def_codec,
4283                        constant_layout.num_rep_values,
4284                        constant_layout.num_def_values,
4285                    )) as Box<dyn StructuralPageScheduler>
4286                }
4287            }
4288            Layout::BlobLayout(blob) => {
4289                let inner_scheduler = Self::page_layout_to_scheduler(
4290                    page_info,
4291                    blob.inner_layout.as_ref().expect_ok()?.as_ref(),
4292                    decompressors,
4293                    cache_repetition_index,
4294                    target_field,
4295                )?;
4296                let def_meaning = blob
4297                    .layers
4298                    .iter()
4299                    .map(|l| ProtobufUtils21::repdef_layer_to_def_interp(*l))
4300                    .collect::<Vec<_>>();
4301                if matches!(target_field.data_type(), DataType::Struct(_)) {
4302                    // User wants to decode blob into struct
4303                    Box::new(BlobDescriptionPageScheduler::new(
4304                        inner_scheduler,
4305                        def_meaning.into(),
4306                    ))
4307                } else {
4308                    // User wants to decode blob into binary data
4309                    Box::new(BlobPageScheduler::new(
4310                        inner_scheduler,
4311                        page_info.priority,
4312                        page_info.num_rows,
4313                        def_meaning.into(),
4314                    ))
4315                }
4316            }
4317        })
4318    }
4319
4320    fn page_info_to_scheduler(
4321        page_info: &PageInfo,
4322        page_index: usize,
4323        decompressors: &dyn DecompressionStrategy,
4324        cache_repetition_index: bool,
4325        target_field: &Field,
4326    ) -> Result<PageInfoAndScheduler> {
4327        let page_layout = page_info.encoding.as_structural();
4328        let scheduler = Self::page_layout_to_scheduler(
4329            page_info,
4330            page_layout,
4331            decompressors,
4332            cache_repetition_index,
4333            target_field,
4334        )?;
4335        Ok(PageInfoAndScheduler {
4336            page_index,
4337            num_rows: page_info.num_rows,
4338            scheduler,
4339        })
4340    }
4341}
4342
4343pub trait CachedPageData: Any + Send + Sync + DeepSizeOf + 'static {
4344    fn as_arc_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync + 'static>;
4345}
4346
4347pub struct NoCachedPageData;
4348
4349impl DeepSizeOf for NoCachedPageData {
4350    fn deep_size_of_children(&self, _ctx: &mut Context) -> usize {
4351        0
4352    }
4353}
4354impl CachedPageData for NoCachedPageData {
4355    fn as_arc_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync + 'static> {
4356        self
4357    }
4358}
4359
4360pub struct CachedFieldData {
4361    pages: Vec<Arc<dyn CachedPageData>>,
4362}
4363
4364impl DeepSizeOf for CachedFieldData {
4365    fn deep_size_of_children(&self, ctx: &mut Context) -> usize {
4366        self.pages.deep_size_of_children(ctx)
4367    }
4368}
4369
4370// Cache key for field data
4371//
4372// Both `column_index` and `view_tag` are part of the key because a single
4373// physical column can be decoded under more than one shape — a blob column,
4374// for instance, materializes as a `Struct<position, size>` descriptor in one
4375// scheduler variant and as the raw `LargeBinary` bytes in another. Each
4376// variant builds different `CachedPageData` types per page, so two readers
4377// that hit the same `column_index` with different shapes used to collide and
4378// crash with a downcast failure when loading cached state.
4379#[derive(Debug, Clone)]
4380pub struct FieldDataCacheKey {
4381    pub column_index: u32,
4382    pub view_tag: String,
4383}
4384
4385impl CacheKey for FieldDataCacheKey {
4386    type ValueType = CachedFieldData;
4387
4388    fn key(&self) -> std::borrow::Cow<'_, str> {
4389        format!("{}:{}", self.column_index, self.view_tag).into()
4390    }
4391
4392    fn type_name() -> &'static str {
4393        "FieldData"
4394    }
4395
4396    fn schema() -> CacheKeySchema {
4397        CacheKeySchema::new("lance.encoding.logical.primitive.field-data-key", 1)
4398    }
4399
4400    fn write_key(&self, builder: &mut KeyBuilder) {
4401        builder.write_u32(self.column_index);
4402        builder.write_str(&self.view_tag);
4403    }
4404}
4405
4406impl StructuralFieldScheduler for StructuralPrimitiveFieldScheduler {
4407    fn initialize<'a>(
4408        &'a mut self,
4409        _filter: &'a FilterExpression,
4410        context: &'a SchedulerContext,
4411    ) -> BoxFuture<'a, Result<()>> {
4412        let cache_key = FieldDataCacheKey {
4413            column_index: self.column_index,
4414            view_tag: self.view_tag.clone(),
4415        };
4416        let cache = context.cache().clone();
4417
4418        async move {
4419            if let Some(cached_data) = cache.get_with_key(&cache_key).await {
4420                self.page_schedulers
4421                    .iter_mut()
4422                    .zip(cached_data.pages.iter())
4423                    .for_each(|(page_scheduler, cached_data)| {
4424                        page_scheduler.scheduler.load(cached_data);
4425                    });
4426                return Ok(());
4427            }
4428
4429            let page_data = self
4430                .page_schedulers
4431                .iter_mut()
4432                .map(|s| s.scheduler.initialize(context.io()))
4433                .collect::<FuturesOrdered<_>>();
4434
4435            let page_data = page_data.try_collect::<Vec<_>>().await?;
4436            let cached_data = Arc::new(CachedFieldData { pages: page_data });
4437            cache.insert_with_key(&cache_key, cached_data).await;
4438            Ok(())
4439        }
4440        .boxed()
4441    }
4442
4443    fn schedule_ranges<'a>(
4444        &'a self,
4445        ranges: &[Range<u64>],
4446        _filter: &FilterExpression,
4447    ) -> Result<Box<dyn StructuralSchedulingJob + 'a>> {
4448        let ranges = ranges.to_vec();
4449        Ok(Box::new(StructuralPrimitiveFieldSchedulingJob::new(
4450            self, ranges,
4451        )))
4452    }
4453}
4454
4455/// Takes the output from several pages decoders and
4456/// concatenates them.
4457#[derive(Debug)]
4458pub struct StructuralCompositeDecodeArrayTask {
4459    tasks: Vec<Box<dyn DecodePageTask>>,
4460    should_validate: bool,
4461    data_type: DataType,
4462}
4463
4464impl StructuralCompositeDecodeArrayTask {
4465    fn restore_validity(
4466        array: Arc<dyn Array>,
4467        unraveler: &mut CompositeRepDefUnraveler,
4468    ) -> Result<Arc<dyn Array>> {
4469        let validity = unraveler.unravel_validity(array.len())?;
4470        let Some(validity) = validity else {
4471            return Ok(array);
4472        };
4473        if array.data_type() == &DataType::Null {
4474            // We unravel from a null array but we don't add the null buffer because arrow-rs doesn't like it
4475            return Ok(array);
4476        }
4477        if validity.len() != array.len() {
4478            return Err(Error::invalid_input_source(
4479                format!(
4480                    "Structural validity has {} entries for an array with {} values",
4481                    validity.len(),
4482                    array.len()
4483                )
4484                .into(),
4485            ));
4486        }
4487        // SAFETY: The array buffers have already been validated and the null buffer length
4488        // matches the array. We are only attaching the null buffer here.
4489        Ok(make_array(unsafe {
4490            array
4491                .to_data()
4492                .into_builder()
4493                .nulls(Some(validity))
4494                .build_unchecked()
4495        }))
4496    }
4497}
4498
4499impl StructuralDecodeArrayTask for StructuralCompositeDecodeArrayTask {
4500    fn decode(self: Box<Self>) -> Result<DecodedArray> {
4501        let mut arrays = Vec::with_capacity(self.tasks.len());
4502        let mut unravelers = Vec::with_capacity(self.tasks.len());
4503        let mut data_size = 0u64;
4504        for task in self.tasks {
4505            let decoded = task.decode()?;
4506            data_size += decoded.data.data_size();
4507            unravelers.push(decoded.repdef);
4508
4509            let array = make_array(
4510                decoded
4511                    .data
4512                    .into_arrow(self.data_type.clone(), self.should_validate)?,
4513            );
4514
4515            arrays.push(array);
4516        }
4517        let array_refs = arrays.iter().map(|arr| arr.as_ref()).collect::<Vec<_>>();
4518        let array = arrow_select::concat::concat(&array_refs)?;
4519        let mut repdef = CompositeRepDefUnraveler::new(unravelers);
4520
4521        let array = Self::restore_validity(array, &mut repdef)?;
4522
4523        Ok(DecodedArray {
4524            array,
4525            repdef,
4526            data_size,
4527        })
4528    }
4529}
4530
4531#[derive(Debug)]
4532pub struct StructuralPrimitiveFieldDecoder {
4533    field: Arc<ArrowField>,
4534    page_decoders: VecDeque<Box<dyn StructuralPageDecoder>>,
4535    should_validate: bool,
4536    rows_drained_in_current: u64,
4537}
4538
4539impl StructuralPrimitiveFieldDecoder {
4540    pub fn new(field: &Arc<ArrowField>, should_validate: bool) -> Self {
4541        Self {
4542            field: field.clone(),
4543            page_decoders: VecDeque::new(),
4544            should_validate,
4545            rows_drained_in_current: 0,
4546        }
4547    }
4548}
4549
4550impl StructuralFieldDecoder for StructuralPrimitiveFieldDecoder {
4551    fn accept_page(&mut self, child: LoadedPageShard) -> Result<()> {
4552        assert!(child.path.is_empty());
4553        self.page_decoders.push_back(child.decoder);
4554        Ok(())
4555    }
4556
4557    fn drain(&mut self, num_rows: u64) -> Result<Box<dyn StructuralDecodeArrayTask>> {
4558        let mut remaining = num_rows;
4559        let mut tasks = Vec::new();
4560        while remaining > 0 {
4561            let queued_pages = self.page_decoders.len();
4562            let Some(cur_page) = self.page_decoders.front_mut() else {
4563                return Err(Error::internal(format!(
4564                    "Primitive decoder missing page decoder while draining field '{}' (data_type={:?}, requested_rows={}, remaining_rows={}, rows_drained_in_current={}, queued_pages={})",
4565                    self.field.name(),
4566                    self.field.data_type(),
4567                    num_rows,
4568                    remaining,
4569                    self.rows_drained_in_current,
4570                    queued_pages
4571                )));
4572            };
4573            let num_in_page = cur_page.num_rows() - self.rows_drained_in_current;
4574            let to_take = num_in_page.min(remaining);
4575
4576            let task = cur_page.drain(to_take)?;
4577            tasks.push(task);
4578
4579            if to_take == num_in_page {
4580                self.page_decoders.pop_front();
4581                self.rows_drained_in_current = 0;
4582            } else {
4583                self.rows_drained_in_current += to_take;
4584            }
4585
4586            remaining -= to_take;
4587        }
4588        Ok(Box::new(StructuralCompositeDecodeArrayTask {
4589            tasks,
4590            should_validate: self.should_validate,
4591            data_type: self.field.data_type().clone(),
4592        }))
4593    }
4594
4595    fn data_type(&self) -> &DataType {
4596        self.field.data_type()
4597    }
4598}
4599
4600/// The serialized representation of full-zip data
4601struct SerializedFullZip {
4602    /// The zipped values buffer
4603    values: LanceBuffer,
4604    /// The repetition index (only present if there is repetition)
4605    repetition_index: Option<LanceBuffer>,
4606}
4607
4608// We align and pad mini-blocks to 8 byte boundaries for two reasons.  First,
4609// to allow us to store a chunk size in 12 bits.
4610//
4611// If we directly record the size in bytes with 12 bits we would be limited to
4612// 4KiB which is too small.  Since we know each mini-block consists of 8 byte
4613// words we can store the # of words instead which gives us 32KiB.
4614//
4615// Second, each chunk in a mini-block is aligned to 8 bytes.  This allows multi-byte
4616// values like offsets to be stored in a mini-block and safely read back out.  It also
4617// helps ensure zero-copy reads in cases where zero-copy is possible (e.g. no decoding
4618// needed).
4619//
4620// Note: by "aligned to 8 bytes" we mean BOTH "aligned to 8 bytes from the start of
4621// the page" and "aligned to 8 bytes from the start of the file."
4622const MINIBLOCK_ALIGNMENT: usize = 8;
4623
4624/// An encoder for primitive (leaf) arrays
4625///
4626/// This encoder is fairly complicated and follows a number of paths depending
4627/// on the data.
4628///
4629/// First, we convert the validity & offsets information into repetition and
4630/// definition levels.  Then we compress the data itself into a single buffer.
4631///
4632/// If the data is narrow then we encode the data in small chunks (each chunk
4633/// should be a few disk sectors and contains a buffer of repetition, a buffer
4634/// of definition, and a buffer of value data).  This approach is called
4635/// "mini-block".  These mini-blocks are stored into a single data buffer.
4636///
4637/// If the data is wide then we zip together the repetition and definition value
4638/// with the value data into a single buffer.  This approach is called "zipped".
4639///
4640/// If there is any repetition information then we create a repetition index
4641///
4642/// In addition, the compression process may create zero or more metadata buffers.
4643/// For example, a dictionary compression will create dictionary metadata.  Any
4644/// mini-block approach has a metadata buffer of block sizes.  This metadata is
4645/// stored in a separate buffer on disk and read at initialization time.
4646///
4647/// TODO: We should concatenate metadata buffers from all pages into a single buffer
4648/// at (roughly) the end of the file so there is, at most, one read per column of
4649/// metadata per file.
4650#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4651pub(crate) enum MiniblockChunkSize {
4652    U16,
4653    U32,
4654}
4655
4656#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4657enum ComplexNullEncoding {
4658    RawLevels,
4659    CompressedLevels,
4660}
4661
4662#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4663enum FixedWidthDictionaryEncoding {
4664    Exclude64Bit,
4665    Include64Bit,
4666}
4667
4668trait PrimitivePageEncodingBehavior: Send + Sync + Debug {
4669    fn validate_field(&self, _field: &Field, _metadata: &HashMap<String, String>) -> Result<()> {
4670        Ok(())
4671    }
4672
4673    fn try_plan_pages(
4674        &self,
4675        _ctx: &PrimitivePlanContext<'_>,
4676        _arrays: &[ArrayRef],
4677        _normalized: &NormalizedStructuralPlan,
4678        _row_number: u64,
4679        _num_rows: u64,
4680        _num_values: u64,
4681    ) -> Result<Option<Vec<PrimitivePageData>>> {
4682        Ok(None)
4683    }
4684
4685    fn try_encode_page(
4686        &self,
4687        _ctx: &PrimitiveEncodeContext,
4688        page: PrimitivePageData,
4689    ) -> Result<PrimitiveEncodeAttempt> {
4690        Ok(PrimitiveEncodeAttempt::Unhandled(page))
4691    }
4692}
4693
4694/// One executable primitive-page behavior selected by an exact file
4695/// composition.
4696#[derive(Debug, Clone)]
4697pub struct PrimitivePageEncoding {
4698    behavior: Arc<dyn PrimitivePageEncodingBehavior>,
4699}
4700
4701impl PrimitivePageEncoding {
4702    /// Reject an explicit request for sparse structural encoding.
4703    pub fn reject_sparse() -> Self {
4704        Self {
4705            behavior: Arc::new(RejectSparsePrimitiveEncoding),
4706        }
4707    }
4708
4709    /// Encode constant non-null values as a constant page when applicable.
4710    pub fn constant() -> Self {
4711        Self {
4712            behavior: Arc::new(ConstantPrimitiveEncoding),
4713        }
4714    }
4715
4716    /// Plan and encode sparse structural pages when applicable.
4717    pub fn sparse(compression: Arc<dyn CompressionStrategy>) -> Self {
4718        Self {
4719            behavior: Arc::new(SparsePrimitiveEncoding { compression }),
4720        }
4721    }
4722
4723    /// Encode dense pages with the original u16 miniblock grammar.
4724    pub fn dense_u16(compression: Arc<dyn CompressionStrategy>) -> Self {
4725        Self {
4726            behavior: Arc::new(DenseU16PrimitiveEncoding { compression }),
4727        }
4728    }
4729
4730    /// Encode dense pages with the u32 miniblock grammar.
4731    pub fn dense_u32(compression: Arc<dyn CompressionStrategy>) -> Self {
4732        Self {
4733            behavior: Arc::new(DenseU32PrimitiveEncoding { compression }),
4734        }
4735    }
4736}
4737
4738#[derive(Debug)]
4739struct RejectSparsePrimitiveEncoding;
4740
4741#[derive(Debug)]
4742struct ConstantPrimitiveEncoding;
4743
4744#[derive(Debug)]
4745struct SparsePrimitiveEncoding {
4746    compression: Arc<dyn CompressionStrategy>,
4747}
4748
4749#[derive(Debug)]
4750struct DenseU16PrimitiveEncoding {
4751    compression: Arc<dyn CompressionStrategy>,
4752}
4753
4754#[derive(Debug)]
4755struct DenseU32PrimitiveEncoding {
4756    compression: Arc<dyn CompressionStrategy>,
4757}
4758
4759pub struct PrimitiveStructuralEncoder {
4760    // Accumulates arrays until we have enough data to justify a disk page
4761    accumulation_queue: AccumulationQueue,
4762
4763    keep_original_array: bool,
4764    accumulated_repdefs: Vec<RepDefBuilder>,
4765    page_encodings: Arc<[PrimitivePageEncoding]>,
4766    column_index: u32,
4767    field: Field,
4768    encoding_metadata: Arc<HashMap<String, String>>,
4769}
4770
4771struct CompressedLevelsChunk {
4772    data: LanceBuffer,
4773    num_levels: u16,
4774}
4775
4776struct CompressedLevels {
4777    data: Vec<CompressedLevelsChunk>,
4778    compression: CompressiveEncoding,
4779    rep_index: Option<LanceBuffer>,
4780}
4781
4782struct SerializedMiniBlockPage {
4783    num_buffers: u64,
4784    data: LanceBuffer,
4785    metadata: LanceBuffer,
4786}
4787
4788#[derive(Debug, Clone, Copy)]
4789struct DictEncodingBudget {
4790    max_dict_entries: u32,
4791    max_encoded_size: usize,
4792}
4793
4794enum PrimitivePageStructure {
4795    Dense {
4796        repdef: SerializedRepDefs,
4797        single_row_miniblock_repdef_levels: Option<u64>,
4798    },
4799    Sparse {
4800        plan: sparse::SparseStructuralPlan,
4801        prepared_values: Option<sparse::writer::PreparedSparseValues>,
4802    },
4803}
4804
4805// A primitive page after structural encoding selection and optional dense splitting.
4806struct PrimitivePageData {
4807    // Arrow leaf arrays that contain this page's visible values.
4808    arrays: Vec<ArrayRef>,
4809    // Structural representation aligned to this page.
4810    structure: PrimitivePageStructure,
4811    // Top-level row number of the first row in this page.
4812    row_number: u64,
4813    // Number of top-level rows in this page.
4814    num_rows: u64,
4815}
4816
4817struct PrimitivePlanContext<'a> {
4818    column_idx: u32,
4819    field: &'a Field,
4820    encoding_metadata: &'a HashMap<String, String>,
4821}
4822
4823enum PrimitiveEncodeAttempt {
4824    Encoded(EncodedPage),
4825    Unhandled(PrimitivePageData),
4826}
4827
4828// Immutable encoder state shared by per-page encode tasks.
4829//
4830// Cloning this only clones Arc-backed configuration and field metadata.  Page data
4831// stays in PrimitivePageData and is moved into exactly one task.
4832#[derive(Clone)]
4833struct PrimitiveEncodeContext {
4834    // Column being encoded.
4835    column_idx: u32,
4836    field: Field,
4837    encoding_metadata: Arc<HashMap<String, String>>,
4838    is_simple_validity: bool,
4839    has_repdef_info: bool,
4840}
4841
4842impl PrimitiveStructuralEncoder {
4843    pub fn try_new(
4844        options: &EncodingOptions,
4845        page_encodings: Arc<[PrimitivePageEncoding]>,
4846        column_index: u32,
4847        field: Field,
4848        encoding_metadata: Arc<HashMap<String, String>>,
4849    ) -> Result<Self> {
4850        for page_encoding in page_encodings.iter() {
4851            page_encoding
4852                .behavior
4853                .validate_field(&field, &encoding_metadata)?;
4854        }
4855        Ok(Self {
4856            accumulation_queue: AccumulationQueue::new(
4857                options.cache_bytes_per_column,
4858                column_index,
4859                options.keep_original_array,
4860            ),
4861            keep_original_array: options.keep_original_array,
4862            accumulated_repdefs: Vec::new(),
4863            column_index,
4864            page_encodings,
4865            field,
4866            encoding_metadata,
4867        })
4868    }
4869
4870    fn encode_page(
4871        page_encodings: &[PrimitivePageEncoding],
4872        ctx: &PrimitiveEncodeContext,
4873        mut page: PrimitivePageData,
4874    ) -> Result<EncodedPage> {
4875        for page_encoding in page_encodings {
4876            match page_encoding.behavior.try_encode_page(ctx, page)? {
4877                PrimitiveEncodeAttempt::Encoded(page) => return Ok(page),
4878                PrimitiveEncodeAttempt::Unhandled(unhandled) => page = unhandled,
4879            }
4880        }
4881        Err(Error::invalid_input_source(
4882            format!(
4883                "No primitive page encoding atom supports field '{}'",
4884                ctx.field.name
4885            )
4886            .into(),
4887        ))
4888    }
4889
4890    // TODO: This is a heuristic we may need to tune at some point
4891    //
4892    // As data gets narrow then the "zipping" process gets too expensive
4893    //   and we prefer mini-block
4894    // As data gets wide then the # of values per block shrinks (very wide)
4895    //   data doesn't even fit in a mini-block and the block overhead gets
4896    //   too large and we prefer zipped.
4897    fn is_narrow(data_block: &DataBlock) -> bool {
4898        const MINIBLOCK_MAX_BYTE_LENGTH_PER_VALUE: u64 = 256;
4899
4900        if let Some(max_len_array) = data_block.get_stat(Stat::MaxLength) {
4901            let max_len_array = max_len_array
4902                .as_any()
4903                .downcast_ref::<PrimitiveArray<UInt64Type>>()
4904                .unwrap();
4905            if max_len_array.value(0) < MINIBLOCK_MAX_BYTE_LENGTH_PER_VALUE {
4906                return true;
4907            }
4908        }
4909        false
4910    }
4911
4912    fn prefers_miniblock(
4913        data_block: &DataBlock,
4914        encoding_metadata: &HashMap<String, String>,
4915    ) -> bool {
4916        // If the user specifically requested miniblock then use it
4917        if let Some(user_requested) = encoding_metadata.get(STRUCTURAL_ENCODING_META_KEY) {
4918            return user_requested.to_lowercase() == STRUCTURAL_ENCODING_MINIBLOCK;
4919        }
4920        // Otherwise only use miniblock if it is narrow
4921        Self::is_narrow(data_block)
4922    }
4923
4924    fn prefers_fullzip(encoding_metadata: &HashMap<String, String>) -> bool {
4925        // Fullzip is the backup option so the only reason we wouldn't use it is if the
4926        // user specifically requested not to use it (in which case we're probably going
4927        // to emit an error)
4928        if let Some(user_requested) = encoding_metadata.get(STRUCTURAL_ENCODING_META_KEY) {
4929            return user_requested.to_lowercase() == STRUCTURAL_ENCODING_FULLZIP;
4930        }
4931        true
4932    }
4933
4934    // Converts value data, repetition levels, and definition levels into a single
4935    // buffer of mini-blocks.  In addition, creates a buffer of mini-block metadata
4936    // which tells us the size of each block.  Finally, if repetition is present then
4937    // we also create a buffer for the repetition index.
4938    //
4939    // Each chunk is serialized as:
4940    // | num_bufs (1 byte) | buf_lens (2 bytes per buffer) | P | buf0 | P | buf1 | ... | bufN | P |
4941    //
4942    // P - Padding inserted to ensure each buffer is 8-byte aligned and the buffer size is a multiple
4943    //     of 8 bytes (so that the next chunk is 8-byte aligned).
4944    //
4945    // Each block has a u16 word of metadata.  The upper 12 bits contain the
4946    // # of 8-byte words in the block (if the block does not fill the final word
4947    // then up to 7 bytes of padding are added).  The lower 4 bits describe the log_2
4948    // number of values (e.g. if there are 1024 then the lower 4 bits will be
4949    // 0xA)  All blocks except the last must have power-of-two number of values.
4950    // This not only makes metadata smaller but it makes decoding easier since
4951    // batch sizes are typically a power of 2.  4 bits would allow us to express
4952    // up to 32Ki values.
4953    //
4954    // This means blocks can have 1 to 32Ki values and 8 - 32Ki bytes.
4955    //
4956    // All metadata words are serialized (as little endian) into a single buffer
4957    // of metadata values.
4958    //
4959    // If there is repetition then we also create a repetition index.  This is a
4960    // single buffer of integer vectors (stored in row major order).  There is one
4961    // entry for each chunk.  The size of the vector is based on the depth of random
4962    // access we want to support.
4963    //
4964    // A vector of size 2 is the minimum and will support row-based random access (e.g.
4965    // "take the 57th row").  A vector of size 3 will support 1 level of nested access
4966    // (e.g. "take the 3rd item in the 57th row").  A vector of size 4 will support 2
4967    // levels of nested access and so on.
4968    //
4969    // The first number in the vector is the number of top-level rows that complete in
4970    // the chunk.  The second number is the number of second-level rows that complete
4971    // after the final top-level row completed (or beginning of the chunk if no top-level
4972    // row completes in the chunk).  And so on.  The final number in the vector is always
4973    // the number of leftover items not covered by earlier entries in the vector.
4974    //
4975    // Currently we are limited to 0 levels of nested access but that will change in the
4976    // future.
4977    //
4978    // The repetition index and the chunk metadata are read at initialization time and
4979    // cached in memory.
4980    fn serialize_miniblocks(
4981        miniblocks: MiniBlockCompressed,
4982        rep: Option<Vec<CompressedLevelsChunk>>,
4983        def: Option<Vec<CompressedLevelsChunk>>,
4984        miniblock_chunk_size: MiniblockChunkSize,
4985    ) -> Result<SerializedMiniBlockPage> {
4986        let bytes_rep = rep
4987            .as_ref()
4988            .map(|rep| rep.iter().map(|r| r.data.len()).sum::<usize>())
4989            .unwrap_or(0);
4990        let bytes_def = def
4991            .as_ref()
4992            .map(|def| def.iter().map(|d| d.data.len()).sum::<usize>())
4993            .unwrap_or(0);
4994        let bytes_data = miniblocks.data.iter().map(|d| d.len()).sum::<usize>();
4995        let mut num_buffers = miniblocks.data.len();
4996        if rep.is_some() {
4997            num_buffers += 1;
4998        }
4999        if def.is_some() {
5000            num_buffers += 1;
5001        }
5002        // 2 bytes for the length of each buffer and up to 7 bytes of padding per buffer
5003        let max_extra = 9 * num_buffers;
5004        let mut data_buffer = Vec::with_capacity(bytes_rep + bytes_def + bytes_data + max_extra);
5005        let chunk_size_bytes = match miniblock_chunk_size {
5006            MiniblockChunkSize::U16 => 2,
5007            MiniblockChunkSize::U32 => 4,
5008        };
5009        let mut meta_buffer = Vec::with_capacity(miniblocks.chunks.len() * chunk_size_bytes);
5010
5011        let mut rep_iter = rep.map(|r| r.into_iter());
5012        let mut def_iter = def.map(|d| d.into_iter());
5013
5014        let mut buffer_offsets = vec![0; miniblocks.data.len()];
5015        for chunk in miniblocks.chunks {
5016            let start_pos = data_buffer.len();
5017            // Start of chunk should be aligned
5018            debug_assert_eq!(start_pos % MINIBLOCK_ALIGNMENT, 0);
5019
5020            let rep = rep_iter.as_mut().map(|r| r.next().unwrap());
5021            let def = def_iter.as_mut().map(|d| d.next().unwrap());
5022
5023            // Write the number of levels, or 0 if there is no rep/def
5024            let num_levels = rep
5025                .as_ref()
5026                .map(|r| r.num_levels)
5027                .unwrap_or(def.as_ref().map(|d| d.num_levels).unwrap_or(0));
5028            data_buffer.extend_from_slice(&num_levels.to_le_bytes());
5029
5030            // Write the buffer lengths
5031            if let Some(rep) = rep.as_ref() {
5032                let bytes_rep = u16::try_from(rep.data.len()).map_err(|_| {
5033                    Error::internal(format!(
5034                        "Repetition buffer size ({} bytes) too large",
5035                        rep.data.len()
5036                    ))
5037                })?;
5038                data_buffer.extend_from_slice(&bytes_rep.to_le_bytes());
5039            }
5040            if let Some(def) = def.as_ref() {
5041                let bytes_def = u16::try_from(def.data.len()).map_err(|_| {
5042                    Error::internal(format!(
5043                        "Definition buffer size ({} bytes) too large",
5044                        def.data.len()
5045                    ))
5046                })?;
5047                data_buffer.extend_from_slice(&bytes_def.to_le_bytes());
5048            }
5049
5050            if miniblock_chunk_size == MiniblockChunkSize::U32 {
5051                for &buffer_size in &chunk.buffer_sizes {
5052                    data_buffer.extend_from_slice(&buffer_size.to_le_bytes());
5053                }
5054            } else {
5055                for &buffer_size in &chunk.buffer_sizes {
5056                    let buffer_size = u16::try_from(buffer_size).map_err(|_| {
5057                        Error::internal(format!(
5058                            "Mini-block buffer size ({} bytes) too large for 16-bit metadata",
5059                            buffer_size
5060                        ))
5061                    })?;
5062                    data_buffer.extend_from_slice(&buffer_size.to_le_bytes());
5063                }
5064            }
5065
5066            // Pad
5067            let add_padding = |data_buffer: &mut Vec<u8>| {
5068                let pad = pad_bytes::<MINIBLOCK_ALIGNMENT>(data_buffer.len());
5069                data_buffer.extend(iter::repeat_n(FILL_BYTE, pad));
5070            };
5071            add_padding(&mut data_buffer);
5072
5073            // Write the buffers themselves
5074            if let Some(rep) = rep.as_ref() {
5075                data_buffer.extend_from_slice(&rep.data);
5076                add_padding(&mut data_buffer);
5077            }
5078            if let Some(def) = def.as_ref() {
5079                data_buffer.extend_from_slice(&def.data);
5080                add_padding(&mut data_buffer);
5081            }
5082            for (buffer_size, (buffer, buffer_offset)) in chunk
5083                .buffer_sizes
5084                .iter()
5085                .zip(miniblocks.data.iter().zip(buffer_offsets.iter_mut()))
5086            {
5087                let start = *buffer_offset;
5088                let end = start + *buffer_size as usize;
5089                *buffer_offset += *buffer_size as usize;
5090                data_buffer.extend_from_slice(&buffer[start..end]);
5091                add_padding(&mut data_buffer);
5092            }
5093
5094            let chunk_bytes = data_buffer.len() - start_pos;
5095            let max_chunk_size = match miniblock_chunk_size {
5096                MiniblockChunkSize::U16 => 32 * 1024,
5097                MiniblockChunkSize::U32 => 1_u64 << 31,
5098            };
5099            if chunk_bytes == 0 || chunk_bytes as u64 > max_chunk_size {
5100                return Err(Error::internal(format!(
5101                    "Mini-block chunk size {} bytes exceeds the {} byte metadata limit",
5102                    chunk_bytes, max_chunk_size
5103                )));
5104            }
5105            if chunk_bytes % MINIBLOCK_ALIGNMENT != 0 {
5106                return Err(Error::internal(format!(
5107                    "Mini-block chunk size {} bytes is not aligned to {} bytes",
5108                    chunk_bytes, MINIBLOCK_ALIGNMENT
5109                )));
5110            }
5111            if chunk.log_num_values > 15 {
5112                return Err(Error::internal(format!(
5113                    "Mini-block log_num_values {} exceeds the 4-bit metadata limit",
5114                    chunk.log_num_values
5115                )));
5116            }
5117            // We subtract 1 here from chunk_bytes because we want to be able to express
5118            // a size of 32KiB and not (32Ki - 8)B which is what we'd get otherwise with
5119            // 0xFFF
5120            let divided_bytes = chunk_bytes / MINIBLOCK_ALIGNMENT;
5121            let divided_bytes_minus_one = (divided_bytes - 1) as u64;
5122
5123            let metadata = (divided_bytes_minus_one << 4) | chunk.log_num_values as u64;
5124            if miniblock_chunk_size == MiniblockChunkSize::U32 {
5125                meta_buffer.extend_from_slice(&(metadata as u32).to_le_bytes());
5126            } else {
5127                meta_buffer.extend_from_slice(&(metadata as u16).to_le_bytes());
5128            }
5129        }
5130
5131        let data_buffer = LanceBuffer::from(data_buffer);
5132        let metadata_buffer = LanceBuffer::from(meta_buffer);
5133
5134        Ok(SerializedMiniBlockPage {
5135            num_buffers: miniblocks.data.len() as u64,
5136            data: data_buffer,
5137            metadata: metadata_buffer,
5138        })
5139    }
5140
5141    /// Compresses a buffer of levels into chunks
5142    ///
5143    /// If these are repetition levels then we also calculate the repetition index here (that
5144    /// is the third return value)
5145    fn compress_levels(
5146        mut levels: RepDefSlicer<'_>,
5147        num_elements: u64,
5148        compression_strategy: &dyn CompressionStrategy,
5149        chunks: &[MiniBlockChunk],
5150        // This will be 0 if we are compressing def levels
5151        max_rep: u16,
5152    ) -> Result<CompressedLevels> {
5153        let mut rep_index = if max_rep > 0 {
5154            Vec::with_capacity(chunks.len())
5155        } else {
5156            vec![]
5157        };
5158        // Make the levels into a FixedWidth data block
5159        let num_levels = levels.num_levels() as u64;
5160        let levels_buf = levels.all_levels().clone();
5161
5162        let mut fixed_width_block = FixedWidthDataBlock {
5163            data: levels_buf,
5164            bits_per_value: 16,
5165            num_values: num_levels,
5166            block_info: BlockInfo::new(),
5167        };
5168        // Compute statistics to enable optimal compression for rep/def levels
5169        fixed_width_block.compute_stat();
5170
5171        let levels_block = DataBlock::FixedWidth(fixed_width_block);
5172        let levels_field = Field::new_arrow("", DataType::UInt16, false)?;
5173        // Pick a block compressor
5174        let compressor =
5175            compression_strategy.create_block_compressor(&levels_field, &levels_block)?;
5176        let mut compressor_desc = None;
5177        // Compress blocks of levels (sized according to the chunks)
5178        let mut level_chunks = Vec::with_capacity(chunks.len());
5179        let mut values_counter = 0;
5180        for (chunk_idx, chunk) in chunks.iter().enumerate() {
5181            let chunk_num_values = chunk.num_values(values_counter, num_elements);
5182            debug_assert!(chunk_num_values > 0);
5183            values_counter += chunk_num_values;
5184            let chunk_levels = if chunk_idx < chunks.len() - 1 {
5185                levels.slice_next(chunk_num_values as usize)
5186            } else {
5187                levels.slice_rest()
5188            };
5189            let num_chunk_levels = (chunk_levels.len() / 2) as u64;
5190            if max_rep > 0 {
5191                // If max_rep > 0 then we are working with rep levels and we need
5192                // to calculate the repetition index.  The repetition index for a
5193                // chunk is currently 2 values (in the future it may be more).
5194                //
5195                // The first value is the number of rows that _finish_ in the
5196                // chunk.
5197                //
5198                // The second value is the number of "leftovers" after the last
5199                // finished row in the chunk.
5200                let rep_values = chunk_levels.borrow_to_typed_slice::<u16>();
5201                let rep_values = rep_values.as_ref();
5202
5203                // We skip 1 here because a max_rep at spot 0 doesn't count as a finished list (we
5204                // will count it in the previous chunk)
5205                let mut num_rows = rep_values.iter().skip(1).filter(|v| **v == max_rep).count();
5206                let num_leftovers = if chunk_idx < chunks.len() - 1 {
5207                    rep_values
5208                        .iter()
5209                        .rev()
5210                        .position(|v| *v == max_rep)
5211                        // # of leftovers includes the max_rep spot
5212                        .map(|pos| pos + 1)
5213                        .unwrap_or(rep_values.len())
5214                } else {
5215                    // Last chunk can't have leftovers
5216                    0
5217                };
5218
5219                if chunk_idx != 0 && rep_values.first() == Some(&max_rep) {
5220                    // This chunk starts with a new row and so, if we thought we had leftovers
5221                    // in the previous chunk, we were mistaken
5222                    // TODO: Can use unchecked here
5223                    let rep_len = rep_index.len();
5224                    if rep_index[rep_len - 1] != 0 {
5225                        // We thought we had leftovers but that was actually a full row
5226                        rep_index[rep_len - 2] += 1;
5227                        rep_index[rep_len - 1] = 0;
5228                    }
5229                }
5230
5231                if chunk_idx == chunks.len() - 1 {
5232                    // The final list
5233                    num_rows += 1;
5234                }
5235                rep_index.push(num_rows as u64);
5236                rep_index.push(num_leftovers as u64);
5237            }
5238            let mut chunk_fixed_width = FixedWidthDataBlock {
5239                data: chunk_levels,
5240                bits_per_value: 16,
5241                num_values: num_chunk_levels,
5242                block_info: BlockInfo::new(),
5243            };
5244            chunk_fixed_width.compute_stat();
5245            let chunk_levels_block = DataBlock::FixedWidth(chunk_fixed_width);
5246            let (compressed_levels, chunk_compressor_desc) =
5247                compressor.compress(chunk_levels_block)?;
5248            if let Some(compressor_desc) = compressor_desc.as_ref() {
5249                if compressor_desc != &chunk_compressor_desc {
5250                    return Err(Error::internal(
5251                        "Rep/def block compressor changed encoding between chunks".to_string(),
5252                    ));
5253                }
5254            } else {
5255                compressor_desc = Some(chunk_compressor_desc);
5256            }
5257            let compressed_levels = compressed_levels.ok_or_else(|| {
5258                Error::internal(
5259                    "Rep/def block compressor selected a metadata-only codec".to_string(),
5260                )
5261            })?;
5262            let num_levels = u16::try_from(num_chunk_levels).map_err(|_| {
5263                Error::invalid_input_source(
5264                    format!(
5265                        "Mini-block cannot encode {} rep/def levels in one chunk. \
5266                         This usually means a top-level row contains too much nested structure \
5267                         for the current layout.",
5268                        num_chunk_levels
5269                    )
5270                    .into(),
5271                )
5272            })?;
5273            level_chunks.push(CompressedLevelsChunk {
5274                data: compressed_levels,
5275                num_levels,
5276            });
5277        }
5278        debug_assert_eq!(levels.num_levels_remaining(), 0);
5279        let rep_index = if rep_index.is_empty() {
5280            None
5281        } else {
5282            Some(LanceBuffer::reinterpret_vec(rep_index))
5283        };
5284        Ok(CompressedLevels {
5285            data: level_chunks,
5286            compression: compressor_desc.ok_or_else(|| {
5287                Error::internal("Rep/def compression produced no chunks".to_string())
5288            })?,
5289            rep_index,
5290        })
5291    }
5292
5293    fn encode_simple_all_null(
5294        column_idx: u32,
5295        num_rows: u64,
5296        row_number: u64,
5297    ) -> Result<EncodedPage> {
5298        let description =
5299            ProtobufUtils21::constant_layout(&[DefinitionInterpretation::NullableItem], None);
5300        Ok(EncodedPage {
5301            column_idx,
5302            data: vec![],
5303            description: PageEncoding::Structural(description),
5304            num_rows,
5305            row_number,
5306        })
5307    }
5308
5309    fn encode_complex_all_null_vals(
5310        data: &Arc<[u16]>,
5311        compression_strategy: &dyn CompressionStrategy,
5312    ) -> Result<(LanceBuffer, pb21::CompressiveEncoding)> {
5313        let buffer = LanceBuffer::reinterpret_slice(data.clone());
5314        let mut fixed_width_block = FixedWidthDataBlock {
5315            data: buffer,
5316            bits_per_value: 16,
5317            num_values: data.len() as u64,
5318            block_info: BlockInfo::new(),
5319        };
5320        fixed_width_block.compute_stat();
5321
5322        let levels_block = DataBlock::FixedWidth(fixed_width_block);
5323        let levels_field = Field::new_arrow("", DataType::UInt16, false)?;
5324        let (compressed_buffer, encoding) =
5325            compress_required_block(compression_strategy, &levels_field, levels_block)?;
5326        Ok((compressed_buffer, encoding))
5327    }
5328
5329    // Encodes a page where all values are null but we have rep/def
5330    // information that we need to store (e.g. to distinguish between
5331    // different kinds of null)
5332    fn encode_complex_all_null(
5333        column_idx: u32,
5334        repdef: crate::repdef::SerializedRepDefs,
5335        row_number: u64,
5336        num_rows: u64,
5337        complex_null_encoding: ComplexNullEncoding,
5338        compression_strategy: &dyn CompressionStrategy,
5339    ) -> Result<EncodedPage> {
5340        if complex_null_encoding == ComplexNullEncoding::RawLevels {
5341            let rep_bytes = if let Some(rep) = repdef.repetition_levels.as_ref() {
5342                LanceBuffer::reinterpret_slice(rep.clone())
5343            } else {
5344                LanceBuffer::empty()
5345            };
5346
5347            let def_bytes = if let Some(def) = repdef.definition_levels.as_ref() {
5348                LanceBuffer::reinterpret_slice(def.clone())
5349            } else {
5350                LanceBuffer::empty()
5351            };
5352
5353            let description = ProtobufUtils21::constant_layout(&repdef.def_meaning, None);
5354            return Ok(EncodedPage {
5355                column_idx,
5356                data: vec![rep_bytes, def_bytes],
5357                description: PageEncoding::Structural(description),
5358                num_rows,
5359                row_number,
5360            });
5361        }
5362
5363        let (rep_bytes, rep_encoding, num_rep_values) = if let Some(rep) =
5364            repdef.repetition_levels.as_ref()
5365        {
5366            let num_values = rep.len() as u64;
5367            let (buffer, encoding) = Self::encode_complex_all_null_vals(rep, compression_strategy)?;
5368            (buffer, Some(encoding), num_values)
5369        } else {
5370            (LanceBuffer::empty(), None, 0)
5371        };
5372
5373        let (def_bytes, def_encoding, num_def_values) = if let Some(def) =
5374            repdef.definition_levels.as_ref()
5375        {
5376            let num_values = def.len() as u64;
5377            let (buffer, encoding) = Self::encode_complex_all_null_vals(def, compression_strategy)?;
5378            (buffer, Some(encoding), num_values)
5379        } else {
5380            (LanceBuffer::empty(), None, 0)
5381        };
5382
5383        let description = ProtobufUtils21::compressed_all_null_constant_layout(
5384            &repdef.def_meaning,
5385            rep_encoding,
5386            def_encoding,
5387            num_rep_values,
5388            num_def_values,
5389        );
5390        Ok(EncodedPage {
5391            column_idx,
5392            data: vec![rep_bytes, def_bytes],
5393            description: PageEncoding::Structural(description),
5394            num_rows,
5395            row_number,
5396        })
5397    }
5398
5399    fn leaf_validity(
5400        repdef: &crate::repdef::SerializedRepDefs,
5401        num_values: usize,
5402    ) -> Result<Option<BooleanBuffer>> {
5403        let rep = repdef
5404            .repetition_levels
5405            .as_ref()
5406            .map(|rep| rep.as_ref().to_vec());
5407        let def = repdef
5408            .definition_levels
5409            .as_ref()
5410            .map(|def| def.as_ref().to_vec());
5411        let mut unraveler = RepDefUnraveler::new(
5412            rep,
5413            def,
5414            repdef.def_meaning.clone().into(),
5415            num_values as u64,
5416        );
5417        if unraveler.is_all_valid() {
5418            return Ok(None);
5419        }
5420        let mut validity = BooleanBufferBuilder::new(num_values);
5421        unraveler.unravel_validity(&mut validity)?;
5422        Ok(Some(validity.finish()))
5423    }
5424
5425    fn is_constant_values(
5426        arrays: &[ArrayRef],
5427        scalar: &ArrayRef,
5428        validity: Option<&BooleanBuffer>,
5429    ) -> Result<bool> {
5430        debug_assert_eq!(scalar.len(), 1);
5431        debug_assert_eq!(scalar.null_count(), 0);
5432
5433        match scalar.data_type() {
5434            DataType::Boolean => {
5435                let mut global_idx = 0usize;
5436                let scalar_val = scalar.as_boolean().value(0);
5437                for arr in arrays {
5438                    let bool_arr = arr.as_boolean();
5439                    for i in 0..arr.len() {
5440                        let is_valid = validity.map(|v| v.value(global_idx)).unwrap_or(true);
5441                        global_idx += 1;
5442                        if !is_valid {
5443                            continue;
5444                        }
5445                        if bool_arr.value(i) != scalar_val {
5446                            return Ok(false);
5447                        }
5448                    }
5449                }
5450                Ok(true)
5451            }
5452            DataType::Utf8 => Self::is_constant_utf8::<i32>(arrays, scalar, validity),
5453            DataType::LargeUtf8 => Self::is_constant_utf8::<i64>(arrays, scalar, validity),
5454            DataType::Binary => Self::is_constant_binary::<i32>(arrays, scalar, validity),
5455            DataType::LargeBinary => Self::is_constant_binary::<i64>(arrays, scalar, validity),
5456            data_type => {
5457                let mut global_idx = 0usize;
5458                let Some(byte_width) = data_type.byte_width_opt() else {
5459                    return Ok(false);
5460                };
5461                let scalar_data = scalar.to_data();
5462                if scalar_data.buffers().len() != 1 || !scalar_data.child_data().is_empty() {
5463                    return Ok(false);
5464                }
5465                let scalar_bytes = scalar_data.buffers()[0].as_slice();
5466                if scalar_bytes.len() != byte_width {
5467                    return Ok(false);
5468                }
5469
5470                for arr in arrays {
5471                    let data = arr.to_data();
5472                    if data.buffers().is_empty() {
5473                        return Ok(false);
5474                    }
5475                    let buf = data.buffers()[0].as_slice();
5476                    let base = data.offset();
5477                    for i in 0..arr.len() {
5478                        let is_valid = validity.map(|v| v.value(global_idx)).unwrap_or(true);
5479                        global_idx += 1;
5480                        if !is_valid {
5481                            continue;
5482                        }
5483                        let start = (base + i) * byte_width;
5484                        if buf[start..start + byte_width] != scalar_bytes[..] {
5485                            return Ok(false);
5486                        }
5487                    }
5488                }
5489                Ok(true)
5490            }
5491        }
5492    }
5493
5494    fn is_constant_utf8<O: arrow_array::OffsetSizeTrait>(
5495        arrays: &[ArrayRef],
5496        scalar: &ArrayRef,
5497        validity: Option<&BooleanBuffer>,
5498    ) -> Result<bool> {
5499        debug_assert_eq!(scalar.len(), 1);
5500        let scalar_val = scalar.as_string::<O>().value(0).as_bytes();
5501        let mut global_idx = 0usize;
5502        for arr in arrays {
5503            let str_arr = arr.as_string::<O>();
5504            for i in 0..arr.len() {
5505                let is_valid = validity.map(|v| v.value(global_idx)).unwrap_or(true);
5506                global_idx += 1;
5507                if !is_valid {
5508                    continue;
5509                }
5510                if str_arr.value(i).as_bytes() != scalar_val {
5511                    return Ok(false);
5512                }
5513            }
5514        }
5515        Ok(true)
5516    }
5517
5518    fn is_constant_binary<O: arrow_array::OffsetSizeTrait>(
5519        arrays: &[ArrayRef],
5520        scalar: &ArrayRef,
5521        validity: Option<&BooleanBuffer>,
5522    ) -> Result<bool> {
5523        debug_assert_eq!(scalar.len(), 1);
5524        let scalar_val = scalar.as_binary::<O>().value(0);
5525        let mut global_idx = 0usize;
5526        for arr in arrays {
5527            let bin_arr = arr.as_binary::<O>();
5528            for i in 0..arr.len() {
5529                let is_valid = validity.map(|v| v.value(global_idx)).unwrap_or(true);
5530                global_idx += 1;
5531                if !is_valid {
5532                    continue;
5533                }
5534                if bin_arr.value(i) != scalar_val {
5535                    return Ok(false);
5536                }
5537            }
5538        }
5539        Ok(true)
5540    }
5541
5542    fn find_constant_scalar(
5543        arrays: &[ArrayRef],
5544        validity: Option<&BooleanBuffer>,
5545    ) -> Result<Option<ArrayRef>> {
5546        if arrays.is_empty() {
5547            return Ok(None);
5548        }
5549
5550        let global_scalar_idx = if let Some(validity) = validity {
5551            let Some(idx) = (0..validity.len()).find(|&i| validity.value(i)) else {
5552                return Ok(None);
5553            };
5554            idx
5555        } else {
5556            0
5557        };
5558
5559        let mut idx_remaining = global_scalar_idx;
5560        let mut scalar_arr_idx = 0usize;
5561        while scalar_arr_idx < arrays.len() {
5562            let len = arrays[scalar_arr_idx].len();
5563            if idx_remaining < len {
5564                break;
5565            }
5566            idx_remaining -= len;
5567            scalar_arr_idx += 1;
5568        }
5569
5570        if scalar_arr_idx >= arrays.len() {
5571            return Ok(None);
5572        }
5573
5574        let scalar =
5575            lance_arrow::scalar::extract_scalar_value(&arrays[scalar_arr_idx], idx_remaining)?;
5576        if scalar.null_count() != 0 {
5577            return Ok(None);
5578        }
5579        if !Self::is_constant_values(arrays, &scalar, validity)? {
5580            return Ok(None);
5581        }
5582        Ok(Some(scalar))
5583    }
5584
5585    fn resolve_dict_values_compression_metadata(
5586        field_metadata: &HashMap<String, String>,
5587        env_compression: Option<String>,
5588        env_compression_level: Option<String>,
5589    ) -> HashMap<String, String> {
5590        let mut metadata = HashMap::new();
5591
5592        let compression = field_metadata
5593            .get(DICT_VALUES_COMPRESSION_META_KEY)
5594            .cloned()
5595            .or(env_compression)
5596            .unwrap_or_else(|| DEFAULT_DICT_VALUES_COMPRESSION.to_string());
5597        metadata.insert(COMPRESSION_META_KEY.to_string(), compression);
5598
5599        if let Some(compression_level) = field_metadata
5600            .get(DICT_VALUES_COMPRESSION_LEVEL_META_KEY)
5601            .cloned()
5602            .or(env_compression_level)
5603        {
5604            metadata.insert(COMPRESSION_LEVEL_META_KEY.to_string(), compression_level);
5605        }
5606
5607        metadata
5608    }
5609
5610    fn build_dict_values_compressor_field(field: &Field) -> Result<Field> {
5611        // This is an internal synthetic field used only to feed metadata into
5612        // `create_block_compressor` for dictionary values. The concrete type/name here
5613        // are not semantically meaningful; we rely on explicit metadata below to control
5614        // general compression selection for dictionary values.
5615        let mut dict_values_field = Field::new_arrow("", DataType::UInt16, false)?;
5616        dict_values_field.metadata = Self::resolve_dict_values_compression_metadata(
5617            &field.metadata,
5618            env::var(DICT_VALUES_COMPRESSION_ENV_VAR).ok(),
5619            env::var(DICT_VALUES_COMPRESSION_LEVEL_ENV_VAR).ok(),
5620        );
5621        Ok(dict_values_field)
5622    }
5623
5624    #[allow(clippy::too_many_arguments)]
5625    fn encode_miniblock(
5626        column_idx: u32,
5627        field: &Field,
5628        compression_strategy: &dyn CompressionStrategy,
5629        data: DataBlock,
5630        repdef: crate::repdef::SerializedRepDefs,
5631        row_number: u64,
5632        dictionary_data: Option<DataBlock>,
5633        num_rows: u64,
5634        miniblock_chunk_size: MiniblockChunkSize,
5635    ) -> Result<EncodedPage> {
5636        if let DataBlock::AllNull(_null_block) = data {
5637            // We should not be using mini-block for all-null.  There are other structural
5638            // encodings for that.
5639            unreachable!()
5640        }
5641
5642        let num_items = data.num_values();
5643
5644        let compressor = compression_strategy.create_miniblock_compressor(field, &data)?;
5645        let common_chunk_buffers =
5646            u64::from(repdef.rep_slicer().is_some()) + u64::from(repdef.def_slicer().is_some());
5647        let support_large_chunk = miniblock_chunk_size == MiniblockChunkSize::U32;
5648        let compression_context =
5649            MiniBlockCompressionContext::new(common_chunk_buffers, support_large_chunk, true);
5650        let (compressed_data, value_encoding) = compressor.compress(compression_context, data)?;
5651
5652        let max_rep = repdef.def_meaning.iter().filter(|l| l.is_list()).count() as u16;
5653
5654        let mut compressed_rep = repdef
5655            .rep_slicer()
5656            .map(|rep_slicer| {
5657                Self::compress_levels(
5658                    rep_slicer,
5659                    num_items,
5660                    compression_strategy,
5661                    &compressed_data.chunks,
5662                    max_rep,
5663                )
5664            })
5665            .transpose()?;
5666
5667        let (rep_index, rep_index_depth) =
5668            match compressed_rep.as_mut().and_then(|cr| cr.rep_index.as_mut()) {
5669                Some(rep_index) => (Some(rep_index.clone()), 1),
5670                None => (None, 0),
5671            };
5672
5673        let mut compressed_def = repdef
5674            .def_slicer()
5675            .map(|def_slicer| {
5676                Self::compress_levels(
5677                    def_slicer,
5678                    num_items,
5679                    compression_strategy,
5680                    &compressed_data.chunks,
5681                    /*max_rep=*/ 0,
5682                )
5683            })
5684            .transpose()?;
5685
5686        // TODO: Parquet sparsely encodes values here.  We could do the same but
5687        // then we won't have log2 values per chunk.  This means more metadata
5688        // and potentially more decoder asymmetry.  However, it may be worth
5689        // investigating at some point
5690
5691        let rep_data = compressed_rep
5692            .as_mut()
5693            .map(|cr| std::mem::take(&mut cr.data));
5694        let def_data = compressed_def
5695            .as_mut()
5696            .map(|cd| std::mem::take(&mut cd.data));
5697
5698        let serialized =
5699            Self::serialize_miniblocks(compressed_data, rep_data, def_data, miniblock_chunk_size)?;
5700        let has_large_chunk = miniblock_chunk_size == MiniblockChunkSize::U32;
5701
5702        // Metadata, Data, Dictionary, (maybe) Repetition Index
5703        let mut data = Vec::with_capacity(4);
5704        data.push(serialized.metadata);
5705        data.push(serialized.data);
5706
5707        if let Some(dictionary_data) = dictionary_data {
5708            let num_dictionary_items = dictionary_data.num_values();
5709            let dict_values_field = Self::build_dict_values_compressor_field(field)?;
5710
5711            let (dictionary_buffer, dictionary_encoding) =
5712                compress_required_block(compression_strategy, &dict_values_field, dictionary_data)?;
5713
5714            data.push(dictionary_buffer);
5715            if let Some(rep_index) = rep_index {
5716                data.push(rep_index);
5717            }
5718
5719            let description = ProtobufUtils21::miniblock_layout(
5720                compressed_rep.map(|cr| cr.compression),
5721                compressed_def.map(|cd| cd.compression),
5722                value_encoding,
5723                rep_index_depth,
5724                serialized.num_buffers,
5725                Some((dictionary_encoding, num_dictionary_items)),
5726                &repdef.def_meaning,
5727                num_items,
5728                has_large_chunk,
5729            );
5730            Ok(EncodedPage {
5731                num_rows,
5732                column_idx,
5733                data,
5734                description: PageEncoding::Structural(description),
5735                row_number,
5736            })
5737        } else {
5738            let description = ProtobufUtils21::miniblock_layout(
5739                compressed_rep.map(|cr| cr.compression),
5740                compressed_def.map(|cd| cd.compression),
5741                value_encoding,
5742                rep_index_depth,
5743                serialized.num_buffers,
5744                None,
5745                &repdef.def_meaning,
5746                num_items,
5747                has_large_chunk,
5748            );
5749
5750            if let Some(rep_index) = rep_index {
5751                let view = rep_index.borrow_to_typed_slice::<u64>();
5752                let total = view.chunks_exact(2).map(|c| c[0]).sum::<u64>();
5753                debug_assert_eq!(total, num_rows);
5754
5755                data.push(rep_index);
5756            }
5757
5758            Ok(EncodedPage {
5759                num_rows,
5760                column_idx,
5761                data,
5762                description: PageEncoding::Structural(description),
5763                row_number,
5764            })
5765        }
5766    }
5767
5768    // For fixed-size data we encode < control word | data > for each value
5769    fn serialize_full_zip_fixed(
5770        fixed: FixedWidthDataBlock,
5771        mut repdef: ControlWordIterator,
5772        num_values: u64,
5773    ) -> Result<SerializedFullZip> {
5774        if !fixed.bits_per_value.is_multiple_of(8) {
5775            return Err(Error::invalid_input_source(
5776                format!(
5777                    "Full-zip fixed-width values must be byte aligned, got {} bits per value",
5778                    fixed.bits_per_value
5779                )
5780                .into(),
5781            ));
5782        }
5783
5784        let len = fixed.data.len() + repdef.bytes_per_word() * num_values as usize;
5785        let mut zipped_data = Vec::with_capacity(len);
5786
5787        let max_rep_index_val = if repdef.has_repetition() {
5788            len as u64
5789        } else {
5790            // Setting this to 0 means we won't write a repetition index
5791            0
5792        };
5793        let mut rep_index_builder =
5794            BytepackedIntegerEncoder::with_capacity(num_values as usize + 1, max_rep_index_val);
5795
5796        let bytes_per_value = fixed.bits_per_value as usize / 8;
5797        let mut offset = 0;
5798
5799        if bytes_per_value == 0 {
5800            // No data, just dump the repdef into the buffer
5801            while let Some(control) = repdef.append_next(&mut zipped_data) {
5802                if control.is_new_row {
5803                    // We have finished a row
5804                    debug_assert!(offset <= len);
5805                    rep_index_builder.append_trusted(offset as u64);
5806                }
5807                offset = zipped_data.len();
5808            }
5809        } else {
5810            // We have data, zip it with the repdef
5811            let mut data_iter = fixed.data.chunks_exact(bytes_per_value);
5812            while let Some(control) = repdef.append_next(&mut zipped_data) {
5813                if control.is_new_row {
5814                    // We have finished a row
5815                    debug_assert!(offset <= len);
5816                    rep_index_builder.append_trusted(offset as u64);
5817                }
5818                if control.is_visible {
5819                    let value = data_iter.next().unwrap();
5820                    zipped_data.extend_from_slice(value);
5821                }
5822                offset = zipped_data.len();
5823            }
5824        }
5825
5826        debug_assert_eq!(zipped_data.len(), len);
5827        // Put the final value in the rep index
5828        rep_index_builder.append_trusted(zipped_data.len() as u64);
5829
5830        let zipped_data = LanceBuffer::from(zipped_data);
5831        let rep_index = rep_index_builder.into_data();
5832        let rep_index = if rep_index.is_empty() {
5833            None
5834        } else {
5835            Some(LanceBuffer::from(rep_index))
5836        };
5837        Ok(SerializedFullZip {
5838            values: zipped_data,
5839            repetition_index: rep_index,
5840        })
5841    }
5842
5843    // For variable-size data we encode < control word | length | data > for each value
5844    //
5845    // In addition, we create a second buffer, the repetition index
5846    fn serialize_full_zip_variable(
5847        variable: VariableWidthBlock,
5848        mut repdef: ControlWordIterator,
5849        num_items: u64,
5850    ) -> Result<SerializedFullZip> {
5851        let bytes_per_offset = variable.bits_per_offset as usize / 8;
5852        if !variable.bits_per_offset.is_multiple_of(8) {
5853            return Err(Error::invalid_input_source(
5854                format!(
5855                    "Full-zip variable-width offsets must be byte aligned, got {} bits per offset",
5856                    variable.bits_per_offset
5857                )
5858                .into(),
5859            ));
5860        }
5861        let len = variable.data.len()
5862            + repdef.bytes_per_word() * num_items as usize
5863            + bytes_per_offset * variable.num_values as usize;
5864        let mut buf = Vec::with_capacity(len);
5865
5866        let max_rep_index_val = len as u64;
5867        let mut rep_index_builder =
5868            BytepackedIntegerEncoder::with_capacity(num_items as usize + 1, max_rep_index_val);
5869
5870        // TODO: byte pack the item lengths with varint encoding
5871        match bytes_per_offset {
5872            4 => {
5873                let offs = variable.offsets.borrow_to_typed_slice::<u32>();
5874                let mut rep_offset = 0;
5875                let mut windows_iter = offs.as_ref().windows(2);
5876                while let Some(control) = repdef.append_next(&mut buf) {
5877                    if control.is_new_row {
5878                        // We have finished a row
5879                        debug_assert!(rep_offset <= len);
5880                        rep_index_builder.append_trusted(rep_offset as u64);
5881                    }
5882                    if control.is_visible {
5883                        let window = windows_iter.next().unwrap();
5884                        if control.is_valid_item {
5885                            buf.extend_from_slice(&(window[1] - window[0]).to_le_bytes());
5886                            buf.extend_from_slice(
5887                                &variable.data[window[0] as usize..window[1] as usize],
5888                            );
5889                        }
5890                    }
5891                    rep_offset = buf.len();
5892                }
5893            }
5894            8 => {
5895                let offs = variable.offsets.borrow_to_typed_slice::<u64>();
5896                let mut rep_offset = 0;
5897                let mut windows_iter = offs.as_ref().windows(2);
5898                while let Some(control) = repdef.append_next(&mut buf) {
5899                    if control.is_new_row {
5900                        // We have finished a row
5901                        debug_assert!(rep_offset <= len);
5902                        rep_index_builder.append_trusted(rep_offset as u64);
5903                    }
5904                    if control.is_visible {
5905                        let window = windows_iter.next().unwrap();
5906                        if control.is_valid_item {
5907                            buf.extend_from_slice(&(window[1] - window[0]).to_le_bytes());
5908                            buf.extend_from_slice(
5909                                &variable.data[window[0] as usize..window[1] as usize],
5910                            );
5911                        }
5912                    }
5913                    rep_offset = buf.len();
5914                }
5915            }
5916            _ => {
5917                return Err(Error::invalid_input_source(
5918                    format!(
5919                        "Full-zip variable-width offsets must be 32 or 64 bits, got {} bits",
5920                        variable.bits_per_offset
5921                    )
5922                    .into(),
5923                ));
5924            }
5925        }
5926
5927        // We might have saved a few bytes by not copying lengths when the length was zero.  However,
5928        // if we are over `len` then we have a bug.
5929        debug_assert!(buf.len() <= len);
5930        // Put the final value in the rep index
5931        rep_index_builder.append_trusted(buf.len() as u64);
5932
5933        let zipped_data = LanceBuffer::from(buf);
5934        let rep_index = rep_index_builder.into_data();
5935        debug_assert!(!rep_index.is_empty());
5936        let rep_index = Some(LanceBuffer::from(rep_index));
5937        Ok(SerializedFullZip {
5938            values: zipped_data,
5939            repetition_index: rep_index,
5940        })
5941    }
5942
5943    /// Serializes data into a single buffer according to the full-zip format which zips
5944    /// together the repetition, definition, and value data into a single buffer.
5945    fn serialize_full_zip(
5946        compressed_data: PerValueDataBlock,
5947        repdef: ControlWordIterator,
5948        num_items: u64,
5949    ) -> Result<SerializedFullZip> {
5950        match compressed_data {
5951            PerValueDataBlock::Fixed(fixed) => {
5952                Self::serialize_full_zip_fixed(fixed, repdef, num_items)
5953            }
5954            PerValueDataBlock::Variable(var) => {
5955                Self::serialize_full_zip_variable(var, repdef, num_items)
5956            }
5957        }
5958    }
5959
5960    fn expand_boolean_to_bytes(fixed: FixedWidthDataBlock) -> FixedWidthDataBlock {
5961        debug_assert_eq!(fixed.bits_per_value, 1);
5962        let num_values = fixed.num_values as usize;
5963        let bool_buf = BooleanBuffer::new(fixed.data.into_buffer(), 0, num_values);
5964        let expanded: Vec<u8> = (0..num_values).map(|i| bool_buf.value(i) as u8).collect();
5965        FixedWidthDataBlock {
5966            data: LanceBuffer::from(expanded),
5967            bits_per_value: 8,
5968            num_values: fixed.num_values,
5969            block_info: BlockInfo::new(),
5970        }
5971    }
5972
5973    fn encode_full_zip(
5974        column_idx: u32,
5975        field: &Field,
5976        compression_strategy: &dyn CompressionStrategy,
5977        data: DataBlock,
5978        repdef: crate::repdef::SerializedRepDefs,
5979        row_number: u64,
5980        num_lists: u64,
5981    ) -> Result<EncodedPage> {
5982        let max_rep = repdef
5983            .repetition_levels
5984            .as_ref()
5985            .map_or(0, |r| r.iter().max().copied().unwrap_or(0));
5986        let max_def = repdef
5987            .definition_levels
5988            .as_ref()
5989            .map_or(0, |d| d.iter().max().copied().unwrap_or(0));
5990
5991        // To handle FSL we just flatten
5992        // let data = data.flatten();
5993
5994        let (num_items, num_visible_items) =
5995            if let Some(rep_levels) = repdef.repetition_levels.as_ref() {
5996                // If there are rep levels there may be "invisible" items and we need to encode
5997                // rep_levels.len() things which might be larger than data.num_values()
5998                (rep_levels.len() as u64, data.num_values())
5999            } else {
6000                // If there are no rep levels then we encode data.num_values() things
6001                (data.num_values(), data.num_values())
6002            };
6003
6004        let max_visible_def = repdef.max_visible_level.unwrap_or(u16::MAX);
6005
6006        let repdef_iter = build_control_word_iterator(
6007            repdef.repetition_levels.as_deref(),
6008            max_rep,
6009            repdef.definition_levels.as_deref(),
6010            max_def,
6011            max_visible_def,
6012            num_items as usize,
6013        );
6014        let bits_rep = repdef_iter.bits_rep();
6015        let bits_def = repdef_iter.bits_def();
6016
6017        // Full-zip requires byte-aligned values; expand 1-bit booleans to 1 byte each.
6018        let data = match data {
6019            DataBlock::FixedWidth(fixed) if fixed.bits_per_value == 1 => {
6020                DataBlock::FixedWidth(Self::expand_boolean_to_bytes(fixed))
6021            }
6022            other => other,
6023        };
6024
6025        let compressor = compression_strategy.create_per_value(field, &data)?;
6026        let (compressed_data, value_encoding) = compressor.compress(data)?;
6027
6028        let description = match &compressed_data {
6029            PerValueDataBlock::Fixed(fixed) => ProtobufUtils21::fixed_full_zip_layout(
6030                bits_rep,
6031                bits_def,
6032                fixed.bits_per_value as u32,
6033                value_encoding,
6034                &repdef.def_meaning,
6035                num_items as u32,
6036                num_visible_items as u32,
6037            ),
6038            PerValueDataBlock::Variable(variable) => ProtobufUtils21::variable_full_zip_layout(
6039                bits_rep,
6040                bits_def,
6041                variable.bits_per_offset as u32,
6042                value_encoding,
6043                &repdef.def_meaning,
6044                num_items as u32,
6045                num_visible_items as u32,
6046            ),
6047        };
6048
6049        let zipped = Self::serialize_full_zip(compressed_data, repdef_iter, num_items)?;
6050
6051        let data = if let Some(repindex) = zipped.repetition_index {
6052            vec![zipped.values, repindex]
6053        } else {
6054            vec![zipped.values]
6055        };
6056
6057        Ok(EncodedPage {
6058            num_rows: num_lists,
6059            column_idx,
6060            data,
6061            description: PageEncoding::Structural(description),
6062            row_number,
6063        })
6064    }
6065
6066    fn should_dictionary_encode(
6067        data_block: &DataBlock,
6068        field: &Field,
6069        fixed_width_dictionary_encoding: FixedWidthDictionaryEncoding,
6070    ) -> Option<DictEncodingBudget> {
6071        const DEFAULT_SAMPLE_SIZE: usize = 4096;
6072        const DEFAULT_SAMPLE_UNIQUE_RATIO: f64 = 0.98;
6073
6074        // Since we only dictionary encode FixedWidth and VariableWidth blocks for now, we skip
6075        // estimating the size for other types.
6076        match data_block {
6077            DataBlock::FixedWidth(fixed) => {
6078                if fixed.bits_per_value == 64
6079                    && fixed_width_dictionary_encoding == FixedWidthDictionaryEncoding::Exclude64Bit
6080                {
6081                    return None;
6082                }
6083                if fixed.bits_per_value != 64 && fixed.bits_per_value != 128 {
6084                    return None;
6085                }
6086                if fixed.bits_per_value % 8 != 0 {
6087                    return None;
6088                }
6089            }
6090            DataBlock::VariableWidth(var) => {
6091                if var.bits_per_offset != 32 && var.bits_per_offset != 64 {
6092                    return None;
6093                }
6094            }
6095            _ => return None,
6096        }
6097
6098        // Don't dictionary encode tiny arrays.
6099        let too_small = env::var("LANCE_ENCODING_DICT_TOO_SMALL")
6100            .ok()
6101            .and_then(|val| val.parse().ok())
6102            .unwrap_or(100);
6103        if data_block.num_values() < too_small {
6104            return None;
6105        }
6106
6107        let num_values = data_block.num_values();
6108
6109        // Apply divisor threshold and cap. This is intentionally conservative: the goal is to
6110        // avoid spending too much CPU trying to estimate very high cardinalities.
6111        let divisor: u64 = field
6112            .metadata
6113            .get(DICT_DIVISOR_META_KEY)
6114            .and_then(|val| val.parse().ok())
6115            .or_else(|| {
6116                env::var("LANCE_ENCODING_DICT_DIVISOR")
6117                    .ok()
6118                    .and_then(|val| val.parse().ok())
6119            })
6120            .unwrap_or(DEFAULT_DICT_DIVISOR);
6121
6122        let max_cardinality: u64 = env::var("LANCE_ENCODING_DICT_MAX_CARDINALITY")
6123            .ok()
6124            .and_then(|val| val.parse().ok())
6125            .unwrap_or(DEFAULT_DICT_MAX_CARDINALITY);
6126
6127        let threshold_cardinality = num_values
6128            .checked_div(divisor.max(1))
6129            .unwrap_or(0)
6130            .min(max_cardinality);
6131        if threshold_cardinality == 0 {
6132            return None;
6133        }
6134
6135        // Get size ratio from metadata or env var.
6136        let threshold_ratio = field
6137            .metadata
6138            .get(DICT_SIZE_RATIO_META_KEY)
6139            .and_then(|val| val.parse::<f64>().ok())
6140            .or_else(|| {
6141                env::var("LANCE_ENCODING_DICT_SIZE_RATIO")
6142                    .ok()
6143                    .and_then(|val| val.parse().ok())
6144            })
6145            .unwrap_or(DEFAULT_DICT_SIZE_RATIO);
6146
6147        if threshold_ratio <= 0.0 || threshold_ratio > 1.0 {
6148            panic!(
6149                "Invalid parameter: dict-size-ratio is {} which is not in the range (0, 1].",
6150                threshold_ratio
6151            );
6152        }
6153
6154        let data_size = data_block.data_size();
6155        if data_size == 0 {
6156            return None;
6157        }
6158
6159        let max_encoded_size = (data_size as f64 * threshold_ratio) as u64;
6160        let max_encoded_size = usize::try_from(max_encoded_size).ok()?;
6161
6162        // Avoid probing dictionary encoding on data that appears to be near-unique
6163        // or likely to exceed the dictionary budget.
6164        if let Some(sample_unique_ratio) =
6165            Self::sample_unique_ratio(data_block, DEFAULT_SAMPLE_SIZE)?
6166        {
6167            if sample_unique_ratio >= DEFAULT_SAMPLE_UNIQUE_RATIO {
6168                return None;
6169            }
6170
6171            let projected_cardinality = (sample_unique_ratio * num_values as f64).ceil() as u64;
6172            if projected_cardinality > threshold_cardinality {
6173                return None;
6174            }
6175        }
6176
6177        let max_dict_entries = u32::try_from(threshold_cardinality.min(i32::MAX as u64)).ok()?;
6178        Some(DictEncodingBudget {
6179            max_dict_entries,
6180            max_encoded_size,
6181        })
6182    }
6183
6184    /// Samples whether a page looks near-unique before attempting dictionary encoding.
6185    ///
6186    /// The probe uses deterministic block sampling (not RNG sampling), which keeps
6187    /// the check cheap and reproducible across runs. The result is only a gate for
6188    /// whether we try dictionary encoding, not a cardinality statistic.
6189    /// Returns `Some(None)` when there are too few reliable samples or the block type does not
6190    /// support dictionary encoding. Returns `None` for malformed data.
6191    fn sample_unique_ratio(data_block: &DataBlock, max_samples: usize) -> Option<Option<f64>> {
6192        use std::collections::HashSet;
6193
6194        const NUM_SAMPLE_BLOCKS: usize = 32;
6195        const MIN_RELIABLE_SAMPLES: usize = 1024;
6196
6197        let num_values = usize::try_from(data_block.num_values()).ok()?;
6198        if num_values == 0 {
6199            return Some(None);
6200        }
6201
6202        let sample_count = num_values.min(max_samples).max(1);
6203        if sample_count < MIN_RELIABLE_SAMPLES {
6204            return Some(None);
6205        }
6206
6207        let block_count = NUM_SAMPLE_BLOCKS.min(sample_count).min(num_values).max(1);
6208        let samples_per_block = (sample_count / block_count).max(1);
6209        let mut indices = Vec::with_capacity(sample_count);
6210        for block_idx in 0..block_count {
6211            let block_start = block_idx * num_values / block_count;
6212            let next_block_start = ((block_idx + 1) * num_values / block_count).min(num_values);
6213            let block_len = next_block_start.saturating_sub(block_start);
6214            let samples_in_block = samples_per_block.min(block_len);
6215            indices.extend((0..samples_in_block).map(|offset| block_start + offset));
6216        }
6217
6218        if indices.len() < MIN_RELIABLE_SAMPLES {
6219            return Some(None);
6220        }
6221
6222        let ratio = match data_block {
6223            DataBlock::FixedWidth(fixed) => match fixed.bits_per_value {
6224                64 => {
6225                    let values = fixed.data.borrow_to_typed_slice::<u64>();
6226                    let values = values.as_ref();
6227                    let mut unique: HashSet<u64> =
6228                        HashSet::with_capacity(indices.len().min(MIN_RELIABLE_SAMPLES));
6229                    for idx in indices.iter().copied() {
6230                        unique.insert(values.get(idx).copied()?);
6231                    }
6232                    unique.len() as f64 / indices.len() as f64
6233                }
6234                128 => {
6235                    let values = fixed.data.borrow_to_typed_slice::<u128>();
6236                    let values = values.as_ref();
6237                    let mut unique: HashSet<u128> =
6238                        HashSet::with_capacity(indices.len().min(MIN_RELIABLE_SAMPLES));
6239                    for idx in indices.iter().copied() {
6240                        unique.insert(values.get(idx).copied()?);
6241                    }
6242                    unique.len() as f64 / indices.len() as f64
6243                }
6244                _ => return Some(None),
6245            },
6246            DataBlock::VariableWidth(var) => {
6247                use xxhash_rust::xxh3::xxh3_64;
6248
6249                // Hash variable-width slices instead of storing borrowed slice keys.
6250                let mut unique: HashSet<u64> =
6251                    HashSet::with_capacity(indices.len().min(MIN_RELIABLE_SAMPLES));
6252                match var.bits_per_offset {
6253                    32 => {
6254                        let offsets_ref = var.offsets.borrow_to_typed_slice::<u32>();
6255                        let offsets: &[u32] = offsets_ref.as_ref();
6256                        for i in indices.iter().copied() {
6257                            let start = usize::try_from(*offsets.get(i)?).ok()?;
6258                            let end = usize::try_from(*offsets.get(i + 1)?).ok()?;
6259                            if start > end || end > var.data.len() {
6260                                return None;
6261                            }
6262                            unique.insert(xxh3_64(&var.data[start..end]));
6263                        }
6264                    }
6265                    64 => {
6266                        let offsets_ref = var.offsets.borrow_to_typed_slice::<u64>();
6267                        let offsets: &[u64] = offsets_ref.as_ref();
6268                        for i in indices.iter().copied() {
6269                            let start = usize::try_from(*offsets.get(i)?).ok()?;
6270                            let end = usize::try_from(*offsets.get(i + 1)?).ok()?;
6271                            if start > end || end > var.data.len() {
6272                                return None;
6273                            }
6274                            unique.insert(xxh3_64(&var.data[start..end]));
6275                        }
6276                    }
6277                    _ => return Some(None),
6278                }
6279                unique.len() as f64 / indices.len() as f64
6280            }
6281            _ => return Some(None),
6282        };
6283
6284        Some(Some(ratio))
6285    }
6286
6287    fn slice_repdef(repdef: &SerializedRepDefs, range: Range<usize>) -> SerializedRepDefs {
6288        let repetition_levels = repdef
6289            .repetition_levels
6290            .as_ref()
6291            .map(|levels| levels[range.clone()].to_vec());
6292        let definition_levels = repdef
6293            .definition_levels
6294            .as_ref()
6295            .map(|levels| levels[range].to_vec());
6296        SerializedRepDefs::new_with_fixed_size_list_levels(
6297            repetition_levels,
6298            definition_levels,
6299            repdef.def_meaning.clone(),
6300            repdef.has_fixed_size_list_levels(),
6301        )
6302    }
6303
6304    fn slice_arrays(
6305        arrays: &[ArrayRef],
6306        value_start: u64,
6307        num_values: u64,
6308    ) -> Result<Vec<ArrayRef>> {
6309        if num_values == 0 {
6310            return Ok(Vec::new());
6311        }
6312
6313        let mut values_to_skip = usize::try_from(value_start).map_err(|_| {
6314            Error::invalid_input(format!("Value start {} is too large", value_start))
6315        })?;
6316        let mut values_remaining = usize::try_from(num_values).map_err(|_| {
6317            Error::invalid_input(format!("Value count {} is too large", num_values))
6318        })?;
6319        let mut sliced = Vec::new();
6320
6321        for array in arrays {
6322            if values_to_skip >= array.len() {
6323                values_to_skip -= array.len();
6324                continue;
6325            }
6326
6327            let offset = values_to_skip;
6328            let len = (array.len() - offset).min(values_remaining);
6329            sliced.push(array.slice(offset, len));
6330            values_remaining -= len;
6331            values_to_skip = 0;
6332
6333            if values_remaining == 0 {
6334                break;
6335            }
6336        }
6337
6338        if values_remaining != 0 {
6339            return Err(Error::internal(format!(
6340                "Page split requested {} values starting at {}, but the page did not contain enough values",
6341                num_values, value_start
6342            )));
6343        }
6344
6345        Ok(sliced)
6346    }
6347
6348    fn split_pages_for_miniblock_repdef_budget(
6349        arrays: Vec<ArrayRef>,
6350        repdef: SerializedRepDefs,
6351        budget: MiniBlockRepDefBudget,
6352        row_number: u64,
6353        num_rows: u64,
6354    ) -> Result<Vec<PrimitivePageData>> {
6355        if budget == MiniBlockRepDefBudget::WithinBudget {
6356            return Ok(vec![PrimitivePageData {
6357                arrays,
6358                structure: PrimitivePageStructure::Dense {
6359                    repdef,
6360                    single_row_miniblock_repdef_levels: None,
6361                },
6362                row_number,
6363                num_rows,
6364            }]);
6365        }
6366        if let MiniBlockRepDefBudget::SingleRowOverBudget(num_levels) = budget {
6367            return Ok(vec![PrimitivePageData {
6368                arrays,
6369                structure: PrimitivePageStructure::Dense {
6370                    repdef,
6371                    single_row_miniblock_repdef_levels: Some(num_levels),
6372                },
6373                row_number,
6374                num_rows,
6375            }]);
6376        }
6377
6378        let MiniBlockRepDefBudget::RequiresPageSplit(splits) = budget else {
6379            unreachable!();
6380        };
6381
6382        let mut pages = Vec::with_capacity(splits.len());
6383        for split in splits {
6384            let arrays = Self::slice_arrays(&arrays, split.value_start, split.num_values)?;
6385            let repdef = Self::slice_repdef(&repdef, split.level_range);
6386            pages.push(PrimitivePageData {
6387                arrays,
6388                structure: PrimitivePageStructure::Dense {
6389                    repdef,
6390                    single_row_miniblock_repdef_levels: None,
6391                },
6392                row_number: row_number + split.row_start,
6393                num_rows: split.num_rows,
6394            });
6395        }
6396        Ok(pages)
6397    }
6398
6399    fn encode_dense_page(
6400        ctx: PrimitiveEncodeContext,
6401        page: PrimitivePageData,
6402        compression_strategy: Arc<dyn CompressionStrategy>,
6403        miniblock_chunk_size: MiniblockChunkSize,
6404        complex_null_encoding: ComplexNullEncoding,
6405        fixed_width_dictionary_encoding: FixedWidthDictionaryEncoding,
6406    ) -> Result<EncodedPage> {
6407        let PrimitiveEncodeContext {
6408            column_idx,
6409            field,
6410            encoding_metadata,
6411            is_simple_validity,
6412            has_repdef_info,
6413        } = ctx;
6414        let PrimitivePageData {
6415            arrays,
6416            structure,
6417            row_number,
6418            num_rows,
6419        } = page;
6420        let num_values = arrays.iter().map(|arr| arr.len() as u64).sum();
6421
6422        let (repdef, single_row_miniblock_repdef_levels) = match structure {
6423            PrimitivePageStructure::Dense {
6424                repdef,
6425                single_row_miniblock_repdef_levels,
6426            } => (repdef, single_row_miniblock_repdef_levels),
6427            PrimitivePageStructure::Sparse { .. } => {
6428                unreachable!("dense atom received sparse page")
6429            }
6430        };
6431
6432        if num_values == 0 {
6433            // This page contains only structural events, such as empty/null list rows.
6434            // The existing complex-null layout stores the rep/def stream without value buffers.
6435            log::debug!(
6436                "Encoding column {} with {} items ({} rows) using complex-null layout",
6437                column_idx,
6438                num_values,
6439                num_rows
6440            );
6441            return Self::encode_complex_all_null(
6442                column_idx,
6443                repdef,
6444                row_number,
6445                num_rows,
6446                complex_null_encoding,
6447                compression_strategy.as_ref(),
6448            );
6449        }
6450
6451        let leaf_validity = Self::leaf_validity(&repdef, num_values as usize)?;
6452        let all_null = leaf_validity
6453            .as_ref()
6454            .map(|validity| validity.count_set_bits() == 0)
6455            .unwrap_or(false);
6456
6457        if all_null {
6458            return if is_simple_validity {
6459                log::debug!(
6460                    "Encoding column {} with {} items ({} rows) using simple-null layout",
6461                    column_idx,
6462                    num_values,
6463                    num_rows
6464                );
6465                Self::encode_simple_all_null(column_idx, num_values, row_number)
6466            } else {
6467                log::debug!(
6468                    "Encoding column {} with {} items ({} rows) using complex-null layout",
6469                    column_idx,
6470                    num_values,
6471                    num_rows
6472                );
6473                Self::encode_complex_all_null(
6474                    column_idx,
6475                    repdef,
6476                    row_number,
6477                    num_rows,
6478                    complex_null_encoding,
6479                    compression_strategy.as_ref(),
6480                )
6481            };
6482        }
6483
6484        if let DataType::Struct(fields) = &field.data_type()
6485            && fields.is_empty()
6486        {
6487            if has_repdef_info {
6488                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()));
6489            }
6490            // This is maybe a little confusing but the reader should never look at this anyways and it
6491            // seems like overkill to invent a new layout just for "empty structs".
6492            return Self::encode_simple_all_null(column_idx, num_values, row_number);
6493        }
6494
6495        let data_block = DataBlock::from_arrays(&arrays, num_values);
6496
6497        if let Some(num_levels) = single_row_miniblock_repdef_levels {
6498            let requested_encoding = encoding_metadata
6499                .get(STRUCTURAL_ENCODING_META_KEY)
6500                .map(|requested| requested.to_lowercase());
6501            let fullzip_error = match &data_block {
6502                // 1-bit booleans are widened to bytes inside `encode_full_zip`.
6503                DataBlock::FixedWidth(fixed)
6504                    if fixed.bits_per_value != 1 && !fixed.bits_per_value.is_multiple_of(8) =>
6505                {
6506                    Some(format!(
6507                        "Full-zip fixed-width values must be byte aligned, got {} bits per value",
6508                        fixed.bits_per_value
6509                    ))
6510                }
6511                DataBlock::VariableWidth(variable)
6512                    if !variable.bits_per_offset.is_multiple_of(8) =>
6513                {
6514                    Some(format!(
6515                        "Full-zip variable-width offsets must be byte aligned, got {} bits per offset",
6516                        variable.bits_per_offset
6517                    ))
6518                }
6519                DataBlock::VariableWidth(variable)
6520                    if variable.bits_per_offset != 32 && variable.bits_per_offset != 64 =>
6521                {
6522                    Some(format!(
6523                        "Full-zip variable-width offsets must be 32 or 64 bits, got {} bits",
6524                        variable.bits_per_offset
6525                    ))
6526                }
6527                DataBlock::Dictionary(_) => {
6528                    Some("Full-zip does not encode dictionary data blocks directly".to_string())
6529                }
6530                DataBlock::FixedSizeList(fsl) => match fsl.clone().try_into_flat() {
6531                    Some(flat) if flat.bits_per_value.is_multiple_of(8) => None,
6532                    Some(flat) => Some(format!(
6533                        "Full-zip fixed-size-list values must be byte aligned after flattening, got {} bits per value",
6534                        flat.bits_per_value
6535                    )),
6536                    None => Some(
6537                        "Full-zip fixed-size-list capability requires a flat fixed-width child"
6538                            .to_string(),
6539                    ),
6540                },
6541                DataBlock::FixedWidth(_) | DataBlock::VariableWidth(_) | DataBlock::Struct(_) => {
6542                    None
6543                }
6544                other => Some(format!(
6545                    "Full-zip does not support value block type {}",
6546                    other.name()
6547                )),
6548            };
6549            match requested_encoding.as_deref() {
6550                Some(STRUCTURAL_ENCODING_FULLZIP) => {
6551                    if let Some(reason) = fullzip_error {
6552                        return Err(Error::invalid_input_source(reason.into()));
6553                    }
6554                    return Self::encode_full_zip(
6555                        column_idx,
6556                        &field,
6557                        compression_strategy.as_ref(),
6558                        data_block,
6559                        repdef,
6560                        row_number,
6561                        num_rows,
6562                    );
6563                }
6564                Some(STRUCTURAL_ENCODING_MINIBLOCK) | None => {
6565                    if requested_encoding.is_none() && fullzip_error.is_none() {
6566                        log::debug!(
6567                            "Encoding column {} with {} items using full-zip layout because mini-block cannot split the structural page",
6568                            column_idx,
6569                            num_values
6570                        );
6571                        return Self::encode_full_zip(
6572                            column_idx,
6573                            &field,
6574                            compression_strategy.as_ref(),
6575                            data_block,
6576                            repdef,
6577                            row_number,
6578                            num_rows,
6579                        );
6580                    }
6581                    return Err(Error::invalid_input_source(
6582                        format!(
6583                            "Mini-block cannot encode {} rep/def levels in one top-level row. \
6584                             This usually means the row contains too much nested structure \
6585                             for the current layout.",
6586                            num_levels
6587                        )
6588                        .into(),
6589                    ));
6590                }
6591                _ => {}
6592            }
6593        }
6594
6595        let requires_full_zip_packed_struct =
6596            if let DataBlock::Struct(ref struct_data_block) = data_block {
6597                struct_data_block.has_variable_width_child()
6598            } else {
6599                false
6600            };
6601
6602        if requires_full_zip_packed_struct {
6603            log::debug!(
6604                "Encoding column {} with {} items using full-zip packed struct layout",
6605                column_idx,
6606                num_values
6607            );
6608            return Self::encode_full_zip(
6609                column_idx,
6610                &field,
6611                compression_strategy.as_ref(),
6612                data_block,
6613                repdef,
6614                row_number,
6615                num_rows,
6616            );
6617        }
6618
6619        if let DataBlock::Dictionary(dict) = data_block {
6620            log::debug!(
6621                "Encoding column {} with {} items using dictionary encoding (already dictionary encoded)",
6622                column_idx,
6623                num_values
6624            );
6625            let (mut indices_data_block, dictionary_data_block) = dict.into_parts();
6626            // TODO: https://github.com/lancedb/lance/issues/4809
6627            // If we compute stats on dictionary_data_block => panic.
6628            // If we don't compute stats on indices_data_block => panic.
6629            // This is messy.  Don't make me call compute_stat ever.
6630            indices_data_block.compute_stat();
6631            return Self::encode_miniblock(
6632                column_idx,
6633                &field,
6634                compression_strategy.as_ref(),
6635                indices_data_block,
6636                repdef,
6637                row_number,
6638                Some(dictionary_data_block),
6639                num_rows,
6640                miniblock_chunk_size,
6641            );
6642        }
6643
6644        // Try dictionary encoding first if applicable. If encoding aborts, fall back to the
6645        // preferred structural encoding.
6646        let dict_result = Self::should_dictionary_encode(
6647            &data_block,
6648            &field,
6649            fixed_width_dictionary_encoding,
6650        )
6651        .and_then(|budget| {
6652            log::debug!(
6653                "Encoding column {} with {} items using dictionary encoding (mini-block layout)",
6654                column_idx,
6655                num_values
6656            );
6657            dict::dictionary_encode(
6658                &data_block,
6659                budget.max_dict_entries,
6660                budget.max_encoded_size,
6661            )
6662        });
6663
6664        if let Some((indices_data_block, dictionary_data_block)) = dict_result {
6665            Self::encode_miniblock(
6666                column_idx,
6667                &field,
6668                compression_strategy.as_ref(),
6669                indices_data_block,
6670                repdef,
6671                row_number,
6672                Some(dictionary_data_block),
6673                num_rows,
6674                miniblock_chunk_size,
6675            )
6676        } else if Self::prefers_miniblock(&data_block, encoding_metadata.as_ref()) {
6677            log::debug!(
6678                "Encoding column {} with {} items using mini-block layout",
6679                column_idx,
6680                num_values
6681            );
6682            Self::encode_miniblock(
6683                column_idx,
6684                &field,
6685                compression_strategy.as_ref(),
6686                data_block,
6687                repdef,
6688                row_number,
6689                None,
6690                num_rows,
6691                miniblock_chunk_size,
6692            )
6693        } else if Self::prefers_fullzip(encoding_metadata.as_ref()) {
6694            log::debug!(
6695                "Encoding column {} with {} items using full-zip layout",
6696                column_idx,
6697                num_values
6698            );
6699            Self::encode_full_zip(
6700                column_idx,
6701                &field,
6702                compression_strategy.as_ref(),
6703                data_block,
6704                repdef,
6705                row_number,
6706                num_rows,
6707            )
6708        } else {
6709            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()))
6710        }
6711    }
6712
6713    /// Rebuilds dictionary chunks whose values array is empty.
6714    ///
6715    /// Such chunks are entirely null and retain their key validity until rep/def has been
6716    /// recorded.  At flush time their keys are zeroed and attached to a neighboring non-empty
6717    /// dictionary.  If every chunk is empty, one non-null placeholder value makes key zero valid.
6718    /// Rep/def retains the logical nullness, so these replacement keys are never exposed.
6719    fn rebuild_empty_dictionary_chunks(arrays: Vec<ArrayRef>) -> Result<Vec<ArrayRef>> {
6720        if !arrays.iter().any(|array| {
6721            array
6722                .as_any_dictionary_opt()
6723                .is_some_and(|dictionary| dictionary.values().is_empty())
6724        }) {
6725            return Ok(arrays);
6726        }
6727
6728        let zeroed = |data_type: &DataType, len: usize| {
6729            new_null_array(data_type, len)
6730                .to_data()
6731                .into_builder()
6732                .nulls(None)
6733                .build()
6734                .map(make_array)
6735        };
6736        let rebuild =
6737            |keys: Vec<ArrayRef>, data_type: &DataType, values: ArrayRef| -> Result<ArrayRef> {
6738                let keys = keys.iter().map(|keys| keys.as_ref()).collect::<Vec<_>>();
6739                let keys = arrow_select::concat::concat(&keys)?;
6740                let data = keys
6741                    .to_data()
6742                    .into_builder()
6743                    .data_type(data_type.clone())
6744                    .child_data(vec![values.to_data()])
6745                    .build()?;
6746                Ok(make_array(data))
6747            };
6748
6749        let mut rebuilt = Vec::with_capacity(arrays.len());
6750        let mut pending_keys = Vec::with_capacity(arrays.len());
6751        let mut empty_data_type = None;
6752        for array in arrays {
6753            let Some(dictionary) = array.as_any_dictionary_opt() else {
6754                return Err(Error::invalid_input_source(
6755                    "Cannot mix dictionary and non-dictionary chunks".into(),
6756                ));
6757            };
6758            if dictionary.values().is_empty() {
6759                pending_keys.push(zeroed(dictionary.keys().data_type(), array.len())?);
6760                empty_data_type.get_or_insert_with(|| array.data_type().clone());
6761            } else if pending_keys.is_empty() {
6762                rebuilt.push(array);
6763            } else {
6764                pending_keys.push(make_array(dictionary.keys().to_data()));
6765                rebuilt.push(rebuild(
6766                    std::mem::take(&mut pending_keys),
6767                    array.data_type(),
6768                    dictionary.values().clone(),
6769                )?);
6770            }
6771        }
6772
6773        if pending_keys.is_empty() {
6774            return Ok(rebuilt);
6775        }
6776        if let Some(array) = rebuilt.pop() {
6777            let Some(dictionary) = array.as_any_dictionary_opt() else {
6778                return Err(Error::invalid_input_source(
6779                    "Cannot mix dictionary and non-dictionary chunks".into(),
6780                ));
6781            };
6782            let mut keys = Vec::with_capacity(pending_keys.len() + 1);
6783            keys.push(make_array(dictionary.keys().to_data()));
6784            keys.append(&mut pending_keys);
6785            rebuilt.push(rebuild(
6786                keys,
6787                array.data_type(),
6788                dictionary.values().clone(),
6789            )?);
6790        } else {
6791            let data_type = empty_data_type.ok_or_else(|| {
6792                Error::internal("Missing data type for an empty dictionary chunk")
6793            })?;
6794            let DataType::Dictionary(_, value_type) = &data_type else {
6795                return Err(Error::internal(format!(
6796                    "Expected dictionary data type, got {data_type}"
6797                )));
6798            };
6799            rebuilt.push(rebuild(pending_keys, &data_type, zeroed(value_type, 1)?)?);
6800        }
6801        Ok(rebuilt)
6802    }
6803
6804    // Creates encode tasks, consuming all buffered data
6805    fn do_flush(
6806        &mut self,
6807        arrays: Vec<ArrayRef>,
6808        repdefs: Vec<RepDefBuilder>,
6809        row_number: u64,
6810        num_rows: u64,
6811    ) -> Result<Vec<EncodeTask>> {
6812        let arrays = Self::rebuild_empty_dictionary_chunks(arrays)?;
6813        DataBlock::validate_arrays(&arrays, &self.field.name)?;
6814        let num_values = arrays.iter().map(|arr| arr.len() as u64).sum();
6815        let is_simple_validity = repdefs.iter().all(|rd| rd.is_simple_validity());
6816        let has_repdef_info = repdefs.iter().any(|rd| !rd.is_empty());
6817        let normalized = RepDefBuilder::normalize(repdefs);
6818        let plan_ctx = PrimitivePlanContext {
6819            column_idx: self.column_index,
6820            field: &self.field,
6821            encoding_metadata: &self.encoding_metadata,
6822        };
6823        let mut pages = None;
6824        for page_encoding in self.page_encodings.iter() {
6825            if let Some(planned) = page_encoding.behavior.try_plan_pages(
6826                &plan_ctx,
6827                &arrays,
6828                &normalized,
6829                row_number,
6830                num_rows,
6831                num_values,
6832            )? {
6833                pages = Some(planned);
6834                break;
6835            }
6836        }
6837        let pages = pages.ok_or_else(|| {
6838            Error::invalid_input_source(
6839                format!(
6840                    "No primitive page planner supports field '{}'",
6841                    self.field.name
6842                )
6843                .into(),
6844            )
6845        })?;
6846
6847        let mut tasks = Vec::with_capacity(pages.len());
6848        let ctx = PrimitiveEncodeContext {
6849            column_idx: self.column_index,
6850            field: self.field.clone(),
6851            encoding_metadata: self.encoding_metadata.clone(),
6852            is_simple_validity,
6853            has_repdef_info,
6854        };
6855        for page in pages {
6856            let ctx = ctx.clone();
6857            let page_encodings = self.page_encodings.clone();
6858            let task =
6859                spawn_cpu(move || Self::encode_page(page_encodings.as_ref(), &ctx, page)).boxed();
6860            tasks.push(task);
6861        }
6862        Ok(tasks)
6863    }
6864
6865    fn extract_validity_buf(
6866        array: Arc<dyn Array>,
6867        repdef: &mut RepDefBuilder,
6868        keep_original_array: bool,
6869    ) -> Result<Arc<dyn Array>> {
6870        if let Some(validity) = array.nulls() {
6871            if keep_original_array {
6872                repdef.add_validity_bitmap(validity.clone());
6873            } else {
6874                repdef.add_validity_bitmap(deep_copy_nulls(Some(validity)).unwrap());
6875            }
6876            // Empty dictionaries have no valid key payload.  Keep the validity until rep/def is
6877            // grouped with the buffered page; `do_flush` then rebuilds the keys against either a
6878            // neighboring values array or an all-empty-page placeholder.
6879            if let Some(dictionary) = array.as_any_dictionary_opt()
6880                && dictionary.values().is_empty()
6881            {
6882                return Ok(array);
6883            }
6884            let data_no_nulls = array.to_data().into_builder().nulls(None).build()?;
6885            Ok(make_array(data_no_nulls))
6886        } else {
6887            repdef.add_no_null(array.len());
6888            Ok(array)
6889        }
6890    }
6891
6892    fn extract_validity(
6893        mut array: Arc<dyn Array>,
6894        repdef: &mut RepDefBuilder,
6895        keep_original_array: bool,
6896    ) -> Result<Arc<dyn Array>> {
6897        match array.data_type() {
6898            DataType::Null => {
6899                repdef.add_validity_bitmap(NullBuffer::new(BooleanBuffer::new_unset(array.len())));
6900                Ok(array)
6901            }
6902            DataType::Dictionary(_, _) => {
6903                array = dict::normalize_dict_nulls(array)?;
6904                array = dict::clear_out_of_range_null_keys(array)?;
6905                Self::extract_validity_buf(array, repdef, keep_original_array)
6906            }
6907            // Extract our validity buf but NOT any child validity bufs. (they will be encoded in
6908            // as part of the values).  Note: for FSL we do not use repdef.add_fsl because we do
6909            // NOT want to increase the repdef depth.
6910            //
6911            // This would be quite catasrophic for something like vector embeddings.  Imagine we
6912            // had thousands of vectors and some were null but no vector contained null items.  If
6913            // we treated the vectors (primitive FSL) like we treat structural FSL we would end up
6914            // with a rep/def value for every single item in the vector.
6915            _ => Self::extract_validity_buf(array, repdef, keep_original_array),
6916        }
6917    }
6918}
6919
6920impl PrimitivePageEncodingBehavior for RejectSparsePrimitiveEncoding {
6921    fn validate_field(&self, field: &Field, metadata: &HashMap<String, String>) -> Result<()> {
6922        if metadata
6923            .get(STRUCTURAL_ENCODING_META_KEY)
6924            .is_some_and(|requested| requested.eq_ignore_ascii_case(STRUCTURAL_ENCODING_SPARSE))
6925        {
6926            return Err(Error::invalid_input_source(
6927                format!(
6928                    "Field '{}' requests sparse structural encoding, which is not enabled by the selected file format",
6929                    field.name
6930                )
6931                .into(),
6932            ));
6933        }
6934        Ok(())
6935    }
6936}
6937
6938fn plan_dense_primitive_pages(
6939    arrays: &[ArrayRef],
6940    normalized: &NormalizedStructuralPlan,
6941    row_number: u64,
6942    num_rows: u64,
6943    num_values: u64,
6944) -> Result<Vec<PrimitivePageData>> {
6945    let (repdef, miniblock_repdef_budget) = normalized.serialize_with_miniblock_repdef_budget(
6946        miniblock::max_repdef_levels_per_chunk,
6947        num_rows,
6948        num_values,
6949    )?;
6950    PrimitiveStructuralEncoder::split_pages_for_miniblock_repdef_budget(
6951        arrays.to_vec(),
6952        repdef,
6953        miniblock_repdef_budget,
6954        row_number,
6955        num_rows,
6956    )
6957}
6958
6959impl PrimitivePageEncodingBehavior for DenseU16PrimitiveEncoding {
6960    fn try_plan_pages(
6961        &self,
6962        _ctx: &PrimitivePlanContext<'_>,
6963        arrays: &[ArrayRef],
6964        normalized: &NormalizedStructuralPlan,
6965        row_number: u64,
6966        num_rows: u64,
6967        num_values: u64,
6968    ) -> Result<Option<Vec<PrimitivePageData>>> {
6969        Ok(Some(plan_dense_primitive_pages(
6970            arrays, normalized, row_number, num_rows, num_values,
6971        )?))
6972    }
6973
6974    fn try_encode_page(
6975        &self,
6976        ctx: &PrimitiveEncodeContext,
6977        page: PrimitivePageData,
6978    ) -> Result<PrimitiveEncodeAttempt> {
6979        if !matches!(&page.structure, PrimitivePageStructure::Dense { .. }) {
6980            return Ok(PrimitiveEncodeAttempt::Unhandled(page));
6981        }
6982        Ok(PrimitiveEncodeAttempt::Encoded(
6983            PrimitiveStructuralEncoder::encode_dense_page(
6984                ctx.clone(),
6985                page,
6986                self.compression.clone(),
6987                MiniblockChunkSize::U16,
6988                ComplexNullEncoding::RawLevels,
6989                FixedWidthDictionaryEncoding::Exclude64Bit,
6990            )?,
6991        ))
6992    }
6993}
6994
6995impl PrimitivePageEncodingBehavior for DenseU32PrimitiveEncoding {
6996    fn try_plan_pages(
6997        &self,
6998        _ctx: &PrimitivePlanContext<'_>,
6999        arrays: &[ArrayRef],
7000        normalized: &NormalizedStructuralPlan,
7001        row_number: u64,
7002        num_rows: u64,
7003        num_values: u64,
7004    ) -> Result<Option<Vec<PrimitivePageData>>> {
7005        Ok(Some(plan_dense_primitive_pages(
7006            arrays, normalized, row_number, num_rows, num_values,
7007        )?))
7008    }
7009
7010    fn try_encode_page(
7011        &self,
7012        ctx: &PrimitiveEncodeContext,
7013        page: PrimitivePageData,
7014    ) -> Result<PrimitiveEncodeAttempt> {
7015        if !matches!(&page.structure, PrimitivePageStructure::Dense { .. }) {
7016            return Ok(PrimitiveEncodeAttempt::Unhandled(page));
7017        }
7018        Ok(PrimitiveEncodeAttempt::Encoded(
7019            PrimitiveStructuralEncoder::encode_dense_page(
7020                ctx.clone(),
7021                page,
7022                self.compression.clone(),
7023                MiniblockChunkSize::U32,
7024                ComplexNullEncoding::CompressedLevels,
7025                FixedWidthDictionaryEncoding::Include64Bit,
7026            )?,
7027        ))
7028    }
7029}
7030
7031impl PrimitivePageEncodingBehavior for SparsePrimitiveEncoding {
7032    fn try_plan_pages(
7033        &self,
7034        ctx: &PrimitivePlanContext<'_>,
7035        arrays: &[ArrayRef],
7036        normalized: &NormalizedStructuralPlan,
7037        row_number: u64,
7038        num_rows: u64,
7039        num_values: u64,
7040    ) -> Result<Option<Vec<PrimitivePageData>>> {
7041        let requested_encoding = ctx.encoding_metadata.get(STRUCTURAL_ENCODING_META_KEY);
7042        let requests_sparse = requested_encoding
7043            .is_some_and(|requested| requested.eq_ignore_ascii_case(STRUCTURAL_ENCODING_SPARSE));
7044        if requests_sparse {
7045            let plan = sparse::writer::plan(normalized, num_values)?;
7046            if sparse::writer::uses_constant_layout(&plan, ctx.field) {
7047                return Ok(None);
7048            }
7049            return Ok(Some(vec![PrimitivePageData {
7050                arrays: arrays.to_vec(),
7051                structure: PrimitivePageStructure::Sparse {
7052                    plan,
7053                    prepared_values: None,
7054                },
7055                row_number,
7056                num_rows,
7057            }]));
7058        }
7059
7060        let (_, miniblock_repdef_budget) = normalized.serialize_with_miniblock_repdef_budget(
7061            miniblock::max_repdef_levels_per_chunk,
7062            num_rows,
7063            num_values,
7064        )?;
7065        let automatic_sparse = layout::select_automatic_sparse(
7066            requested_encoding.map(String::as_str),
7067            &miniblock_repdef_budget,
7068            || {
7069                let data = DataBlock::from_arrays(arrays, num_values);
7070                if !sparse::writer::supports_value_block(&data) {
7071                    return Ok(None);
7072                }
7073                let prepared_values = match sparse::writer::prepare_values(
7074                    ctx.field,
7075                    self.compression.as_ref(),
7076                    data,
7077                    MiniblockChunkSize::U32,
7078                ) {
7079                    Ok(prepared_values) => prepared_values,
7080                    Err(error) => {
7081                        debug!(
7082                            "Keeping column {} on its dense structural path because sparse value preparation is unavailable: {}",
7083                            ctx.column_idx, error
7084                        );
7085                        return Ok(None);
7086                    }
7087                };
7088                let plan = sparse::writer::plan(normalized, num_values)?;
7089                if sparse::writer::uses_constant_layout(&plan, ctx.field) {
7090                    return Ok(None);
7091                }
7092                Ok(Some((plan, prepared_values)))
7093            },
7094        )?;
7095        Ok(automatic_sparse.map(|(plan, prepared_values)| {
7096            vec![PrimitivePageData {
7097                arrays: arrays.to_vec(),
7098                structure: PrimitivePageStructure::Sparse {
7099                    plan,
7100                    prepared_values: Some(prepared_values),
7101                },
7102                row_number,
7103                num_rows,
7104            }]
7105        }))
7106    }
7107
7108    fn try_encode_page(
7109        &self,
7110        ctx: &PrimitiveEncodeContext,
7111        page: PrimitivePageData,
7112    ) -> Result<PrimitiveEncodeAttempt> {
7113        if !matches!(&page.structure, PrimitivePageStructure::Sparse { .. }) {
7114            return Ok(PrimitiveEncodeAttempt::Unhandled(page));
7115        }
7116        let PrimitivePageData {
7117            arrays,
7118            structure:
7119                PrimitivePageStructure::Sparse {
7120                    plan,
7121                    prepared_values,
7122                },
7123            row_number,
7124            num_rows,
7125        } = page
7126        else {
7127            unreachable!()
7128        };
7129        let num_values = arrays.iter().map(|array| array.len() as u64).sum();
7130        log::debug!(
7131            "Encoding column {} with {} visible items ({} rows) using sparse layout",
7132            ctx.column_idx,
7133            num_values,
7134            num_rows
7135        );
7136        Ok(PrimitiveEncodeAttempt::Encoded(
7137            sparse::writer::encode_page(
7138                ctx.column_idx,
7139                &ctx.field,
7140                self.compression.as_ref(),
7141                prepared_values.map_or_else(
7142                    || {
7143                        sparse::writer::SparseValueInput::Unprepared(DataBlock::from_arrays(
7144                            &arrays, num_values,
7145                        ))
7146                    },
7147                    sparse::writer::SparseValueInput::Prepared,
7148                ),
7149                plan,
7150                row_number,
7151                num_rows,
7152                MiniblockChunkSize::U32,
7153            )?,
7154        ))
7155    }
7156}
7157
7158impl PrimitivePageEncodingBehavior for ConstantPrimitiveEncoding {
7159    fn try_encode_page(
7160        &self,
7161        ctx: &PrimitiveEncodeContext,
7162        page: PrimitivePageData,
7163    ) -> Result<PrimitiveEncodeAttempt> {
7164        let PrimitivePageStructure::Dense { repdef, .. } = &page.structure else {
7165            return Ok(PrimitiveEncodeAttempt::Unhandled(page));
7166        };
7167        let num_values: u64 = page.arrays.iter().map(|array| array.len() as u64).sum();
7168        if num_values == 0 {
7169            return Ok(PrimitiveEncodeAttempt::Unhandled(page));
7170        }
7171        let leaf_validity = PrimitiveStructuralEncoder::leaf_validity(repdef, num_values as usize)?;
7172        if leaf_validity
7173            .as_ref()
7174            .is_some_and(|validity| validity.count_set_bits() == 0)
7175            || matches!(ctx.field.data_type(), DataType::Struct(fields) if fields.is_empty())
7176        {
7177            return Ok(PrimitiveEncodeAttempt::Unhandled(page));
7178        }
7179        let Some(scalar) =
7180            PrimitiveStructuralEncoder::find_constant_scalar(&page.arrays, leaf_validity.as_ref())?
7181        else {
7182            return Ok(PrimitiveEncodeAttempt::Unhandled(page));
7183        };
7184        let PrimitivePageData {
7185            structure: PrimitivePageStructure::Dense { repdef, .. },
7186            row_number,
7187            num_rows,
7188            ..
7189        } = page
7190        else {
7191            unreachable!()
7192        };
7193        log::debug!(
7194            "Encoding column {} with {} items ({} rows) using constant layout",
7195            ctx.column_idx,
7196            num_values,
7197            num_rows
7198        );
7199        Ok(PrimitiveEncodeAttempt::Encoded(
7200            constant::encode_constant_page(ctx.column_idx, scalar, repdef, row_number, num_rows)?,
7201        ))
7202    }
7203}
7204
7205impl FieldEncoder for PrimitiveStructuralEncoder {
7206    // Buffers data, if there is enough to write a page then we create an encode task
7207    fn maybe_encode(
7208        &mut self,
7209        array: ArrayRef,
7210        _external_buffers: &mut OutOfLineBuffers,
7211        mut repdef: RepDefBuilder,
7212        row_number: u64,
7213        num_rows: u64,
7214    ) -> Result<Vec<EncodeTask>> {
7215        let array = Self::extract_validity(array, &mut repdef, self.keep_original_array)?;
7216        self.accumulated_repdefs.push(repdef);
7217
7218        if let Some((arrays, row_number, num_rows)) =
7219            self.accumulation_queue.insert(array, row_number, num_rows)
7220        {
7221            let accumulated_repdefs = std::mem::take(&mut self.accumulated_repdefs);
7222            Ok(self.do_flush(arrays, accumulated_repdefs, row_number, num_rows)?)
7223        } else {
7224            Ok(vec![])
7225        }
7226    }
7227
7228    // If there is any data left in the buffer then create an encode task from it
7229    fn flush(&mut self, _external_buffers: &mut OutOfLineBuffers) -> Result<Vec<EncodeTask>> {
7230        if let Some((arrays, row_number, num_rows)) = self.accumulation_queue.flush() {
7231            let accumulated_repdefs = std::mem::take(&mut self.accumulated_repdefs);
7232            Ok(self.do_flush(arrays, accumulated_repdefs, row_number, num_rows)?)
7233        } else {
7234            Ok(vec![])
7235        }
7236    }
7237
7238    fn num_columns(&self) -> u32 {
7239        1
7240    }
7241
7242    fn finish(
7243        &mut self,
7244        _external_buffers: &mut OutOfLineBuffers,
7245    ) -> BoxFuture<'_, Result<Vec<crate::encoder::EncodedColumn>>> {
7246        std::future::ready(Ok(vec![EncodedColumn::default()])).boxed()
7247    }
7248}
7249
7250#[cfg(test)]
7251#[allow(clippy::single_range_in_vec_init)]
7252mod tests {
7253    use super::{
7254        ChunkInstructions, DataBlock, DecodeMiniBlockTask, DecodePageTask, FixedFullZipDecodeTask,
7255        FixedPerValueDecompressor, FixedWidthDataBlock, FixedWidthDictionaryEncoding,
7256        FullZipCacheableState, FullZipDecodeDetails, FullZipDecodeTaskItem, FullZipReadSource,
7257        FullZipRepIndexDetails, FullZipScheduler, LazyLevels, LevelCodec, LevelCursor, LevelPlan,
7258        MiniBlockChunk, MiniBlockChunkIndex, MiniBlockCompressed, MiniblockChunkSize,
7259        PerValueDataBlock, PerValueDecompressor, PreambleAction, RunEndsBuilder, RunPosition,
7260        RunStorage, StructuralPageScheduler, VariableFullZipDecoder, dense_levels_from_block,
7261        validate_complex_all_null_levels,
7262    };
7263    use crate::buffer::LanceBuffer;
7264    use crate::compression::{
7265        BlockCompressor, DefaultDecompressionStrategy, MiniBlockDecompressor,
7266    };
7267    use crate::constants::{
7268        COMPRESSION_LEVEL_META_KEY, COMPRESSION_META_KEY, DICT_VALUES_COMPRESSION_LEVEL_META_KEY,
7269        DICT_VALUES_COMPRESSION_META_KEY, STRUCTURAL_ENCODING_META_KEY,
7270        STRUCTURAL_ENCODING_MINIBLOCK,
7271    };
7272    use crate::data::BlockInfo;
7273    use crate::decoder::{PageEncoding, StructuralFieldDecoder};
7274    use crate::encodings::logical::primitive::fullzip::PerValueCompressor;
7275    use crate::encodings::logical::primitive::{
7276        ChunkDrainInstructions, LoadedChunk, PrimitiveStructuralEncoder,
7277        StructuralPrimitiveFieldDecoder,
7278    };
7279    use crate::encodings::physical::rle::{RleDecompressor, RleEncoder, RleRuns, RunLengthWidth};
7280    use crate::encodings::physical::value::{ValueDecompressor, ValueEncoder};
7281    use crate::format::ProtobufUtils21;
7282    use crate::format::pb21;
7283    use crate::format::pb21::compressive_encoding::Compression;
7284    use crate::repdef::build_control_word_iterator;
7285    use crate::testing::TestEncoding;
7286    use crate::testing::{TestCases, check_round_trip_encoding_of_data};
7287    use arrow_array::{
7288        Array, ArrayRef, DictionaryArray, FixedSizeListArray, Float32Array, Int8Array,
7289        PrimitiveArray, StringArray, UInt8Array, make_array, new_null_array, types::Int32Type,
7290    };
7291    use arrow_buffer::{BooleanBuffer, NullBuffer, ScalarBuffer};
7292    use arrow_schema::{DataType, Field as ArrowField};
7293    use std::collections::HashMap;
7294    use std::{collections::VecDeque, sync::Arc};
7295
7296    #[test]
7297    fn test_is_narrow() {
7298        let int8_array = Int8Array::from(vec![1, 2, 3]);
7299        let array_ref: ArrayRef = Arc::new(int8_array);
7300        let block = DataBlock::from_array(array_ref);
7301
7302        assert!(PrimitiveStructuralEncoder::is_narrow(&block));
7303
7304        let string_array = StringArray::from(vec![Some("hello"), Some("world")]);
7305        let block = DataBlock::from_array(string_array);
7306        assert!(PrimitiveStructuralEncoder::is_narrow(&block));
7307
7308        let string_array = StringArray::from(vec![
7309            Some("hello world".repeat(100)),
7310            Some("world".to_string()),
7311        ]);
7312        let block = DataBlock::from_array(string_array);
7313        assert!((!PrimitiveStructuralEncoder::is_narrow(&block)));
7314    }
7315
7316    fn valued_dictionary() -> ArrayRef {
7317        Arc::new(
7318            DictionaryArray::<Int32Type>::try_new(
7319                PrimitiveArray::<Int32Type>::from(vec![0]),
7320                Arc::new(StringArray::from(vec!["a"])),
7321            )
7322            .unwrap(),
7323        )
7324    }
7325
7326    fn empty_dictionary() -> ArrayRef {
7327        new_null_array(
7328            &DataType::Dictionary(Box::new(DataType::Int32), Box::new(DataType::Utf8)),
7329            1,
7330        )
7331    }
7332
7333    fn null_valued_dictionary() -> ArrayRef {
7334        Arc::new(
7335            DictionaryArray::<Int32Type>::try_new(
7336                PrimitiveArray::<Int32Type>::from(vec![0]),
7337                Arc::new(StringArray::from(vec![None::<&str>])),
7338            )
7339            .unwrap(),
7340        )
7341    }
7342
7343    fn sliced_empty_dictionary_with_hidden_key_payload() -> ArrayRef {
7344        let keys = PrimitiveArray::<Int32Type>::new(
7345            ScalarBuffer::from(vec![5, 7, 9]),
7346            Some(NullBuffer::new(BooleanBuffer::new_unset(3))),
7347        );
7348        let values = Arc::new(StringArray::from(Vec::<&str>::new())) as ArrayRef;
7349        let dictionary = DictionaryArray::<Int32Type>::try_new(keys, values).unwrap();
7350        Arc::new(dictionary.slice(1, 1))
7351    }
7352
7353    #[rstest::rstest]
7354    #[case::empty_after_value(vec![valued_dictionary(), empty_dictionary()])]
7355    #[case::empty_before_value(vec![empty_dictionary(), valued_dictionary()])]
7356    #[case::null_value_after_value(vec![valued_dictionary(), null_valued_dictionary()])]
7357    #[case::hidden_sliced_after_value(vec![
7358        valued_dictionary(),
7359        sliced_empty_dictionary_with_hidden_key_payload(),
7360    ])]
7361    #[tokio::test]
7362    async fn test_mixed_valued_and_all_null_dictionary_chunks(#[case] dictionaries: Vec<ArrayRef>) {
7363        check_round_trip_encoding_of_data(
7364            dictionaries,
7365            &TestCases::default()
7366                .with_structural_encodings()
7367                .with_page_sizes(vec![4096]),
7368            HashMap::new(),
7369        )
7370        .await;
7371    }
7372
7373    #[tokio::test]
7374    async fn test_sliced_empty_dictionary_with_hidden_key_payload() {
7375        check_round_trip_encoding_of_data(
7376            vec![sliced_empty_dictionary_with_hidden_key_payload()],
7377            &TestCases::default()
7378                .with_structural_encodings()
7379                .with_page_sizes(vec![4096]),
7380            HashMap::new(),
7381        )
7382        .await;
7383    }
7384
7385    #[test]
7386    fn test_primitive_decoder_empty_page_queue_returns_error() {
7387        let field = Arc::new(ArrowField::new("vector", DataType::Float32, true));
7388        let mut decoder = StructuralPrimitiveFieldDecoder::new(&field, false);
7389
7390        let err = decoder.drain(1).unwrap_err();
7391        assert!(
7392            matches!(&err, lance_core::Error::Internal { .. }),
7393            "expected internal error, got: {err:?}"
7394        );
7395        let message = err.to_string();
7396        for expected in [
7397            "Primitive decoder missing page decoder",
7398            "field 'vector'",
7399            "data_type=Float32",
7400            "requested_rows=1",
7401            "remaining_rows=1",
7402            "rows_drained_in_current=0",
7403            "queued_pages=0",
7404        ] {
7405            assert!(
7406                message.contains(expected),
7407                "expected error to contain {expected:?}, got: {message}"
7408            );
7409        }
7410    }
7411
7412    #[test]
7413    fn test_fullzip_fixed_rejects_non_byte_aligned_values() {
7414        let fixed = FixedWidthDataBlock {
7415            data: LanceBuffer::from(vec![0_u8]),
7416            bits_per_value: 1,
7417            num_values: 8,
7418            block_info: BlockInfo::new(),
7419        };
7420        let repdef = build_control_word_iterator(None, 0, None, 0, u16::MAX, 8);
7421
7422        let Err(err) = PrimitiveStructuralEncoder::serialize_full_zip_fixed(fixed, repdef, 8)
7423        else {
7424            panic!("expected full-zip to reject 1-bit fixed-width values");
7425        };
7426        assert!(
7427            err.to_string().contains("byte aligned"),
7428            "unexpected error: {err}"
7429        );
7430    }
7431
7432    fn decode_fixed_fullzip_no_levels(
7433        decompressor: Arc<dyn FixedPerValueDecompressor>,
7434        data: Vec<FullZipDecodeTaskItem>,
7435        num_rows: usize,
7436        bytes_per_value: usize,
7437    ) -> DataBlock {
7438        Box::new(FixedFullZipDecodeTask {
7439            details: Arc::new(FullZipDecodeDetails {
7440                value_decompressor: PerValueDecompressor::Fixed(decompressor),
7441                def_meaning: Arc::from([]),
7442                ctrl_word_parser: crate::repdef::ControlWordParser::new(0, 0),
7443                max_rep: 0,
7444                max_visible_def: u16::MAX,
7445            }),
7446            data,
7447            num_rows,
7448            bytes_per_value,
7449        })
7450        .decode()
7451        .unwrap()
7452        .data
7453    }
7454
7455    #[test]
7456    fn test_fixed_fullzip_decode_preallocates_exact_output_size() {
7457        #[derive(Debug)]
7458        struct IdentityFixedDecompressor;
7459
7460        impl FixedPerValueDecompressor for IdentityFixedDecompressor {
7461            fn decompress(
7462                &self,
7463                data: FixedWidthDataBlock,
7464                num_rows: u64,
7465            ) -> crate::Result<DataBlock> {
7466                assert_eq!(data.num_values, num_rows);
7467                Ok(DataBlock::FixedWidth(data))
7468            }
7469
7470            fn bits_per_value(&self) -> u64 {
7471                32
7472            }
7473
7474            fn decoded_size_bytes(&self, num_values: u64) -> Option<u64> {
7475                num_values.checked_mul(4)
7476            }
7477        }
7478
7479        let make_item = |num_rows: u64| FullZipDecodeTaskItem {
7480            data: PerValueDataBlock::Fixed(FixedWidthDataBlock {
7481                data: LanceBuffer::from(vec![7_u8; num_rows as usize * 4]),
7482                bits_per_value: 32,
7483                num_values: num_rows,
7484                block_info: BlockInfo::new(),
7485            }),
7486            rows_in_buf: num_rows,
7487        };
7488
7489        let num_rows = 512;
7490        let decoded = decode_fixed_fullzip_no_levels(
7491            Arc::new(IdentityFixedDecompressor),
7492            vec![make_item(128), make_item(384)],
7493            num_rows,
7494            4,
7495        );
7496        let values = decoded.as_fixed_width_ref().unwrap();
7497        let expected_size = num_rows * 4;
7498        assert_eq!(values.data.len(), expected_size);
7499        assert_eq!(values.data.clone().into_buffer().capacity(), expected_size);
7500    }
7501
7502    #[test]
7503    fn test_fixed_fullzip_decode_falls_back_when_output_size_is_not_exact() {
7504        #[derive(Debug)]
7505        struct FallbackFixedDecompressor;
7506
7507        impl FixedPerValueDecompressor for FallbackFixedDecompressor {
7508            fn decompress(
7509                &self,
7510                data: FixedWidthDataBlock,
7511                num_rows: u64,
7512            ) -> crate::Result<DataBlock> {
7513                assert_eq!(data.num_values, num_rows);
7514                Ok(DataBlock::FixedWidth(FixedWidthDataBlock {
7515                    data: LanceBuffer::from(vec![7_u8; num_rows as usize * 4]),
7516                    bits_per_value: 32,
7517                    num_values: num_rows,
7518                    block_info: BlockInfo::new(),
7519                }))
7520            }
7521
7522            fn bits_per_value(&self) -> u64 {
7523                // This deliberately cannot be multiplied by num_rows. If FullZip treats
7524                // bits_per_value as an exact decoded-size estimate, decoding will fail.
7525                u64::MAX - 7
7526            }
7527        }
7528
7529        let num_rows = 2;
7530        let decoded = decode_fixed_fullzip_no_levels(
7531            Arc::new(FallbackFixedDecompressor),
7532            vec![FullZipDecodeTaskItem {
7533                data: PerValueDataBlock::Fixed(FixedWidthDataBlock {
7534                    data: LanceBuffer::from(vec![0_u8; num_rows * 4]),
7535                    bits_per_value: 32,
7536                    num_values: num_rows as u64,
7537                    block_info: BlockInfo::new(),
7538                }),
7539                rows_in_buf: num_rows as u64,
7540            }],
7541            num_rows,
7542            4,
7543        );
7544        let values = decoded.as_fixed_width_ref().unwrap();
7545        assert_eq!(values.num_values, num_rows as u64);
7546        assert_eq!(values.data.len(), num_rows * 4);
7547    }
7548
7549    #[test]
7550    fn test_fixed_fullzip_real_fsl_preallocates_exact_output_size() {
7551        let num_rows = 64;
7552        let dimension = 32;
7553        let items = Arc::new(Float32Array::from_iter_values(
7554            (0..num_rows * dimension).map(|value| value as f32),
7555        ));
7556        let item_field = Arc::new(ArrowField::new("item", DataType::Float32, false));
7557        let sample = FixedSizeListArray::new(item_field, dimension as i32, items, None);
7558
7559        let (data, compression) = PerValueCompressor::compress(
7560            &ValueEncoder::default(),
7561            DataBlock::from_array(sample.clone()),
7562        )
7563        .unwrap();
7564        let Compression::FixedSizeList(fsl) = compression.compression.unwrap() else {
7565            panic!("expected fixed-size-list compression");
7566        };
7567        let decompressor = ValueDecompressor::from_fsl(fsl.as_ref()).unwrap();
7568        let expected_size = num_rows * dimension * size_of::<f32>();
7569        assert_eq!(
7570            FixedPerValueDecompressor::decoded_size_bytes(&decompressor, num_rows as u64),
7571            Some(expected_size as u64)
7572        );
7573
7574        let decoded = decode_fixed_fullzip_no_levels(
7575            Arc::new(decompressor),
7576            vec![FullZipDecodeTaskItem {
7577                data,
7578                rows_in_buf: num_rows as u64,
7579            }],
7580            num_rows,
7581            dimension * size_of::<f32>(),
7582        );
7583        let fsl = decoded.as_fixed_size_list_ref().unwrap();
7584        let values = fsl.child.as_fixed_width_ref().unwrap();
7585        assert_eq!(values.data.len(), expected_size);
7586        assert_eq!(values.data.clone().into_buffer().capacity(), expected_size);
7587
7588        let decoded_array = make_array(
7589            decoded
7590                .into_arrow(sample.data_type().clone(), true)
7591                .unwrap(),
7592        );
7593        assert_eq!(decoded_array.as_ref(), &sample);
7594    }
7595
7596    #[test]
7597    fn test_fixed_fullzip_nullable_fsl_uses_fallback_end_to_end() {
7598        #[derive(Debug)]
7599        struct NullableFslDecompressor {
7600            inner: ValueDecompressor,
7601        }
7602
7603        impl FixedPerValueDecompressor for NullableFslDecompressor {
7604            fn decompress(
7605                &self,
7606                data: FixedWidthDataBlock,
7607                num_rows: u64,
7608            ) -> crate::Result<DataBlock> {
7609                FixedPerValueDecompressor::decompress(&self.inner, data, num_rows)
7610            }
7611
7612            fn bits_per_value(&self) -> u64 {
7613                // FullZip must not use this physical row width as an exact decoded-size
7614                // estimate for the nullable, multi-buffer Arrow output.
7615                u64::MAX - 7
7616            }
7617
7618            fn decoded_size_bytes(&self, num_values: u64) -> Option<u64> {
7619                FixedPerValueDecompressor::decoded_size_bytes(&self.inner, num_values)
7620            }
7621        }
7622
7623        let num_rows = 64;
7624        let items = Arc::new(UInt8Array::from_iter(
7625            (0..num_rows).map(|value| (value % 3 != 0).then_some(value as u8)),
7626        ));
7627        let item_field = Arc::new(ArrowField::new("item", DataType::UInt8, true));
7628        let sample = FixedSizeListArray::new(item_field, 1, items, None);
7629
7630        let (data, compression) = PerValueCompressor::compress(
7631            &ValueEncoder::default(),
7632            DataBlock::from_array(sample.clone()),
7633        )
7634        .unwrap();
7635        let Compression::FixedSizeList(fsl) = compression.compression.unwrap() else {
7636            panic!("expected fixed-size-list compression");
7637        };
7638        let decompressor = NullableFslDecompressor {
7639            inner: ValueDecompressor::from_fsl(fsl.as_ref()).unwrap(),
7640        };
7641        assert_eq!(
7642            FixedPerValueDecompressor::decoded_size_bytes(&decompressor, num_rows as u64),
7643            None
7644        );
7645
7646        let decoded = decode_fixed_fullzip_no_levels(
7647            Arc::new(decompressor),
7648            vec![FullZipDecodeTaskItem {
7649                data,
7650                rows_in_buf: num_rows as u64,
7651            }],
7652            num_rows,
7653            2,
7654        );
7655        let decoded_array = make_array(
7656            decoded
7657                .into_arrow(sample.data_type().clone(), true)
7658                .unwrap(),
7659        );
7660        assert_eq!(decoded_array.as_ref(), &sample);
7661    }
7662
7663    #[test]
7664    fn test_miniblock_decode_uses_exact_fixed_width_output_size() {
7665        #[derive(Debug)]
7666        struct FixedWidthMiniBlockDecompressor;
7667
7668        impl MiniBlockDecompressor for FixedWidthMiniBlockDecompressor {
7669            fn decompress(
7670                &self,
7671                data: Vec<LanceBuffer>,
7672                num_values: u64,
7673            ) -> crate::Result<DataBlock> {
7674                assert_eq!(data.len(), 1);
7675                Ok(DataBlock::FixedWidth(FixedWidthDataBlock {
7676                    data: data.into_iter().next().unwrap(),
7677                    bits_per_value: 32,
7678                    num_values,
7679                    block_info: BlockInfo::new(),
7680                }))
7681            }
7682
7683            fn decoded_size_bytes(&self, num_values: u64) -> Option<u64> {
7684                num_values.checked_mul(4)
7685            }
7686        }
7687
7688        let num_rows = 512;
7689        let expected_size = num_rows * 4;
7690        let mut chunk_data = Vec::new();
7691        chunk_data.extend_from_slice(&0_u16.to_le_bytes());
7692        chunk_data.extend_from_slice(&(expected_size as u16).to_le_bytes());
7693        let header_padding =
7694            lance_core::utils::bit::pad_bytes::<{ super::MINIBLOCK_ALIGNMENT }>(chunk_data.len());
7695        chunk_data.resize(chunk_data.len() + header_padding, 0);
7696        chunk_data.resize(chunk_data.len() + expected_size as usize, 7);
7697
7698        let task = DecodeMiniBlockTask {
7699            rep_decompressor: None,
7700            def_decompressor: None,
7701            value_decompressor: Arc::new(FixedWidthMiniBlockDecompressor),
7702            dictionary_data: None,
7703            def_meaning: Arc::from([]),
7704            num_buffers: 1,
7705            max_visible_level: 0,
7706            instructions: vec![(
7707                ChunkDrainInstructions {
7708                    chunk_instructions: ChunkInstructions {
7709                        chunk_idx: 0,
7710                        preamble: PreambleAction::Absent,
7711                        rows_to_skip: 0,
7712                        rows_to_take: num_rows,
7713                        take_trailer: false,
7714                    },
7715                    rows_to_skip: 0,
7716                    rows_to_take: num_rows,
7717                    preamble_action: PreambleAction::Absent,
7718                },
7719                LoadedChunk {
7720                    byte_range: 0..chunk_data.len() as u64,
7721                    data: LanceBuffer::from(chunk_data),
7722                    items_in_chunk: num_rows,
7723                    chunk_idx: 0,
7724                },
7725            )],
7726            has_large_chunk: false,
7727        };
7728
7729        let decoded = Box::new(task).decode().unwrap();
7730        let values = decoded.data.as_fixed_width_ref().unwrap();
7731        assert_eq!(values.data.len(), expected_size as usize);
7732        assert_eq!(
7733            values.data.clone().into_buffer().capacity(),
7734            expected_size as usize
7735        );
7736    }
7737
7738    #[test]
7739    fn test_map_range() {
7740        // Null in the middle
7741        // [[A, B, C], [D, E], NULL, [F, G, H]]
7742        let rep = Some(vec![1, 0, 0, 1, 0, 1, 1, 0, 0]);
7743        let def = Some(vec![0, 0, 0, 0, 0, 1, 0, 0, 0]);
7744        let max_visible_def = 0;
7745        let total_items = 8;
7746        let max_rep = 1;
7747
7748        let check = |range, expected_item_range, expected_level_range| {
7749            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
7750                range,
7751                rep.as_ref(),
7752                def.as_ref(),
7753                max_rep,
7754                max_visible_def,
7755                total_items,
7756                PreambleAction::Absent,
7757            );
7758            assert_eq!(item_range, expected_item_range);
7759            assert_eq!(level_range, expected_level_range);
7760        };
7761
7762        check(0..1, 0..3, 0..3);
7763        check(1..2, 3..5, 3..5);
7764        check(2..3, 5..5, 5..6);
7765        check(3..4, 5..8, 6..9);
7766        check(0..2, 0..5, 0..5);
7767        check(1..3, 3..5, 3..6);
7768        check(2..4, 5..8, 5..9);
7769        check(0..3, 0..5, 0..6);
7770        check(1..4, 3..8, 3..9);
7771        check(0..4, 0..8, 0..9);
7772
7773        // Null at start
7774        // [NULL, [A, B], [C]]
7775        let rep = Some(vec![1, 1, 0, 1]);
7776        let def = Some(vec![1, 0, 0, 0]);
7777        let max_visible_def = 0;
7778        let total_items = 3;
7779
7780        let check = |range, expected_item_range, expected_level_range| {
7781            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
7782                range,
7783                rep.as_ref(),
7784                def.as_ref(),
7785                max_rep,
7786                max_visible_def,
7787                total_items,
7788                PreambleAction::Absent,
7789            );
7790            assert_eq!(item_range, expected_item_range);
7791            assert_eq!(level_range, expected_level_range);
7792        };
7793
7794        check(0..1, 0..0, 0..1);
7795        check(1..2, 0..2, 1..3);
7796        check(2..3, 2..3, 3..4);
7797        check(0..2, 0..2, 0..3);
7798        check(1..3, 0..3, 1..4);
7799        check(0..3, 0..3, 0..4);
7800
7801        // Null at end
7802        // [[A], [B, C], NULL]
7803        let rep = Some(vec![1, 1, 0, 1]);
7804        let def = Some(vec![0, 0, 0, 1]);
7805        let max_visible_def = 0;
7806        let total_items = 3;
7807
7808        let check = |range, expected_item_range, expected_level_range| {
7809            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
7810                range,
7811                rep.as_ref(),
7812                def.as_ref(),
7813                max_rep,
7814                max_visible_def,
7815                total_items,
7816                PreambleAction::Absent,
7817            );
7818            assert_eq!(item_range, expected_item_range);
7819            assert_eq!(level_range, expected_level_range);
7820        };
7821
7822        check(0..1, 0..1, 0..1);
7823        check(1..2, 1..3, 1..3);
7824        check(2..3, 3..3, 3..4);
7825        check(0..2, 0..3, 0..3);
7826        check(1..3, 1..3, 1..4);
7827        check(0..3, 0..3, 0..4);
7828
7829        // No nulls, with repetition
7830        // [[A, B], [C, D], [E, F]]
7831        let rep = Some(vec![1, 0, 1, 0, 1, 0]);
7832        let def: Option<&[u16]> = None;
7833        let max_visible_def = 0;
7834        let total_items = 6;
7835
7836        let check = |range, expected_item_range, expected_level_range| {
7837            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
7838                range,
7839                rep.as_ref(),
7840                def.as_ref(),
7841                max_rep,
7842                max_visible_def,
7843                total_items,
7844                PreambleAction::Absent,
7845            );
7846            assert_eq!(item_range, expected_item_range);
7847            assert_eq!(level_range, expected_level_range);
7848        };
7849
7850        check(0..1, 0..2, 0..2);
7851        check(1..2, 2..4, 2..4);
7852        check(2..3, 4..6, 4..6);
7853        check(0..2, 0..4, 0..4);
7854        check(1..3, 2..6, 2..6);
7855        check(0..3, 0..6, 0..6);
7856
7857        // No repetition, with nulls (this case is trivial)
7858        // [A, B, NULL, C]
7859        let rep: Option<&[u16]> = None;
7860        let def = Some(vec![0, 0, 1, 0]);
7861        let max_visible_def = 1;
7862        let total_items = 4;
7863
7864        let check = |range, expected_item_range, expected_level_range| {
7865            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
7866                range,
7867                rep.as_ref(),
7868                def.as_ref(),
7869                max_rep,
7870                max_visible_def,
7871                total_items,
7872                PreambleAction::Absent,
7873            );
7874            assert_eq!(item_range, expected_item_range);
7875            assert_eq!(level_range, expected_level_range);
7876        };
7877
7878        check(0..1, 0..1, 0..1);
7879        check(1..2, 1..2, 1..2);
7880        check(2..3, 2..3, 2..3);
7881        check(0..2, 0..2, 0..2);
7882        check(1..3, 1..3, 1..3);
7883        check(0..3, 0..3, 0..3);
7884
7885        // Tricky case, this chunk is a continuation and starts with a rep-index = 0
7886        // [[..., A] [B, C], NULL]
7887        //
7888        // What we do will depend on the preamble action
7889        let rep = Some(vec![0, 1, 0, 1]);
7890        let def = Some(vec![0, 0, 0, 1]);
7891        let max_visible_def = 0;
7892        let total_items = 3;
7893
7894        let check = |range, expected_item_range, expected_level_range| {
7895            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
7896                range,
7897                rep.as_ref(),
7898                def.as_ref(),
7899                max_rep,
7900                max_visible_def,
7901                total_items,
7902                PreambleAction::Take,
7903            );
7904            assert_eq!(item_range, expected_item_range);
7905            assert_eq!(level_range, expected_level_range);
7906        };
7907
7908        // If we are taking the preamble then the range must start at 0
7909        check(0..1, 0..3, 0..3);
7910        check(0..2, 0..3, 0..4);
7911
7912        let check = |range, expected_item_range, expected_level_range| {
7913            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
7914                range,
7915                rep.as_ref(),
7916                def.as_ref(),
7917                max_rep,
7918                max_visible_def,
7919                total_items,
7920                PreambleAction::Skip,
7921            );
7922            assert_eq!(item_range, expected_item_range);
7923            assert_eq!(level_range, expected_level_range);
7924        };
7925
7926        check(0..1, 1..3, 1..3);
7927        check(1..2, 3..3, 3..4);
7928        check(0..2, 1..3, 1..4);
7929
7930        // Another preamble case but now it doesn't end with a new list
7931        // [[..., A], NULL, [D, E]]
7932        //
7933        // What we do will depend on the preamble action
7934        let rep = Some(vec![0, 1, 1, 0]);
7935        let def = Some(vec![0, 1, 0, 0]);
7936        let max_visible_def = 0;
7937        let total_items = 4;
7938
7939        let check = |range, expected_item_range, expected_level_range| {
7940            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
7941                range,
7942                rep.as_ref(),
7943                def.as_ref(),
7944                max_rep,
7945                max_visible_def,
7946                total_items,
7947                PreambleAction::Take,
7948            );
7949            assert_eq!(item_range, expected_item_range);
7950            assert_eq!(level_range, expected_level_range);
7951        };
7952
7953        // If we are taking the preamble then the range must start at 0
7954        check(0..1, 0..1, 0..2);
7955        check(0..2, 0..3, 0..4);
7956
7957        let check = |range, expected_item_range, expected_level_range| {
7958            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
7959                range,
7960                rep.as_ref(),
7961                def.as_ref(),
7962                max_rep,
7963                max_visible_def,
7964                total_items,
7965                PreambleAction::Skip,
7966            );
7967            assert_eq!(item_range, expected_item_range);
7968            assert_eq!(level_range, expected_level_range);
7969        };
7970
7971        // If we are taking the preamble then the range must start at 0
7972        check(0..1, 1..1, 1..2);
7973        check(1..2, 1..3, 2..4);
7974        check(0..2, 1..3, 1..4);
7975
7976        // Now a preamble case without any definition levels
7977        // [[..., A] [B, C], [D]]
7978        let rep = Some(vec![0, 1, 0, 1]);
7979        let def: Option<Vec<u16>> = None;
7980        let max_visible_def = 0;
7981        let total_items = 4;
7982
7983        let check = |range, expected_item_range, expected_level_range| {
7984            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
7985                range,
7986                rep.as_ref(),
7987                def.as_ref(),
7988                max_rep,
7989                max_visible_def,
7990                total_items,
7991                PreambleAction::Take,
7992            );
7993            assert_eq!(item_range, expected_item_range);
7994            assert_eq!(level_range, expected_level_range);
7995        };
7996
7997        // If we are taking the preamble then the range must start at 0
7998        check(0..1, 0..3, 0..3);
7999        check(0..2, 0..4, 0..4);
8000
8001        let check = |range, expected_item_range, expected_level_range| {
8002            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
8003                range,
8004                rep.as_ref(),
8005                def.as_ref(),
8006                max_rep,
8007                max_visible_def,
8008                total_items,
8009                PreambleAction::Skip,
8010            );
8011            assert_eq!(item_range, expected_item_range);
8012            assert_eq!(level_range, expected_level_range);
8013        };
8014
8015        check(0..1, 1..3, 1..3);
8016        check(1..2, 3..4, 3..4);
8017        check(0..2, 1..4, 1..4);
8018
8019        // If we have nested lists then non-top level lists may be empty/null
8020        // and we need to make sure we still handle them as invisible items (we
8021        // failed to do this previously)
8022        let rep = Some(vec![2, 1, 2, 0, 1, 2]);
8023        let def = Some(vec![0, 1, 2, 0, 0, 0]);
8024        let max_rep = 2;
8025        let max_visible_def = 0;
8026        let total_items = 4;
8027
8028        let check = |range, expected_item_range, expected_level_range| {
8029            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
8030                range,
8031                rep.as_ref(),
8032                def.as_ref(),
8033                max_rep,
8034                max_visible_def,
8035                total_items,
8036                PreambleAction::Absent,
8037            );
8038            assert_eq!(item_range, expected_item_range);
8039            assert_eq!(level_range, expected_level_range);
8040        };
8041
8042        check(0..3, 0..4, 0..6);
8043        check(0..1, 0..1, 0..2);
8044        check(1..2, 1..3, 2..5);
8045        check(2..3, 3..4, 5..6);
8046
8047        // Invisible items in a preamble that we are taking (regressing a previous failure)
8048        let rep = Some(vec![0, 0, 1, 0, 1, 1]);
8049        let def = Some(vec![0, 1, 0, 0, 0, 0]);
8050        let max_rep = 1;
8051        let max_visible_def = 0;
8052        let total_items = 5;
8053
8054        let check = |range, expected_item_range, expected_level_range| {
8055            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
8056                range,
8057                rep.as_ref(),
8058                def.as_ref(),
8059                max_rep,
8060                max_visible_def,
8061                total_items,
8062                PreambleAction::Take,
8063            );
8064            assert_eq!(item_range, expected_item_range);
8065            assert_eq!(level_range, expected_level_range);
8066        };
8067
8068        check(0..0, 0..1, 0..2);
8069        check(0..1, 0..3, 0..4);
8070        check(0..2, 0..4, 0..5);
8071
8072        // Skip preamble (with invis items) and skip a few rows (with invis items)
8073        // and then take a few rows but not all the rows
8074        let rep = Some(vec![0, 1, 0, 1, 0, 1, 0, 1]);
8075        let def = Some(vec![1, 0, 1, 1, 0, 0, 0, 0]);
8076        let max_rep = 1;
8077        let max_visible_def = 0;
8078        let total_items = 5;
8079
8080        let check = |range, expected_item_range, expected_level_range| {
8081            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
8082                range,
8083                rep.as_ref(),
8084                def.as_ref(),
8085                max_rep,
8086                max_visible_def,
8087                total_items,
8088                PreambleAction::Skip,
8089            );
8090            assert_eq!(item_range, expected_item_range);
8091            assert_eq!(level_range, expected_level_range);
8092        };
8093
8094        check(2..3, 2..4, 5..7);
8095    }
8096
8097    #[test]
8098    fn test_slice_batch_data_and_rebase_offsets_u32() {
8099        let data = LanceBuffer::copy_slice(b"0123456789abcdefghij");
8100        let offsets = LanceBuffer::reinterpret_vec(vec![6_u32, 8_u32, 8_u32, 12_u32]);
8101
8102        let (sliced_data, normalized_offsets) =
8103            VariableFullZipDecoder::slice_batch_data_and_rebase_offsets(&data, &offsets, 32)
8104                .unwrap();
8105
8106        assert_eq!(sliced_data.as_ref(), b"6789ab");
8107        let normalized = normalized_offsets.borrow_to_typed_slice::<u32>();
8108        assert_eq!(normalized.as_ref(), &[0, 2, 2, 6]);
8109    }
8110
8111    #[test]
8112    fn test_slice_batch_data_and_rebase_offsets_u64() {
8113        let data = LanceBuffer::copy_slice(b"abcdefghijklmnopqrstuvwxyz");
8114        let offsets = LanceBuffer::reinterpret_vec(vec![10_u64, 12_u64, 16_u64, 20_u64]);
8115
8116        let (sliced_data, normalized_offsets) =
8117            VariableFullZipDecoder::slice_batch_data_and_rebase_offsets(&data, &offsets, 64)
8118                .unwrap();
8119
8120        assert_eq!(sliced_data.as_ref(), b"klmnopqrst");
8121        let normalized = normalized_offsets.borrow_to_typed_slice::<u64>();
8122        assert_eq!(normalized.as_ref(), &[0, 2, 6, 10]);
8123    }
8124
8125    #[test]
8126    fn test_slice_batch_data_and_rebase_offsets_rejects_invalid_offsets() {
8127        let data = LanceBuffer::copy_slice(b"abcd");
8128        let offsets = LanceBuffer::reinterpret_vec(vec![3_u32, 2_u32]);
8129
8130        let err = VariableFullZipDecoder::slice_batch_data_and_rebase_offsets(&data, &offsets, 32)
8131            .expect_err("offset end before start should error");
8132        assert!(err.to_string().contains("less than base"));
8133    }
8134
8135    #[test]
8136    fn test_schedule_instructions() {
8137        // Convert repetition index to bytes for testing
8138        let rep_data: Vec<u64> = vec![5, 2, 3, 0, 4, 7, 2, 0];
8139        let rep_bytes: Vec<u8> = rep_data.iter().flat_map(|v| v.to_le_bytes()).collect();
8140        let chunk_index = MiniBlockChunkIndex::new_nested_for_test(&rep_bytes, 2);
8141
8142        let check = |user_ranges, expected_instructions| {
8143            let instructions = ChunkInstructions::schedule_instructions(&chunk_index, user_ranges);
8144            assert_eq!(instructions, expected_instructions);
8145        };
8146
8147        // The instructions we expect if we're grabbing the whole range
8148        let expected_take_all = vec![
8149            ChunkInstructions {
8150                chunk_idx: 0,
8151                preamble: PreambleAction::Absent,
8152                rows_to_skip: 0,
8153                rows_to_take: 6,
8154                take_trailer: true,
8155            },
8156            ChunkInstructions {
8157                chunk_idx: 1,
8158                preamble: PreambleAction::Take,
8159                rows_to_skip: 0,
8160                rows_to_take: 2,
8161                take_trailer: false,
8162            },
8163            ChunkInstructions {
8164                chunk_idx: 2,
8165                preamble: PreambleAction::Absent,
8166                rows_to_skip: 0,
8167                rows_to_take: 5,
8168                take_trailer: true,
8169            },
8170            ChunkInstructions {
8171                chunk_idx: 3,
8172                preamble: PreambleAction::Take,
8173                rows_to_skip: 0,
8174                rows_to_take: 1,
8175                take_trailer: false,
8176            },
8177        ];
8178
8179        // Take all as 1 range
8180        check(&[0..14], expected_take_all.clone());
8181
8182        // Take all a individual rows
8183        check(
8184            &[
8185                0..1,
8186                1..2,
8187                2..3,
8188                3..4,
8189                4..5,
8190                5..6,
8191                6..7,
8192                7..8,
8193                8..9,
8194                9..10,
8195                10..11,
8196                11..12,
8197                12..13,
8198                13..14,
8199            ],
8200            expected_take_all,
8201        );
8202
8203        // Test some partial takes
8204
8205        // 2 rows in the same chunk but not contiguous
8206        check(
8207            &[0..1, 3..4],
8208            vec![
8209                ChunkInstructions {
8210                    chunk_idx: 0,
8211                    preamble: PreambleAction::Absent,
8212                    rows_to_skip: 0,
8213                    rows_to_take: 1,
8214                    take_trailer: false,
8215                },
8216                ChunkInstructions {
8217                    chunk_idx: 0,
8218                    preamble: PreambleAction::Absent,
8219                    rows_to_skip: 3,
8220                    rows_to_take: 1,
8221                    take_trailer: false,
8222                },
8223            ],
8224        );
8225
8226        // Taking just a trailer/preamble
8227        check(
8228            &[5..6],
8229            vec![
8230                ChunkInstructions {
8231                    chunk_idx: 0,
8232                    preamble: PreambleAction::Absent,
8233                    rows_to_skip: 5,
8234                    rows_to_take: 1,
8235                    take_trailer: true,
8236                },
8237                ChunkInstructions {
8238                    chunk_idx: 1,
8239                    preamble: PreambleAction::Take,
8240                    rows_to_skip: 0,
8241                    rows_to_take: 0,
8242                    take_trailer: false,
8243                },
8244            ],
8245        );
8246
8247        // Skipping an entire chunk
8248        check(
8249            &[7..10],
8250            vec![
8251                ChunkInstructions {
8252                    chunk_idx: 1,
8253                    preamble: PreambleAction::Skip,
8254                    rows_to_skip: 1,
8255                    rows_to_take: 1,
8256                    take_trailer: false,
8257                },
8258                ChunkInstructions {
8259                    chunk_idx: 2,
8260                    preamble: PreambleAction::Absent,
8261                    rows_to_skip: 0,
8262                    rows_to_take: 2,
8263                    take_trailer: false,
8264                },
8265            ],
8266        );
8267    }
8268
8269    #[test]
8270    fn test_drain_instructions() {
8271        fn drain_from_instructions(
8272            instructions: &mut VecDeque<ChunkInstructions>,
8273            mut rows_desired: u64,
8274            need_preamble: &mut bool,
8275            skip_in_chunk: &mut u64,
8276        ) -> Vec<ChunkDrainInstructions> {
8277            // Note: instructions.len() is an upper bound, we typically take much fewer
8278            let mut drain_instructions = Vec::with_capacity(instructions.len());
8279            while rows_desired > 0 || *need_preamble {
8280                let (next_instructions, consumed_chunk) = instructions
8281                    .front()
8282                    .unwrap()
8283                    .drain_from_instruction(&mut rows_desired, need_preamble, skip_in_chunk);
8284                if consumed_chunk {
8285                    instructions.pop_front();
8286                }
8287                drain_instructions.push(next_instructions);
8288            }
8289            drain_instructions
8290        }
8291
8292        // Convert repetition index to bytes for testing
8293        let rep_data: Vec<u64> = vec![5, 2, 3, 0, 4, 7, 2, 0];
8294        let rep_bytes: Vec<u8> = rep_data.iter().flat_map(|v| v.to_le_bytes()).collect();
8295        let chunk_index = MiniBlockChunkIndex::new_nested_for_test(&rep_bytes, 2);
8296        let user_ranges = vec![1..7, 10..14];
8297
8298        // First, schedule the ranges
8299        let scheduled = ChunkInstructions::schedule_instructions(&chunk_index, &user_ranges);
8300
8301        let mut to_drain = VecDeque::from(scheduled.clone());
8302
8303        // Now we drain in batches of 4
8304
8305        let mut need_preamble = false;
8306        let mut skip_in_chunk = 0;
8307
8308        let next_batch =
8309            drain_from_instructions(&mut to_drain, 4, &mut need_preamble, &mut skip_in_chunk);
8310
8311        assert!(!need_preamble);
8312        assert_eq!(skip_in_chunk, 4);
8313        assert_eq!(
8314            next_batch,
8315            vec![ChunkDrainInstructions {
8316                chunk_instructions: scheduled[0].clone(),
8317                rows_to_take: 4,
8318                rows_to_skip: 0,
8319                preamble_action: PreambleAction::Absent,
8320            }]
8321        );
8322
8323        let next_batch =
8324            drain_from_instructions(&mut to_drain, 4, &mut need_preamble, &mut skip_in_chunk);
8325
8326        assert!(!need_preamble);
8327        assert_eq!(skip_in_chunk, 2);
8328
8329        assert_eq!(
8330            next_batch,
8331            vec![
8332                ChunkDrainInstructions {
8333                    chunk_instructions: scheduled[0].clone(),
8334                    rows_to_take: 1,
8335                    rows_to_skip: 4,
8336                    preamble_action: PreambleAction::Absent,
8337                },
8338                ChunkDrainInstructions {
8339                    chunk_instructions: scheduled[1].clone(),
8340                    rows_to_take: 1,
8341                    rows_to_skip: 0,
8342                    preamble_action: PreambleAction::Take,
8343                },
8344                ChunkDrainInstructions {
8345                    chunk_instructions: scheduled[2].clone(),
8346                    rows_to_take: 2,
8347                    rows_to_skip: 0,
8348                    preamble_action: PreambleAction::Absent,
8349                }
8350            ]
8351        );
8352
8353        let next_batch =
8354            drain_from_instructions(&mut to_drain, 2, &mut need_preamble, &mut skip_in_chunk);
8355
8356        assert!(!need_preamble);
8357        assert_eq!(skip_in_chunk, 0);
8358
8359        assert_eq!(
8360            next_batch,
8361            vec![
8362                ChunkDrainInstructions {
8363                    chunk_instructions: scheduled[2].clone(),
8364                    rows_to_take: 1,
8365                    rows_to_skip: 2,
8366                    preamble_action: PreambleAction::Absent,
8367                },
8368                ChunkDrainInstructions {
8369                    chunk_instructions: scheduled[3].clone(),
8370                    rows_to_take: 1,
8371                    rows_to_skip: 0,
8372                    preamble_action: PreambleAction::Take,
8373                },
8374            ]
8375        );
8376
8377        // Regression case.  Need a chunk with preamble, rows, and trailer (the middle chunk here)
8378        let rep_data: Vec<u64> = vec![5, 2, 3, 3, 20, 0];
8379        let rep_bytes: Vec<u8> = rep_data.iter().flat_map(|v| v.to_le_bytes()).collect();
8380        let chunk_index = MiniBlockChunkIndex::new_nested_for_test(&rep_bytes, 2);
8381        let user_ranges = vec![0..28];
8382
8383        // First, schedule the ranges
8384        let scheduled = ChunkInstructions::schedule_instructions(&chunk_index, &user_ranges);
8385
8386        let mut to_drain = VecDeque::from(scheduled.clone());
8387
8388        // Drain first chunk and some of second chunk
8389
8390        let mut need_preamble = false;
8391        let mut skip_in_chunk = 0;
8392
8393        let next_batch =
8394            drain_from_instructions(&mut to_drain, 7, &mut need_preamble, &mut skip_in_chunk);
8395
8396        assert_eq!(
8397            next_batch,
8398            vec![
8399                ChunkDrainInstructions {
8400                    chunk_instructions: scheduled[0].clone(),
8401                    rows_to_take: 6,
8402                    rows_to_skip: 0,
8403                    preamble_action: PreambleAction::Absent,
8404                },
8405                ChunkDrainInstructions {
8406                    chunk_instructions: scheduled[1].clone(),
8407                    rows_to_take: 1,
8408                    rows_to_skip: 0,
8409                    preamble_action: PreambleAction::Take,
8410                },
8411            ]
8412        );
8413
8414        assert!(!need_preamble);
8415        assert_eq!(skip_in_chunk, 1);
8416
8417        // Now, the tricky part.  We drain the second chunk, including the trailer, and need to make sure
8418        // we get a drain task to take the preamble of the third chunk (and nothing else)
8419        let next_batch =
8420            drain_from_instructions(&mut to_drain, 2, &mut need_preamble, &mut skip_in_chunk);
8421
8422        assert_eq!(
8423            next_batch,
8424            vec![
8425                ChunkDrainInstructions {
8426                    chunk_instructions: scheduled[1].clone(),
8427                    rows_to_take: 2,
8428                    rows_to_skip: 1,
8429                    preamble_action: PreambleAction::Skip,
8430                },
8431                ChunkDrainInstructions {
8432                    chunk_instructions: scheduled[2].clone(),
8433                    rows_to_take: 0,
8434                    rows_to_skip: 0,
8435                    preamble_action: PreambleAction::Take,
8436                },
8437            ]
8438        );
8439
8440        assert!(!need_preamble);
8441        assert_eq!(skip_in_chunk, 0);
8442    }
8443
8444    use super::chunk_index::{PrefixSums, RowMapping};
8445    use super::{MINIBLOCK_ALIGNMENT, Words, build_chunk_index};
8446    use bytes::Bytes;
8447    use lance_core::cache::{Context, DeepSizeOf};
8448    use rstest::rstest;
8449
8450    /// Builds a `Words` metadata buffer (u16 words) from `(log_num_values, num_bytes)`
8451    /// pairs, returning the words and the total data-buffer size.
8452    fn words_from(entries: &[(u32, u32)]) -> (Words, u64) {
8453        let mut raw = Vec::with_capacity(entries.len() * 2);
8454        let mut total = 0u64;
8455        for &(log, num_bytes) in entries {
8456            assert!(num_bytes > 0 && num_bytes % MINIBLOCK_ALIGNMENT as u32 == 0);
8457            let divided = num_bytes / MINIBLOCK_ALIGNMENT as u32 - 1;
8458            let word = (divided << 4) | log;
8459            assert!(word <= u16::MAX as u32, "test word {word} exceeds u16");
8460            raw.extend_from_slice(&(word as u16).to_le_bytes());
8461            total += num_bytes as u64;
8462        }
8463        (Words::from_bytes(Bytes::from(raw), false).unwrap(), total)
8464    }
8465
8466    fn rep_bytes_from(values: &[u64]) -> Vec<u8> {
8467        values.iter().flat_map(|v| v.to_le_bytes()).collect()
8468    }
8469
8470    #[rstest]
8471    // Two full chunks of 8 values (log 3) plus a partial last chunk; byte sizes vary
8472    // independently of value counts.
8473    #[case::uniform_partial_last(&[(3, 16), (3, 24), (0, 8)], 19, "uniform_flat", 8, 3)]
8474    // Single chunk covers the whole page.
8475    #[case::single_chunk(&[(0, 24)], 5, "uniform_flat", 5, 5)]
8476    // Last chunk is also full (exact multiple).
8477    #[case::exact_multiple(&[(3, 16), (3, 16)], 16, "uniform_flat", 8, 8)]
8478    // Non-last chunks differ in size, so this is a non-uniform flat page.
8479    #[case::non_uniform(&[(4, 16), (2, 16), (0, 8)], 21, "flat", 16, 1)]
8480    fn test_flat_detection(
8481        #[case] entries: &[(u32, u32)],
8482        #[case] items_in_page: u64,
8483        #[case] expected_kind: &str,
8484        #[case] expected_first_items: u64,
8485        #[case] expected_last_items: u64,
8486    ) {
8487        let base = 100u64;
8488        let (words, data_buf_size) = words_from(entries);
8489        let index = build_chunk_index(&words, items_in_page, base, data_buf_size, None, 0).unwrap();
8490
8491        assert_eq!(index.row_mapping_debug(), expected_kind);
8492        assert_eq!(index.num_chunks(), entries.len());
8493        assert_eq!(index.items_in_chunk(0), expected_first_items);
8494        assert_eq!(index.items_in_chunk(entries.len() - 1), expected_last_items);
8495
8496        // Byte ranges are absolute, contiguous, and exactly cover the data buffer.
8497        let mut expected_start = base;
8498        for (i, &(_, num_bytes)) in entries.iter().enumerate() {
8499            let range = index.byte_range(i);
8500            assert_eq!(range.start, expected_start);
8501            assert_eq!(range.end - range.start, num_bytes as u64);
8502            expected_start = range.end;
8503        }
8504        assert_eq!(expected_start, base + data_buf_size);
8505
8506        // For flat pages rows == items, so the per-chunk items sum to the page total.
8507        let total_items: u64 = (0..index.num_chunks())
8508            .map(|i| index.items_in_chunk(i))
8509            .sum();
8510        assert_eq!(total_items, items_in_page);
8511    }
8512
8513    #[test]
8514    fn test_nested_detection_and_axes() {
8515        // Repetition index (stride 2): three chunks holding 5, 4, 3 rows, no trailers.
8516        let rep = rep_bytes_from(&[5, 0, 4, 0, 3, 0]);
8517
8518        // Uniform leaf chunking: value counts 4, 4, 2.
8519        let (words, data_buf_size) = words_from(&[(2, 8), (2, 8), (0, 8)]);
8520        let index = build_chunk_index(&words, 10, 0, data_buf_size, Some(&rep), 1).unwrap();
8521        assert_eq!(index.row_mapping_debug(), "nested");
8522        assert_eq!(index.num_chunks(), 3);
8523        // Rows come from the repetition index, not the value counts.
8524        assert_eq!(index.first_row(0), 0);
8525        assert_eq!(index.rows_in_chunk(0), 5);
8526        assert_eq!(index.first_row(1), 5);
8527        assert_eq!(index.rows_in_chunk(1), 4);
8528        assert_eq!(index.first_row(2), 9);
8529        assert_eq!(index.rows_in_chunk(2), 3);
8530        // Items come from the value words.
8531        assert_eq!(index.items_in_chunk(0), 4);
8532        assert_eq!(index.items_in_chunk(1), 4);
8533        assert_eq!(index.items_in_chunk(2), 2);
8534
8535        // Non-uniform leaf chunking: value counts 8, 2, 5.
8536        let (words_nu, dbs_nu) = words_from(&[(3, 8), (1, 8), (0, 8)]);
8537        let index_nu = build_chunk_index(&words_nu, 15, 0, dbs_nu, Some(&rep), 1).unwrap();
8538        assert_eq!(index_nu.row_mapping_debug(), "nested");
8539        assert_eq!(index_nu.items_in_chunk(0), 8);
8540        assert_eq!(index_nu.items_in_chunk(1), 2);
8541        assert_eq!(index_nu.items_in_chunk(2), 5);
8542        // The row axis is unchanged by the leaf chunking.
8543        assert_eq!(index_nu.rows_in_chunk(0), 5);
8544    }
8545
8546    #[test]
8547    fn test_uniform_flat_matches_prefix_sum_flat() {
8548        // Distribution: 4 chunks of 4 values, last chunk 3 (15 items total).
8549        let (words, data_buf_size) = words_from(&[(2, 8), (2, 8), (2, 8), (0, 8)]);
8550        let uniform = build_chunk_index(&words, 15, 0, data_buf_size, None, 0).unwrap();
8551        assert_eq!(uniform.row_mapping_debug(), "uniform_flat");
8552
8553        // The same distribution expressed as a non-uniform Flat prefix-sum index.
8554        let byte_starts = PrefixSums::from_deltas([8u64, 8, 8, 8].into_iter(), 4, 32);
8555        let value_starts = PrefixSums::from_deltas([4u64, 4, 4, 3].into_iter(), 4, 15);
8556        let flat = MiniBlockChunkIndex::new(0, byte_starts, RowMapping::Flat { value_starts });
8557        assert_eq!(flat.row_mapping_debug(), "flat");
8558
8559        // Lookup parity: identical byte ranges and item counts.
8560        for i in 0..4 {
8561            assert_eq!(uniform.byte_range(i), flat.byte_range(i));
8562            assert_eq!(uniform.items_in_chunk(i), flat.items_in_chunk(i));
8563        }
8564
8565        // Scheduler parity across scan / single-row / partial / scattered multi-range.
8566        let range_sets: Vec<Vec<std::ops::Range<u64>>> = vec![
8567            vec![0..15],
8568            vec![0..1],
8569            vec![7..8],
8570            vec![14..15],
8571            vec![3..10],
8572            vec![0..2, 5..6, 12..15],
8573        ];
8574        for ranges in &range_sets {
8575            let from_uniform = ChunkInstructions::schedule_instructions(&uniform, ranges);
8576            let from_flat = ChunkInstructions::schedule_instructions(&flat, ranges);
8577            assert_eq!(from_uniform, from_flat, "mismatch for ranges {ranges:?}");
8578        }
8579
8580        // A full scan yields one Absent, no-trailer instruction per chunk.
8581        let full = ChunkInstructions::schedule_instructions(&uniform, &[0..15]);
8582        assert_eq!(full.len(), 4);
8583        for (i, inst) in full.iter().enumerate() {
8584            assert_eq!(inst.chunk_idx, i);
8585            assert_eq!(inst.preamble, PreambleAction::Absent);
8586            assert_eq!(inst.rows_to_skip, 0);
8587            assert!(!inst.take_trailer);
8588        }
8589        assert_eq!(full.iter().map(|i| i.rows_to_take).sum::<u64>(), 15);
8590    }
8591
8592    #[test]
8593    fn test_deep_size_per_variant_below_legacy() {
8594        // The previous representation cached 48 bytes per chunk (24 for ChunkMeta plus
8595        // 24 for a rep-index block); every variant's heap must be well below that.
8596        const LEGACY_PER_CHUNK: usize = 48;
8597        let num_chunks = 3;
8598        let heap = |index: &MiniBlockChunkIndex| index.deep_size_of_children(&mut Context::new());
8599
8600        let (uniform_words, uniform_dbs) = words_from(&[(2, 8), (2, 8), (0, 8)]);
8601        let uniform = build_chunk_index(&uniform_words, 10, 0, uniform_dbs, None, 0).unwrap();
8602        assert_eq!(uniform.row_mapping_debug(), "uniform_flat");
8603        assert!(heap(&uniform) < LEGACY_PER_CHUNK * num_chunks);
8604
8605        let (flat_words, flat_dbs) = words_from(&[(3, 8), (1, 8), (0, 8)]);
8606        let flat = build_chunk_index(&flat_words, 11, 0, flat_dbs, None, 0).unwrap();
8607        assert_eq!(flat.row_mapping_debug(), "flat");
8608        assert!(heap(&flat) < LEGACY_PER_CHUNK * num_chunks);
8609        // Flat carries a value-starts array that UniformFlat derives arithmetically.
8610        assert!(heap(&flat) > heap(&uniform));
8611
8612        let rep = rep_bytes_from(&[4, 0, 3, 0, 3, 0]);
8613        let (nested_words, nested_dbs) = words_from(&[(2, 8), (2, 8), (0, 8)]);
8614        let nested = build_chunk_index(&nested_words, 10, 0, nested_dbs, Some(&rep), 1).unwrap();
8615        assert_eq!(nested.row_mapping_debug(), "nested");
8616        assert!(heap(&nested) < LEGACY_PER_CHUNK * num_chunks);
8617    }
8618
8619    #[tokio::test]
8620    async fn test_fullzip_initialize_is_lazy() {
8621        use futures::{FutureExt, future::BoxFuture};
8622        use std::ops::Range;
8623        use std::sync::Mutex;
8624
8625        #[derive(Debug, Clone)]
8626        struct RecordingScheduler {
8627            data: bytes::Bytes,
8628            requests: Arc<Mutex<Vec<Vec<Range<u64>>>>>,
8629        }
8630
8631        impl RecordingScheduler {
8632            fn new(data: bytes::Bytes) -> Self {
8633                Self {
8634                    data,
8635                    requests: Arc::new(Mutex::new(Vec::new())),
8636                }
8637            }
8638
8639            fn requests(&self) -> Vec<Vec<Range<u64>>> {
8640                self.requests.lock().unwrap().clone()
8641            }
8642        }
8643
8644        impl crate::EncodingsIo for RecordingScheduler {
8645            fn submit_request(
8646                &self,
8647                ranges: Vec<Range<u64>>,
8648                _priority: u64,
8649            ) -> BoxFuture<'static, crate::Result<Vec<bytes::Bytes>>> {
8650                self.requests.lock().unwrap().push(ranges.clone());
8651                let data = ranges
8652                    .into_iter()
8653                    .map(|range| self.data.slice(range.start as usize..range.end as usize))
8654                    .collect::<Vec<_>>();
8655                std::future::ready(Ok(data)).boxed()
8656            }
8657        }
8658
8659        #[derive(Debug)]
8660        struct TestFixedDecompressor;
8661
8662        impl FixedPerValueDecompressor for TestFixedDecompressor {
8663            fn decompress(
8664                &self,
8665                _data: FixedWidthDataBlock,
8666                _num_rows: u64,
8667            ) -> crate::Result<DataBlock> {
8668                unimplemented!("Test decompressor")
8669            }
8670
8671            fn bits_per_value(&self) -> u64 {
8672                32
8673            }
8674        }
8675
8676        let io = Arc::new(RecordingScheduler::new(bytes::Bytes::from(vec![
8677            0;
8678            16 * 1024
8679        ])));
8680        let mut scheduler = FullZipScheduler {
8681            data_buf_position: 0,
8682            data_buf_size: 4096,
8683            rep_index: Some(FullZipRepIndexDetails {
8684                buf_position: 1000,
8685                bytes_per_value: 4,
8686            }),
8687            priority: 0,
8688            rows_in_page: 100,
8689            bits_per_offset: 32,
8690            details: Arc::new(FullZipDecodeDetails {
8691                value_decompressor: PerValueDecompressor::Fixed(Arc::new(TestFixedDecompressor)),
8692                def_meaning: Arc::new([crate::repdef::DefinitionInterpretation::NullableItem]),
8693                ctrl_word_parser: crate::repdef::ControlWordParser::new(0, 1),
8694                max_rep: 0,
8695                max_visible_def: 0,
8696            }),
8697            cached_state: None,
8698            enable_cache: false,
8699        };
8700
8701        let io_dyn: Arc<dyn crate::EncodingsIo> = io.clone();
8702        let cached_data = scheduler.initialize(&io_dyn).await.unwrap();
8703
8704        assert!(
8705            cached_data
8706                .as_arc_any()
8707                .downcast_ref::<super::NoCachedPageData>()
8708                .is_some(),
8709            "FullZip initialize should not eagerly load repetition index data"
8710        );
8711        assert!(scheduler.cached_state.is_none());
8712        assert!(
8713            io.requests().is_empty(),
8714            "FullZip initialize should not issue any I/O"
8715        );
8716    }
8717
8718    #[tokio::test]
8719    async fn test_fullzip_read_source_slices_prefetched_page() {
8720        let page_start = 200_u64;
8721        let page_data = LanceBuffer::copy_slice(&[0, 1, 2, 3, 4, 5, 6, 7]);
8722        let source = FullZipReadSource::PrefetchedPage {
8723            base_offset: page_start,
8724            data: page_data,
8725        };
8726        let ranges = vec![
8727            page_start..(page_start + 3),
8728            (page_start + 4)..(page_start + 8),
8729        ];
8730        let mut data = source.fetch(&ranges, 0).await.unwrap();
8731        assert_eq!(data.pop_front().unwrap().as_ref(), &[0, 1, 2]);
8732        assert_eq!(data.pop_front().unwrap().as_ref(), &[4, 5, 6, 7]);
8733    }
8734
8735    #[tokio::test]
8736    async fn test_fullzip_initialize_caches_rep_index_when_enabled() {
8737        use futures::{FutureExt, future::BoxFuture};
8738        use std::ops::Range;
8739        use std::sync::Mutex;
8740
8741        #[derive(Debug, Clone)]
8742        struct RecordingScheduler {
8743            data: bytes::Bytes,
8744            requests: Arc<Mutex<Vec<Vec<Range<u64>>>>>,
8745        }
8746
8747        impl RecordingScheduler {
8748            fn new(data: bytes::Bytes) -> Self {
8749                Self {
8750                    data,
8751                    requests: Arc::new(Mutex::new(Vec::new())),
8752                }
8753            }
8754
8755            fn requests(&self) -> Vec<Vec<Range<u64>>> {
8756                self.requests.lock().unwrap().clone()
8757            }
8758        }
8759
8760        impl crate::EncodingsIo for RecordingScheduler {
8761            fn submit_request(
8762                &self,
8763                ranges: Vec<Range<u64>>,
8764                _priority: u64,
8765            ) -> BoxFuture<'static, crate::Result<Vec<bytes::Bytes>>> {
8766                self.requests.lock().unwrap().push(ranges.clone());
8767                let data = ranges
8768                    .into_iter()
8769                    .map(|range| self.data.slice(range.start as usize..range.end as usize))
8770                    .collect::<Vec<_>>();
8771                std::future::ready(Ok(data)).boxed()
8772            }
8773        }
8774
8775        #[derive(Debug)]
8776        struct TestFixedDecompressor;
8777
8778        impl FixedPerValueDecompressor for TestFixedDecompressor {
8779            fn decompress(
8780                &self,
8781                _data: FixedWidthDataBlock,
8782                _num_rows: u64,
8783            ) -> crate::Result<DataBlock> {
8784                unimplemented!("Test decompressor")
8785            }
8786
8787            fn bits_per_value(&self) -> u64 {
8788                32
8789            }
8790        }
8791
8792        let rows_in_page = 100_u64;
8793        let bytes_per_value = 4_u64;
8794        let rep_start = 1000_u64;
8795        let rep_size = ((rows_in_page + 1) * bytes_per_value) as usize;
8796        let mut data = vec![0_u8; 16 * 1024];
8797        data[rep_start as usize..rep_start as usize + rep_size].fill(7);
8798        let io = Arc::new(RecordingScheduler::new(bytes::Bytes::from(data)));
8799
8800        let mut scheduler = FullZipScheduler {
8801            data_buf_position: 0,
8802            data_buf_size: 4096,
8803            rep_index: Some(FullZipRepIndexDetails {
8804                buf_position: rep_start,
8805                bytes_per_value,
8806            }),
8807            priority: 0,
8808            rows_in_page,
8809            bits_per_offset: 32,
8810            details: Arc::new(FullZipDecodeDetails {
8811                value_decompressor: PerValueDecompressor::Fixed(Arc::new(TestFixedDecompressor)),
8812                def_meaning: Arc::new([crate::repdef::DefinitionInterpretation::NullableItem]),
8813                ctrl_word_parser: crate::repdef::ControlWordParser::new(0, 1),
8814                max_rep: 0,
8815                max_visible_def: 0,
8816            }),
8817            cached_state: None,
8818            enable_cache: true,
8819        };
8820
8821        let io_dyn: Arc<dyn crate::EncodingsIo> = io.clone();
8822        let cached_data = scheduler.initialize(&io_dyn).await.unwrap();
8823        assert!(
8824            cached_data
8825                .as_arc_any()
8826                .downcast_ref::<FullZipCacheableState>()
8827                .is_some()
8828        );
8829        assert!(scheduler.cached_state.is_some());
8830        assert_eq!(
8831            io.requests(),
8832            vec![vec![
8833                rep_start..(rep_start + (rows_in_page + 1) * bytes_per_value)
8834            ]]
8835        );
8836    }
8837
8838    #[tokio::test]
8839    async fn test_fullzip_full_page_bypasses_rep_index_io() {
8840        use futures::{FutureExt, future::BoxFuture};
8841        use std::ops::Range;
8842        use std::sync::Mutex;
8843
8844        #[derive(Debug, Clone)]
8845        struct RecordingScheduler {
8846            data: bytes::Bytes,
8847            requests: Arc<Mutex<Vec<Vec<Range<u64>>>>>,
8848        }
8849
8850        impl RecordingScheduler {
8851            fn new(data: bytes::Bytes) -> Self {
8852                Self {
8853                    data,
8854                    requests: Arc::new(Mutex::new(Vec::new())),
8855                }
8856            }
8857
8858            fn requests(&self) -> Vec<Vec<Range<u64>>> {
8859                self.requests.lock().unwrap().clone()
8860            }
8861        }
8862
8863        impl crate::EncodingsIo for RecordingScheduler {
8864            fn submit_request(
8865                &self,
8866                ranges: Vec<Range<u64>>,
8867                _priority: u64,
8868            ) -> BoxFuture<'static, crate::Result<Vec<bytes::Bytes>>> {
8869                self.requests.lock().unwrap().push(ranges.clone());
8870                let data = ranges
8871                    .into_iter()
8872                    .map(|range| self.data.slice(range.start as usize..range.end as usize))
8873                    .collect::<Vec<_>>();
8874                std::future::ready(Ok(data)).boxed()
8875            }
8876        }
8877
8878        #[derive(Debug)]
8879        struct TestFixedDecompressor;
8880
8881        impl FixedPerValueDecompressor for TestFixedDecompressor {
8882            fn decompress(
8883                &self,
8884                _data: FixedWidthDataBlock,
8885                _num_rows: u64,
8886            ) -> crate::Result<DataBlock> {
8887                unimplemented!("Test decompressor")
8888            }
8889
8890            fn bits_per_value(&self) -> u64 {
8891                32
8892            }
8893        }
8894
8895        let rows_in_page = 100_u64;
8896        let data_start = 256_u64;
8897        let data_size = 500_u64;
8898        let rep_start = 4096_u64;
8899        let bytes_per_value = 4_u64;
8900
8901        let mut bytes = vec![0_u8; 16 * 1024];
8902        for i in 0..=rows_in_page {
8903            let offset = (i * 5) as u32;
8904            let pos = rep_start as usize + (i * bytes_per_value) as usize;
8905            bytes[pos..pos + 4].copy_from_slice(&offset.to_le_bytes());
8906        }
8907        let io = Arc::new(RecordingScheduler::new(bytes::Bytes::from(bytes)));
8908
8909        let scheduler = FullZipScheduler {
8910            data_buf_position: data_start,
8911            data_buf_size: data_size,
8912            rep_index: Some(FullZipRepIndexDetails {
8913                buf_position: rep_start,
8914                bytes_per_value,
8915            }),
8916            priority: 0,
8917            rows_in_page,
8918            bits_per_offset: 32,
8919            details: Arc::new(FullZipDecodeDetails {
8920                value_decompressor: PerValueDecompressor::Fixed(Arc::new(TestFixedDecompressor)),
8921                def_meaning: Arc::new([crate::repdef::DefinitionInterpretation::NullableItem]),
8922                ctrl_word_parser: crate::repdef::ControlWordParser::new(0, 1),
8923                max_rep: 0,
8924                max_visible_def: 0,
8925            }),
8926            cached_state: None,
8927            enable_cache: false,
8928        };
8929
8930        let io_dyn: Arc<dyn crate::EncodingsIo> = io.clone();
8931        let tasks = scheduler
8932            .schedule_ranges_rep(
8933                &[0..rows_in_page],
8934                &io_dyn,
8935                FullZipRepIndexDetails {
8936                    buf_position: rep_start,
8937                    bytes_per_value,
8938                },
8939            )
8940            .unwrap();
8941
8942        let requests = io.requests();
8943        assert_eq!(requests.len(), 1);
8944        assert_eq!(requests[0], vec![data_start..(data_start + data_size)]);
8945
8946        let _ = tasks.into_iter().next().unwrap().decoder_fut.await.unwrap();
8947        let requests_after_await = io.requests();
8948        assert_eq!(
8949            requests_after_await.len(),
8950            1,
8951            "full page path should not issue rep-index I/O"
8952        );
8953    }
8954
8955    /// This test is used to reproduce fuzz test https://github.com/lancedb/lance/issues/4492
8956    #[tokio::test]
8957    async fn test_fuzz_issue_4492_empty_rep_values() {
8958        use lance_datagen::{RowCount, Seed, array, gen_batch};
8959
8960        let seed = 1823859942947654717u64;
8961        let num_rows = 2741usize;
8962
8963        // Generate the exact same data that caused the failure
8964        let batch_gen = gen_batch().with_seed(Seed::from(seed));
8965        let base_generator = array::rand_type(&DataType::FixedSizeBinary(32));
8966        let list_generator = array::rand_list_any(base_generator, false);
8967
8968        let batch = batch_gen
8969            .anon_col(list_generator)
8970            .into_batch_rows(RowCount::from(num_rows as u64))
8971            .unwrap();
8972
8973        let list_array = batch.column(0).clone();
8974
8975        // Force miniblock encoding
8976        let mut metadata = HashMap::new();
8977        metadata.insert(
8978            STRUCTURAL_ENCODING_META_KEY.to_string(),
8979            STRUCTURAL_ENCODING_MINIBLOCK.to_string(),
8980        );
8981
8982        let test_cases = TestCases::default()
8983            .with_structural_encodings()
8984            .with_batch_size(100)
8985            .with_range(0..num_rows.min(500) as u64)
8986            .with_indices(vec![0, num_rows as u64 / 2, (num_rows - 1) as u64]);
8987
8988        check_round_trip_encoding_of_data(vec![list_array], &test_cases, metadata).await
8989    }
8990
8991    async fn test_minichunk_size_helper(
8992        string_data: Vec<Option<String>>,
8993        minichunk_size: u64,
8994        encodings: &[TestEncoding],
8995    ) {
8996        use crate::constants::MINICHUNK_SIZE_META_KEY;
8997        use crate::testing::{TestCases, check_round_trip_encoding_of_data};
8998        use arrow_array::{ArrayRef, StringArray};
8999        use std::sync::Arc;
9000
9001        let string_array: ArrayRef = Arc::new(StringArray::from(string_data));
9002
9003        let mut metadata = HashMap::new();
9004        metadata.insert(
9005            MINICHUNK_SIZE_META_KEY.to_string(),
9006            minichunk_size.to_string(),
9007        );
9008        metadata.insert(
9009            STRUCTURAL_ENCODING_META_KEY.to_string(),
9010            STRUCTURAL_ENCODING_MINIBLOCK.to_string(),
9011        );
9012
9013        let test_cases = TestCases::default()
9014            .with_encodings(encodings.iter().copied())
9015            .with_batch_size(1000);
9016
9017        check_round_trip_encoding_of_data(vec![string_array], &test_cases, metadata).await;
9018    }
9019
9020    #[tokio::test]
9021    async fn test_minichunk_size_roundtrip() {
9022        // Test that minichunk size can be configured and works correctly in round-trip encoding
9023        let mut string_data = Vec::new();
9024        for i in 0..100 {
9025            string_data.push(Some(format!("test_string_{}", i).repeat(50)));
9026        }
9027        // configure minichunk size to 64 bytes (smaller than the default 4kb) for Lance 2.1
9028        test_minichunk_size_helper(
9029            string_data,
9030            64,
9031            &[
9032                TestEncoding::StructuralU16,
9033                TestEncoding::StructuralU32,
9034                TestEncoding::StructuralSparse,
9035            ],
9036        )
9037        .await;
9038    }
9039
9040    #[tokio::test]
9041    async fn test_minichunk_size_128kb_v2_2() {
9042        // Test that minichunk size can be configured to 128KB and works correctly with Lance 2.2
9043        let mut string_data = Vec::new();
9044        // create a 500kb string array
9045        for i in 0..10000 {
9046            string_data.push(Some(format!("test_string_{}", i).repeat(50)));
9047        }
9048        test_minichunk_size_helper(
9049            string_data,
9050            128 * 1024,
9051            &[TestEncoding::StructuralU32, TestEncoding::StructuralSparse],
9052        )
9053        .await;
9054    }
9055
9056    #[tokio::test]
9057    async fn test_binary_large_minichunk_size_over_max_miniblock_values() {
9058        let mut string_data = Vec::new();
9059        // 128kb/chunk / 6 bytes (t_9999) = 21845 items per chunk
9060        for i in 0..10000 {
9061            string_data.push(Some(format!("t_{}", i)));
9062        }
9063        test_minichunk_size_helper(
9064            string_data,
9065            128 * 1024,
9066            &[TestEncoding::StructuralU32, TestEncoding::StructuralSparse],
9067        )
9068        .await;
9069    }
9070
9071    #[tokio::test]
9072    async fn test_large_dictionary_general_compression() {
9073        use arrow_array::{ArrayRef, StringArray};
9074        use std::collections::HashMap;
9075        use std::sync::Arc;
9076
9077        // Create large string dictionary data (>32KiB) with low cardinality
9078        // Use 100 unique strings, each 500 bytes long = 50KB dictionary
9079        let unique_values: Vec<String> = (0..100)
9080            .map(|i| format!("value_{:04}_{}", i, "x".repeat(500)))
9081            .collect();
9082
9083        // Repeat these strings many times to create a large array
9084        let repeated_strings: Vec<_> = unique_values
9085            .iter()
9086            .cycle()
9087            .take(10_000)
9088            .map(|s| Some(s.as_str()))
9089            .collect();
9090
9091        let string_array = Arc::new(StringArray::from(repeated_strings)) as ArrayRef;
9092
9093        // Configure test to use V2_2 and verify encoding
9094        let test_cases = TestCases::default()
9095            .with_u32_structural_encodings()
9096            .with_verify_encoding(Arc::new(|cols: &[crate::encoder::EncodedColumn], _| {
9097                assert_eq!(cols.len(), 1);
9098                let col = &cols[0];
9099
9100                // Navigate to the dictionary encoding in the page layout
9101                if let Some(PageEncoding::Structural(page_layout)) =
9102                    &col.final_pages.first().map(|p| &p.description)
9103                    && let Some(pb21::page_layout::Layout::MiniBlockLayout(mini_block)) =
9104                        &page_layout.layout
9105                    && let Some(dictionary_encoding) = &mini_block.dictionary
9106                {
9107                    match dictionary_encoding.compression.as_ref() {
9108                        Some(Compression::General(general)) => {
9109                            // Verify it's using LZ4 or Zstd
9110                            let compression = general.compression.as_ref().unwrap();
9111                            assert!(
9112                                compression.scheme()
9113                                    == pb21::CompressionScheme::CompressionAlgorithmLz4
9114                                    || compression.scheme()
9115                                        == pb21::CompressionScheme::CompressionAlgorithmZstd,
9116                                "Expected LZ4 or Zstd compression for large dictionary"
9117                            );
9118                        }
9119                        _ => panic!("Expected General compression for large dictionary"),
9120                    }
9121                }
9122            }));
9123
9124        check_round_trip_encoding_of_data(vec![string_array], &test_cases, HashMap::new()).await;
9125    }
9126
9127    fn dictionary_encoding_from_page(
9128        page: &crate::encoder::EncodedPage,
9129    ) -> &crate::format::pb21::CompressiveEncoding {
9130        let PageEncoding::Structural(layout) = &page.description else {
9131            panic!("Expected structural page encoding");
9132        };
9133        let pb21::page_layout::Layout::MiniBlockLayout(layout) = layout.layout.as_ref().unwrap()
9134        else {
9135            panic!("Expected mini-block layout");
9136        };
9137        layout
9138            .dictionary
9139            .as_ref()
9140            .unwrap_or_else(|| panic!("Expected dictionary encoding"))
9141    }
9142
9143    async fn encode_variable_dict_page(
9144        metadata: HashMap<String, String>,
9145    ) -> crate::encoder::EncodedPage {
9146        use arrow_array::types::Int32Type;
9147        use arrow_array::{ArrayRef, DictionaryArray, Int32Array, StringArray};
9148
9149        let values = Arc::new(StringArray::from(
9150            (0..128)
9151                .map(|i| format!("value_{i:04}_{}", "x".repeat(256)))
9152                .collect::<Vec<_>>(),
9153        )) as ArrayRef;
9154        let keys = Int32Array::from_iter_values((0..20_000).map(|i| i % 128));
9155        let dict_array =
9156            Arc::new(DictionaryArray::<Int32Type>::try_new(keys, values).unwrap()) as ArrayRef;
9157
9158        let field = arrow_schema::Field::new(
9159            "dict_col",
9160            DataType::Dictionary(Box::new(DataType::Int32), Box::new(DataType::Utf8)),
9161            false,
9162        )
9163        .with_metadata(metadata);
9164
9165        encode_first_page(field, dict_array, TestEncoding::StructuralU32).await
9166    }
9167
9168    async fn encode_auto_fixed_dict_page(
9169        metadata: HashMap<String, String>,
9170    ) -> crate::encoder::EncodedPage {
9171        use arrow_array::{ArrayRef, Decimal128Array};
9172
9173        // 128-bit fixed-width values with low cardinality to trigger dictionary encoding.
9174        let values = (0..20_000)
9175            .map(|i| match i % 3 {
9176                0 => 10_i128,
9177                1 => 20_i128,
9178                _ => 30_i128,
9179            })
9180            .collect::<Vec<_>>();
9181        let decimal = Decimal128Array::from_iter_values(values)
9182            .with_precision_and_scale(38, 0)
9183            .unwrap();
9184        let decimal = Arc::new(decimal) as ArrayRef;
9185
9186        let mut field_metadata = metadata;
9187        // Strongly encourage dictionary encoding for this synthetic test data.
9188        field_metadata.insert(
9189            "lance-encoding:dict-size-ratio".to_string(),
9190            "0.99".to_string(),
9191        );
9192        let field = arrow_schema::Field::new("fixed_col", DataType::Decimal128(38, 0), false)
9193            .with_metadata(field_metadata);
9194
9195        encode_first_page(field, decimal, TestEncoding::StructuralU32).await
9196    }
9197
9198    #[tokio::test]
9199    async fn test_dict_values_general_compression_default_lz4_for_variable_dict_values() {
9200        let page = encode_variable_dict_page(HashMap::new()).await;
9201        let dictionary_encoding = dictionary_encoding_from_page(&page);
9202        let Some(Compression::General(general)) = dictionary_encoding.compression.as_ref() else {
9203            panic!("Expected General compression for dictionary values");
9204        };
9205        let compression = general.compression.as_ref().unwrap();
9206        assert_eq!(
9207            compression.scheme(),
9208            pb21::CompressionScheme::CompressionAlgorithmLz4
9209        );
9210    }
9211
9212    #[tokio::test]
9213    async fn test_dict_values_general_compression_default_lz4_for_fixed_dict_values() {
9214        let page = encode_auto_fixed_dict_page(HashMap::new()).await;
9215        let dictionary_encoding = dictionary_encoding_from_page(&page);
9216        let Some(Compression::General(general)) = dictionary_encoding.compression.as_ref() else {
9217            panic!("Expected General compression for dictionary values");
9218        };
9219        let compression = general.compression.as_ref().unwrap();
9220        assert_eq!(
9221            compression.scheme(),
9222            pb21::CompressionScheme::CompressionAlgorithmLz4
9223        );
9224    }
9225
9226    #[tokio::test]
9227    async fn test_dict_values_general_compression_zstd() {
9228        let mut metadata = HashMap::new();
9229        metadata.insert(
9230            DICT_VALUES_COMPRESSION_META_KEY.to_string(),
9231            "zstd".to_string(),
9232        );
9233        let page = encode_variable_dict_page(metadata).await;
9234        let dictionary_encoding = dictionary_encoding_from_page(&page);
9235        let Some(Compression::General(general)) = dictionary_encoding.compression.as_ref() else {
9236            panic!("Expected General compression for dictionary values");
9237        };
9238        let compression = general.compression.as_ref().unwrap();
9239        assert_eq!(
9240            compression.scheme(),
9241            pb21::CompressionScheme::CompressionAlgorithmZstd
9242        );
9243    }
9244
9245    #[tokio::test]
9246    async fn test_dict_values_general_compression_none() {
9247        let mut metadata = HashMap::new();
9248        metadata.insert(
9249            DICT_VALUES_COMPRESSION_META_KEY.to_string(),
9250            "none".to_string(),
9251        );
9252        let page = encode_variable_dict_page(metadata).await;
9253        let dictionary_encoding = dictionary_encoding_from_page(&page);
9254        assert!(
9255            !matches!(
9256                dictionary_encoding.compression.as_ref(),
9257                Some(Compression::General(_))
9258            ),
9259            "Expected dictionary values to avoid General compression"
9260        );
9261    }
9262
9263    #[test]
9264    fn test_resolve_dict_values_compression_metadata_defaults_to_lz4() {
9265        let metadata = PrimitiveStructuralEncoder::resolve_dict_values_compression_metadata(
9266            &HashMap::new(),
9267            None,
9268            None,
9269        );
9270        assert_eq!(metadata.get(COMPRESSION_META_KEY), Some(&"lz4".to_string()),);
9271        assert!(!metadata.contains_key(COMPRESSION_LEVEL_META_KEY));
9272    }
9273
9274    #[test]
9275    fn test_resolve_dict_values_compression_metadata_metadata_overrides_env() {
9276        let field_metadata = HashMap::from([
9277            (
9278                DICT_VALUES_COMPRESSION_META_KEY.to_string(),
9279                "none".to_string(),
9280            ),
9281            (
9282                DICT_VALUES_COMPRESSION_LEVEL_META_KEY.to_string(),
9283                "7".to_string(),
9284            ),
9285        ]);
9286        let metadata = PrimitiveStructuralEncoder::resolve_dict_values_compression_metadata(
9287            &field_metadata,
9288            Some("zstd".to_string()),
9289            Some("3".to_string()),
9290        );
9291        assert_eq!(
9292            metadata.get(COMPRESSION_META_KEY),
9293            Some(&"none".to_string()),
9294        );
9295        assert_eq!(
9296            metadata.get(COMPRESSION_LEVEL_META_KEY),
9297            Some(&"7".to_string()),
9298        );
9299    }
9300
9301    #[test]
9302    fn test_resolve_dict_values_compression_metadata_env_fallback() {
9303        let metadata = PrimitiveStructuralEncoder::resolve_dict_values_compression_metadata(
9304            &HashMap::new(),
9305            Some("zstd".to_string()),
9306            Some("9".to_string()),
9307        );
9308        assert_eq!(
9309            metadata.get(COMPRESSION_META_KEY),
9310            Some(&"zstd".to_string()),
9311        );
9312        assert_eq!(
9313            metadata.get(COMPRESSION_LEVEL_META_KEY),
9314            Some(&"9".to_string()),
9315        );
9316    }
9317
9318    #[tokio::test]
9319    async fn test_dictionary_encode_int64() {
9320        use crate::constants::{DICT_SIZE_RATIO_META_KEY, STRUCTURAL_ENCODING_META_KEY};
9321        use crate::testing::{TestCases, check_round_trip_encoding_of_data};
9322        use arrow_array::{ArrayRef, Int64Array};
9323        use std::collections::HashMap;
9324        use std::sync::Arc;
9325
9326        // Low cardinality with poor RLE opportunity.
9327        let values = (0..1000)
9328            .map(|i| match i % 3 {
9329                0 => 10i64,
9330                1 => 20i64,
9331                _ => 30i64,
9332            })
9333            .collect::<Vec<_>>();
9334        let array = Arc::new(Int64Array::from(values)) as ArrayRef;
9335
9336        let mut metadata = HashMap::new();
9337        metadata.insert(
9338            STRUCTURAL_ENCODING_META_KEY.to_string(),
9339            STRUCTURAL_ENCODING_MINIBLOCK.to_string(),
9340        );
9341        metadata.insert(DICT_SIZE_RATIO_META_KEY.to_string(), "0.99".to_string());
9342
9343        let test_cases = TestCases::default()
9344            .with_u32_structural_encodings()
9345            .with_batch_size(1000)
9346            .with_range(0..1000)
9347            .with_indices(vec![0, 1, 10, 999])
9348            .with_expected_encoding("dictionary");
9349
9350        check_round_trip_encoding_of_data(vec![array], &test_cases, metadata).await;
9351    }
9352
9353    #[tokio::test]
9354    async fn test_dictionary_encode_float64() {
9355        use crate::constants::{DICT_SIZE_RATIO_META_KEY, STRUCTURAL_ENCODING_META_KEY};
9356        use crate::testing::{TestCases, check_round_trip_encoding_of_data};
9357        use arrow_array::{ArrayRef, Float64Array};
9358        use std::collections::HashMap;
9359        use std::sync::Arc;
9360
9361        // Low cardinality with poor RLE opportunity.
9362        let values = (0..1000)
9363            .map(|i| match i % 3 {
9364                0 => 0.1f64,
9365                1 => 0.2f64,
9366                _ => 0.3f64,
9367            })
9368            .collect::<Vec<_>>();
9369        let array = Arc::new(Float64Array::from(values)) as ArrayRef;
9370
9371        let mut metadata = HashMap::new();
9372        metadata.insert(
9373            STRUCTURAL_ENCODING_META_KEY.to_string(),
9374            STRUCTURAL_ENCODING_MINIBLOCK.to_string(),
9375        );
9376        metadata.insert(DICT_SIZE_RATIO_META_KEY.to_string(), "0.99".to_string());
9377
9378        let test_cases = TestCases::default()
9379            .with_u32_structural_encodings()
9380            .with_batch_size(1000)
9381            .with_range(0..1000)
9382            .with_indices(vec![0, 1, 10, 999])
9383            .with_expected_encoding("dictionary");
9384
9385        check_round_trip_encoding_of_data(vec![array], &test_cases, metadata).await;
9386    }
9387
9388    #[test]
9389    fn test_miniblock_dictionary_out_of_line_bitpacking_decode() {
9390        let rows = 10_000;
9391        let unique_values = 2_000;
9392
9393        let dictionary_encoding =
9394            ProtobufUtils21::out_of_line_bitpacking(64, ProtobufUtils21::flat(11, None));
9395        let layout = pb21::MiniBlockLayout {
9396            rep_compression: None,
9397            def_compression: None,
9398            value_compression: Some(ProtobufUtils21::flat(64, None)),
9399            dictionary: Some(dictionary_encoding),
9400            num_dictionary_items: unique_values,
9401            layers: vec![pb21::RepDefLayer::RepdefAllValidItem as i32],
9402            num_buffers: 1,
9403            repetition_index_depth: 0,
9404            num_items: rows,
9405            has_large_chunk: false,
9406        };
9407
9408        let buffer_offsets_and_sizes = vec![(0, 0), (0, 0), (0, 0)];
9409        let scheduler = super::MiniBlockScheduler::try_new(
9410            &buffer_offsets_and_sizes,
9411            /*priority=*/ 0,
9412            /*items_in_page=*/ rows,
9413            &layout,
9414            &DefaultDecompressionStrategy::default(),
9415        )
9416        .unwrap();
9417
9418        let dictionary = scheduler.dictionary.unwrap();
9419        assert_eq!(dictionary.num_dictionary_items, unique_values);
9420        assert_eq!(
9421            dictionary.dictionary_data_alignment,
9422            crate::encoder::MIN_PAGE_BUFFER_ALIGNMENT
9423        );
9424    }
9425
9426    // Dictionary encoding decision tests
9427    fn create_test_fixed_data_block(
9428        num_values: u64,
9429        cardinality: u64,
9430        bits_per_value: u64,
9431    ) -> DataBlock {
9432        assert!(cardinality > 0);
9433        assert!(cardinality <= num_values);
9434        let block_info = BlockInfo::default();
9435
9436        assert_eq!(bits_per_value % 8, 0);
9437        let data = match bits_per_value {
9438            32 => {
9439                let values = (0..num_values)
9440                    .map(|i| (i % cardinality) as u32)
9441                    .collect::<Vec<_>>();
9442                crate::buffer::LanceBuffer::reinterpret_vec(values)
9443            }
9444            64 => {
9445                let values = (0..num_values).map(|i| i % cardinality).collect::<Vec<_>>();
9446                crate::buffer::LanceBuffer::reinterpret_vec(values)
9447            }
9448            128 => {
9449                let values = (0..num_values)
9450                    .map(|i| (i % cardinality) as u128)
9451                    .collect::<Vec<_>>();
9452                crate::buffer::LanceBuffer::reinterpret_vec(values)
9453            }
9454            _ => unreachable!(),
9455        };
9456        DataBlock::FixedWidth(FixedWidthDataBlock {
9457            bits_per_value,
9458            data,
9459            num_values,
9460            block_info,
9461        })
9462    }
9463
9464    /// Helper to create VariableWidth (string) test data block with exact cardinality
9465    fn create_test_variable_width_block(num_values: u64, cardinality: u64) -> DataBlock {
9466        use arrow_array::StringArray;
9467
9468        assert!(cardinality <= num_values && cardinality > 0);
9469
9470        let mut values = Vec::with_capacity(num_values as usize);
9471        for i in 0..num_values {
9472            values.push(format!("value_{:016}", i % cardinality));
9473        }
9474
9475        let array = StringArray::from(values);
9476        DataBlock::from_array(Arc::new(array) as ArrayRef)
9477    }
9478
9479    fn create_sorted_string_array(num_values: u64, cardinality: u64) -> ArrayRef {
9480        use arrow_array::StringArray;
9481
9482        assert!(cardinality <= num_values && cardinality > 0);
9483
9484        let mut values = Vec::with_capacity(num_values as usize);
9485        for i in 0..num_values {
9486            let value_idx = i * cardinality / num_values;
9487            values.push(format!("value_{:016}", value_idx));
9488        }
9489
9490        Arc::new(StringArray::from(values)) as ArrayRef
9491    }
9492
9493    fn create_sorted_variable_width_block(num_values: u64, cardinality: u64) -> DataBlock {
9494        DataBlock::from_array(create_sorted_string_array(num_values, cardinality))
9495    }
9496
9497    #[test]
9498    fn test_should_dictionary_encode() {
9499        use crate::constants::DICT_SIZE_RATIO_META_KEY;
9500        use lance_core::datatypes::Field as LanceField;
9501
9502        // Create data where dict encoding saves space
9503        let block = create_test_variable_width_block(1000, 10);
9504
9505        let mut metadata = HashMap::new();
9506        metadata.insert(DICT_SIZE_RATIO_META_KEY.to_string(), "0.8".to_string());
9507        let arrow_field =
9508            arrow_schema::Field::new("test", DataType::Utf8, false).with_metadata(metadata);
9509        let field = LanceField::try_from(&arrow_field).unwrap();
9510
9511        let result = PrimitiveStructuralEncoder::should_dictionary_encode(
9512            &block,
9513            &field,
9514            FixedWidthDictionaryEncoding::Exclude64Bit,
9515        );
9516
9517        assert!(
9518            result.is_some(),
9519            "Should use dictionary encode based on size"
9520        );
9521    }
9522
9523    #[test]
9524    fn test_block_sampling_detects_low_cardinality_in_short_sorted_runs() {
9525        let sample_count: usize = 4096;
9526        let num_values: u64 = 200_000;
9527        let cardinality: u64 = 8_000;
9528        let run_length = num_values / cardinality;
9529        let stride = num_values as usize / sample_count;
9530        assert!(
9531            stride > run_length as usize,
9532            "test must construct the stride > run_length case"
9533        );
9534
9535        let block = create_sorted_variable_width_block(num_values, cardinality);
9536        let sample_unique_ratio =
9537            PrimitiveStructuralEncoder::sample_unique_ratio(&block, sample_count).unwrap();
9538
9539        assert!(
9540            sample_unique_ratio.is_some_and(|ratio| ratio < 0.98),
9541            "sorted low-cardinality data must not be classified as near-unique"
9542        );
9543    }
9544
9545    #[test]
9546    fn test_should_dictionary_encode_sorted_low_cardinality() {
9547        use crate::constants::DICT_SIZE_RATIO_META_KEY;
9548        use lance_core::datatypes::Field as LanceField;
9549
9550        let block = create_sorted_variable_width_block(200_000, 8_000);
9551
9552        let mut metadata = HashMap::new();
9553        metadata.insert(DICT_SIZE_RATIO_META_KEY.to_string(), "0.8".to_string());
9554        let arrow_field =
9555            arrow_schema::Field::new("test", DataType::Utf8, false).with_metadata(metadata);
9556        let field = LanceField::try_from(&arrow_field).unwrap();
9557
9558        let result = PrimitiveStructuralEncoder::should_dictionary_encode(
9559            &block,
9560            &field,
9561            FixedWidthDictionaryEncoding::Include64Bit,
9562        );
9563
9564        assert!(
9565            result.is_some(),
9566            "sorted low-cardinality data should reach dictionary encoding"
9567        );
9568    }
9569
9570    #[test]
9571    fn test_should_not_dictionary_encode_sorted_high_cardinality_short_runs() {
9572        use crate::constants::DICT_SIZE_RATIO_META_KEY;
9573        use lance_core::datatypes::Field as LanceField;
9574
9575        let num_values = 200_002;
9576        let cardinality = 100_001;
9577        let block = create_sorted_variable_width_block(num_values, cardinality);
9578
9579        let mut metadata = HashMap::new();
9580        metadata.insert(DICT_SIZE_RATIO_META_KEY.to_string(), "0.8".to_string());
9581        let arrow_field =
9582            arrow_schema::Field::new("test", DataType::Utf8, false).with_metadata(metadata);
9583        let field = LanceField::try_from(&arrow_field).unwrap();
9584
9585        let result = PrimitiveStructuralEncoder::should_dictionary_encode(
9586            &block,
9587            &field,
9588            FixedWidthDictionaryEncoding::Include64Bit,
9589        );
9590
9591        assert!(
9592            result.is_none(),
9593            "sorted high-cardinality short runs should not trigger a full dictionary probe"
9594        );
9595    }
9596
9597    #[tokio::test]
9598    async fn test_encode_sorted_low_cardinality_uses_dictionary_layout() {
9599        use crate::constants::DICT_SIZE_RATIO_META_KEY;
9600
9601        let mut metadata = HashMap::new();
9602        metadata.insert(DICT_SIZE_RATIO_META_KEY.to_string(), "0.8".to_string());
9603        let field = arrow_schema::Field::new("test", DataType::Utf8, false).with_metadata(metadata);
9604        let array = create_sorted_string_array(200_000, 8_000);
9605
9606        let page = encode_first_page(field, array, TestEncoding::StructuralU32).await;
9607        let _ = dictionary_encoding_from_page(&page);
9608    }
9609
9610    #[test]
9611    fn test_should_not_dictionary_encode_unsupported_bits() {
9612        use crate::constants::DICT_SIZE_RATIO_META_KEY;
9613        use lance_core::datatypes::Field as LanceField;
9614
9615        let block = create_test_fixed_data_block(1000, 1000, 32);
9616
9617        let mut metadata = HashMap::new();
9618        metadata.insert(DICT_SIZE_RATIO_META_KEY.to_string(), "0.8".to_string());
9619        let arrow_field =
9620            arrow_schema::Field::new("test", DataType::Int32, false).with_metadata(metadata);
9621        let field = LanceField::try_from(&arrow_field).unwrap();
9622
9623        let result = PrimitiveStructuralEncoder::should_dictionary_encode(
9624            &block,
9625            &field,
9626            FixedWidthDictionaryEncoding::Exclude64Bit,
9627        );
9628
9629        assert!(
9630            result.is_none(),
9631            "Should not use dictionary encode for unsupported bit width"
9632        );
9633    }
9634
9635    #[test]
9636    fn test_should_not_dictionary_encode_near_unique_sample() {
9637        use crate::constants::DICT_SIZE_RATIO_META_KEY;
9638        use lance_core::datatypes::Field as LanceField;
9639
9640        let num_values = 5000;
9641        let block = create_test_variable_width_block(num_values, num_values);
9642
9643        let mut metadata = HashMap::new();
9644        metadata.insert(DICT_SIZE_RATIO_META_KEY.to_string(), "1.0".to_string());
9645        let arrow_field =
9646            arrow_schema::Field::new("test", DataType::Utf8, false).with_metadata(metadata);
9647        let field = LanceField::try_from(&arrow_field).unwrap();
9648
9649        let result = PrimitiveStructuralEncoder::should_dictionary_encode(
9650            &block,
9651            &field,
9652            FixedWidthDictionaryEncoding::Exclude64Bit,
9653        );
9654
9655        assert!(
9656            result.is_none(),
9657            "Should not probe dictionary encoding for near-unique data"
9658        );
9659    }
9660
9661    #[test]
9662    fn test_v2_1_miniblock_serializes_log_num_values_15() {
9663        let miniblocks = MiniBlockCompressed {
9664            data: vec![LanceBuffer::from(vec![1_u8; 16])],
9665            chunks: vec![
9666                MiniBlockChunk {
9667                    buffer_sizes: vec![8],
9668                    log_num_values: 15,
9669                },
9670                MiniBlockChunk {
9671                    buffer_sizes: vec![8],
9672                    log_num_values: 0,
9673                },
9674            ],
9675            num_values: 32_769,
9676        };
9677
9678        let serialized = PrimitiveStructuralEncoder::serialize_miniblocks(
9679            miniblocks,
9680            None,
9681            None,
9682            MiniblockChunkSize::U16,
9683        )
9684        .unwrap();
9685
9686        let chunk_metadata = serialized.metadata.borrow_to_typed_slice::<u16>();
9687        assert_eq!(chunk_metadata.len(), 2);
9688        assert_eq!(
9689            chunk_metadata[0] & 0x0F,
9690            15,
9691            "V2.1 metadata should use all 4 bits for log_num_values"
9692        );
9693    }
9694
9695    async fn encode_first_page(
9696        field: arrow_schema::Field,
9697        array: ArrayRef,
9698        version: TestEncoding,
9699    ) -> crate::encoder::EncodedPage {
9700        use crate::repdef::RepDefBuilder;
9701        use crate::{
9702            encoder::{
9703                ColumnIndexSequence, EncodingOptions, MIN_PAGE_BUFFER_ALIGNMENT, OutOfLineBuffers,
9704            },
9705            testing::{create_test_field_encoder, test_encoding_strategy},
9706        };
9707
9708        let lance_field = lance_core::datatypes::Field::try_from(&field).unwrap();
9709        let encoding_strategy = test_encoding_strategy(version);
9710        let mut column_index_seq = ColumnIndexSequence::default();
9711        let encoding_options = EncodingOptions {
9712            cache_bytes_per_column: 1,
9713            max_page_bytes: 32 * 1024 * 1024,
9714            keep_original_array: true,
9715            buffer_alignment: MIN_PAGE_BUFFER_ALIGNMENT,
9716        };
9717
9718        let mut encoder = create_test_field_encoder(
9719            encoding_strategy.as_ref(),
9720            &lance_field,
9721            &mut column_index_seq,
9722            &encoding_options,
9723        )
9724        .unwrap();
9725
9726        let mut external_buffers = OutOfLineBuffers::new(0, MIN_PAGE_BUFFER_ALIGNMENT);
9727        let repdef = RepDefBuilder::default();
9728        let num_rows = array.len() as u64;
9729        let mut pages = Vec::new();
9730        for task in encoder
9731            .maybe_encode(array, &mut external_buffers, repdef, 0, num_rows)
9732            .unwrap()
9733        {
9734            pages.push(task.await.unwrap());
9735        }
9736        for task in encoder.flush(&mut external_buffers).unwrap() {
9737            pages.push(task.await.unwrap());
9738        }
9739        pages.into_iter().next().unwrap()
9740    }
9741
9742    #[tokio::test]
9743    async fn test_constant_layout_out_of_line_fixed_size_binary_v2_2() {
9744        use crate::format::pb21::page_layout::Layout;
9745
9746        let val = vec![0xABu8; 33];
9747        let arr: ArrayRef = Arc::new(
9748            arrow_array::FixedSizeBinaryArray::try_from_sparse_iter_with_size(
9749                std::iter::repeat_n(Some(val.as_slice()), 256),
9750                33,
9751            )
9752            .unwrap(),
9753        );
9754        let field = arrow_schema::Field::new("c", DataType::FixedSizeBinary(33), true);
9755        let page = encode_first_page(field, arr.clone(), TestEncoding::StructuralU32).await;
9756
9757        let PageEncoding::Structural(layout) = &page.description else {
9758            panic!("Expected structural encoding");
9759        };
9760        let Layout::ConstantLayout(layout) = layout.layout.as_ref().unwrap() else {
9761            panic!("Expected constant layout in slot 2");
9762        };
9763        assert!(layout.inline_value.is_none());
9764        assert_eq!(page.data.len(), 1);
9765
9766        let test_cases = TestCases::default()
9767            .with_encoding(TestEncoding::StructuralU32)
9768            .with_page_sizes(vec![4096]);
9769        check_round_trip_encoding_of_data(vec![arr], &test_cases, HashMap::new()).await;
9770    }
9771
9772    #[tokio::test]
9773    async fn test_constant_layout_out_of_line_utf8_v2_2() {
9774        use crate::format::pb21::page_layout::Layout;
9775
9776        let arr: ArrayRef = Arc::new(arrow_array::StringArray::from_iter_values(
9777            std::iter::repeat_n("hello", 512),
9778        ));
9779        let field = arrow_schema::Field::new("c", DataType::Utf8, true);
9780        let page = encode_first_page(field, arr.clone(), TestEncoding::StructuralU32).await;
9781
9782        let PageEncoding::Structural(layout) = &page.description else {
9783            panic!("Expected structural encoding");
9784        };
9785        let Layout::ConstantLayout(layout) = layout.layout.as_ref().unwrap() else {
9786            panic!("Expected constant layout in slot 2");
9787        };
9788        assert!(layout.inline_value.is_none());
9789        assert_eq!(page.data.len(), 1);
9790
9791        let test_cases = TestCases::default()
9792            .with_encoding(TestEncoding::StructuralU32)
9793            .with_page_sizes(vec![4096]);
9794        check_round_trip_encoding_of_data(vec![arr], &test_cases, HashMap::new()).await;
9795    }
9796
9797    #[tokio::test]
9798    async fn test_constant_layout_nullable_item_v2_2() {
9799        use crate::format::pb21::page_layout::Layout;
9800
9801        let arr: ArrayRef = Arc::new(arrow_array::Int32Array::from(vec![
9802            Some(7),
9803            None,
9804            Some(7),
9805            None,
9806            Some(7),
9807        ]));
9808        let field = arrow_schema::Field::new("c", DataType::Int32, true);
9809        let page = encode_first_page(field, arr.clone(), TestEncoding::StructuralU32).await;
9810
9811        let PageEncoding::Structural(layout) = &page.description else {
9812            panic!("Expected structural encoding");
9813        };
9814        let Layout::ConstantLayout(layout) = layout.layout.as_ref().unwrap() else {
9815            panic!("Expected constant layout in slot 2");
9816        };
9817        assert!(layout.inline_value.is_some());
9818        assert_eq!(page.data.len(), 2);
9819
9820        let test_cases = TestCases::default()
9821            .with_encoding(TestEncoding::StructuralU32)
9822            .with_page_sizes(vec![4096]);
9823        check_round_trip_encoding_of_data(vec![arr], &test_cases, HashMap::new()).await;
9824    }
9825
9826    #[tokio::test]
9827    async fn test_constant_layout_list_repdef_v2_2() {
9828        use crate::format::pb21::page_layout::Layout;
9829        use arrow_array::builder::{Int32Builder, ListBuilder};
9830
9831        let mut builder = ListBuilder::new(Int32Builder::new());
9832        builder.values().append_value(7);
9833        builder.values().append_null();
9834        builder.values().append_value(7);
9835        builder.append(true);
9836
9837        builder.append(true);
9838
9839        builder.values().append_value(7);
9840        builder.append(true);
9841
9842        builder.append_null();
9843
9844        let arr: ArrayRef = Arc::new(builder.finish());
9845        let field = arrow_schema::Field::new(
9846            "c",
9847            DataType::List(Arc::new(arrow_schema::Field::new(
9848                "item",
9849                DataType::Int32,
9850                true,
9851            ))),
9852            true,
9853        );
9854        let page = encode_first_page(field, arr.clone(), TestEncoding::StructuralU32).await;
9855
9856        let PageEncoding::Structural(layout) = &page.description else {
9857            panic!("Expected structural encoding");
9858        };
9859        let Layout::ConstantLayout(layout) = layout.layout.as_ref().unwrap() else {
9860            panic!("Expected constant layout in slot 2");
9861        };
9862        assert!(layout.inline_value.is_some());
9863        assert_eq!(page.data.len(), 2);
9864
9865        let test_cases = TestCases::default()
9866            .with_encoding(TestEncoding::StructuralU32)
9867            .with_page_sizes(vec![4096]);
9868        check_round_trip_encoding_of_data(vec![arr], &test_cases, HashMap::new()).await;
9869    }
9870
9871    #[tokio::test]
9872    async fn test_constant_layout_fixed_size_list_not_used_v2_2() {
9873        use crate::format::pb21::page_layout::Layout;
9874        use arrow_array::builder::{FixedSizeListBuilder, Int32Builder};
9875
9876        let mut builder = FixedSizeListBuilder::new(Int32Builder::new(), 3);
9877        for _ in 0..64 {
9878            builder.values().append_value(1);
9879            builder.values().append_null();
9880            builder.values().append_value(3);
9881            builder.append(true);
9882        }
9883        let arr: ArrayRef = Arc::new(builder.finish());
9884        let field = arrow_schema::Field::new(
9885            "c",
9886            DataType::FixedSizeList(
9887                Arc::new(arrow_schema::Field::new("item", DataType::Int32, true)),
9888                3,
9889            ),
9890            true,
9891        );
9892        let page = encode_first_page(field, arr.clone(), TestEncoding::StructuralU32).await;
9893
9894        if let PageEncoding::Structural(layout) = &page.description {
9895            assert!(
9896                !matches!(layout.layout.as_ref().unwrap(), Layout::ConstantLayout(_)),
9897                "FixedSizeList should not use constant layout yet"
9898            );
9899        }
9900
9901        let test_cases = TestCases::default()
9902            .with_encoding(TestEncoding::StructuralU32)
9903            .with_page_sizes(vec![4096]);
9904        check_round_trip_encoding_of_data(vec![arr], &test_cases, HashMap::new()).await;
9905    }
9906
9907    #[tokio::test]
9908    async fn test_constant_layout_not_written_before_v2_2() {
9909        use crate::format::pb21::page_layout::Layout;
9910
9911        let arr: ArrayRef = Arc::new(arrow_array::Int32Array::from(vec![7; 1024]));
9912        let field = arrow_schema::Field::new("c", DataType::Int32, true);
9913        let page = encode_first_page(field, arr.clone(), TestEncoding::StructuralU16).await;
9914
9915        let PageEncoding::Structural(layout) = &page.description else {
9916            return;
9917        };
9918        assert!(
9919            !matches!(layout.layout.as_ref().unwrap(), Layout::ConstantLayout(_)),
9920            "Should not emit constant layout before v2.2"
9921        );
9922
9923        let test_cases = TestCases::default()
9924            .with_encoding(TestEncoding::StructuralU16)
9925            .with_page_sizes(vec![4096]);
9926        check_round_trip_encoding_of_data(vec![arr], &test_cases, HashMap::new()).await;
9927    }
9928
9929    #[tokio::test]
9930    async fn test_all_null_constant_layout_still_works_v2_2() {
9931        use crate::format::pb21::page_layout::Layout;
9932
9933        let arr: ArrayRef = Arc::new(arrow_array::Int32Array::from(vec![None, None, None]));
9934        let field = arrow_schema::Field::new("c", DataType::Int32, true);
9935        let page = encode_first_page(field, arr.clone(), TestEncoding::StructuralU32).await;
9936
9937        let PageEncoding::Structural(layout) = &page.description else {
9938            panic!("Expected structural encoding");
9939        };
9940        let Layout::ConstantLayout(layout) = layout.layout.as_ref().unwrap() else {
9941            panic!("Expected layout in slot 2");
9942        };
9943        assert!(layout.inline_value.is_none());
9944        assert_eq!(page.data.len(), 0);
9945
9946        let test_cases = TestCases::default()
9947            .with_encoding(TestEncoding::StructuralU32)
9948            .with_page_sizes(vec![4096]);
9949        check_round_trip_encoding_of_data(vec![arr], &test_cases, HashMap::new()).await;
9950    }
9951
9952    fn hand_built_dictionary_with_out_of_range_null_keys() -> ArrayRef {
9953        use arrow_array::{DictionaryArray, Int32Array, types::Int32Type};
9954        use arrow_buffer::NullBuffer;
9955
9956        let keys = Int32Array::new(
9957            vec![0, 7, 7].into(),
9958            Some(NullBuffer::from(vec![true, false, false])),
9959        );
9960        let values = Arc::new(StringArray::from(vec!["a"]));
9961        Arc::new(DictionaryArray::<Int32Type>::try_new(keys, values).unwrap()) as ArrayRef
9962    }
9963
9964    fn concatenated_dictionary_with_out_of_range_null_keys() -> ArrayRef {
9965        use arrow_array::{builder::StringDictionaryBuilder, new_null_array, types::Int32Type};
9966
9967        let mut builder = StringDictionaryBuilder::<Int32Type>::new();
9968        builder.append_value("a");
9969        for _ in 0..7 {
9970            builder.append_null();
9971        }
9972        let valued = Arc::new(builder.finish()) as ArrayRef;
9973        let all_null = new_null_array(valued.data_type(), 8);
9974        arrow_select::concat::concat(&[valued.as_ref(), all_null.as_ref()]).unwrap()
9975    }
9976
9977    #[rstest::rstest]
9978    #[case::hand_built(hand_built_dictionary_with_out_of_range_null_keys())]
9979    #[case::concatenated(concatenated_dictionary_with_out_of_range_null_keys())]
9980    #[tokio::test]
9981    async fn test_dictionary_out_of_range_null_keys_round_trip(#[case] dictionary: ArrayRef) {
9982        let test_cases = TestCases::default()
9983            .with_encoding(TestEncoding::StructuralU32)
9984            .with_page_sizes(vec![4096]);
9985
9986        check_round_trip_encoding_of_data(vec![dictionary], &test_cases, HashMap::new()).await;
9987    }
9988
9989    #[test]
9990    fn test_encode_decode_complex_all_null_vals_roundtrip() {
9991        use crate::compression::{DecompressionStrategy, DefaultDecompressionStrategy};
9992
9993        let values: Arc<[u16]> = Arc::from((0..2048).map(|i| (i % 5) as u16).collect::<Vec<u16>>());
9994
9995        let compression_strategy = crate::testing::test_compression_strategy(
9996            TestEncoding::StructuralU16,
9997            crate::compression_config::CompressionParams::default(),
9998        );
9999        let decompression_strategy = DefaultDecompressionStrategy::default();
10000
10001        let (compressed_buf, encoding) = PrimitiveStructuralEncoder::encode_complex_all_null_vals(
10002            &values,
10003            compression_strategy.as_ref(),
10004        )
10005        .unwrap();
10006
10007        let decompressor = decompression_strategy
10008            .create_block_decompressor(&encoding)
10009            .unwrap();
10010        let decompressed = decompressor
10011            .decompress(Some(compressed_buf), values.len() as u64)
10012            .unwrap();
10013        let decompressed_fixed_width = decompressed.as_fixed_width().unwrap();
10014        assert_eq!(decompressed_fixed_width.num_values, values.len() as u64);
10015        assert_eq!(decompressed_fixed_width.bits_per_value, 16);
10016        let rep_result = decompressed_fixed_width.data.borrow_to_typed_slice::<u16>();
10017        assert_eq!(rep_result.as_ref(), values.as_ref());
10018    }
10019
10020    #[tokio::test]
10021    async fn test_complex_all_null_compression_gated_by_version() {
10022        use crate::format::pb21::page_layout::Layout;
10023        use arrow_array::ListArray;
10024
10025        let list_array = ListArray::from_iter_primitive::<arrow_array::types::Int32Type, _, _>(
10026            (0..1000).map(|i| if i % 2 == 0 { None } else { Some(vec![]) }),
10027        );
10028        let arr: ArrayRef = Arc::new(list_array);
10029        let field = arrow_schema::Field::new(
10030            "c",
10031            DataType::List(Arc::new(arrow_schema::Field::new(
10032                "item",
10033                DataType::Int32,
10034                true,
10035            ))),
10036            true,
10037        );
10038
10039        let page_v21 =
10040            encode_first_page(field.clone(), arr.clone(), TestEncoding::StructuralU16).await;
10041        let PageEncoding::Structural(layout_v21) = &page_v21.description else {
10042            panic!("Expected structural encoding");
10043        };
10044        let Layout::ConstantLayout(layout_v21) = layout_v21.layout.as_ref().unwrap() else {
10045            panic!("Expected constant layout");
10046        };
10047        assert!(layout_v21.rep_compression.is_none());
10048        assert!(layout_v21.def_compression.is_none());
10049        assert_eq!(layout_v21.num_rep_values, 0);
10050        assert_eq!(layout_v21.num_def_values, 0);
10051
10052        let page_v22 = encode_first_page(field, arr, TestEncoding::StructuralU32).await;
10053        let PageEncoding::Structural(layout_v22) = &page_v22.description else {
10054            panic!("Expected structural encoding");
10055        };
10056        let Layout::ConstantLayout(layout_v22) = layout_v22.layout.as_ref().unwrap() else {
10057            panic!("Expected constant layout");
10058        };
10059        assert!(layout_v22.def_compression.is_some());
10060        assert!(layout_v22.num_def_values > 0);
10061    }
10062
10063    #[tokio::test]
10064    async fn test_complex_all_null_round_trip() {
10065        use arrow_array::ListArray;
10066
10067        let list_array = ListArray::from_iter_primitive::<arrow_array::types::Int32Type, _, _>(
10068            (0..1000).map(|i| if i % 2 == 0 { None } else { Some(vec![]) }),
10069        );
10070
10071        let test_cases = TestCases::default().with_u32_structural_encodings();
10072        check_round_trip_encoding_of_data(vec![Arc::new(list_array)], &test_cases, HashMap::new())
10073            .await;
10074    }
10075
10076    #[tokio::test]
10077    async fn test_complex_all_null_constant_def_round_trip() {
10078        use arrow_array::ListArray;
10079
10080        // Every row is a null list => constant def levels => a single RLE run,
10081        // exercising the lazy run-form decode end to end.
10082        let list_array = ListArray::from_iter_primitive::<arrow_array::types::Int32Type, _, _>(
10083            (0..5000).map(|_| None::<Vec<Option<i32>>>),
10084        );
10085
10086        let test_cases = TestCases::default().with_u32_structural_encodings();
10087        check_round_trip_encoding_of_data(vec![Arc::new(list_array)], &test_cases, HashMap::new())
10088            .await;
10089    }
10090
10091    fn encoded_u16_frame(levels: &[u16], run_length_width: RunLengthWidth) -> LanceBuffer {
10092        let block = DataBlock::FixedWidth(FixedWidthDataBlock {
10093            data: LanceBuffer::reinterpret_slice(Arc::from(levels)),
10094            bits_per_value: 16,
10095            num_values: levels.len() as u64,
10096            block_info: BlockInfo::new(),
10097        });
10098        BlockCompressor::compress(&RleEncoder::with_run_length_width(run_length_width), block)
10099            .unwrap()
10100            .0
10101            .unwrap()
10102    }
10103
10104    fn encoded_u16_runs(levels: &[u16], run_length_width: RunLengthWidth) -> RleRuns {
10105        let frame = encoded_u16_frame(levels, run_length_width);
10106        RleDecompressor::with_run_length_width(16, run_length_width)
10107            .decode_u16_runs(frame, levels.len() as u64)
10108            .unwrap()
10109    }
10110
10111    #[test]
10112    fn miniblock_levels_use_one_count_for_mixed_codecs() {
10113        let actual_num_levels = usize::from(u16::MAX) + 8;
10114        let levels = vec![1_u16; actual_num_levels];
10115        let rep_frame = encoded_u16_frame(&levels, RunLengthWidth::U8);
10116        let rep_decompressor = RleDecompressor::new(16);
10117        let def_frame = LanceBuffer::reinterpret_slice(Arc::from(levels.clone()));
10118        let def_decompressor = ValueDecompressor::from_flat(&pb21::Flat {
10119            bits_per_value: 16,
10120            data: None,
10121        });
10122
10123        let num_levels = DecodeMiniBlockTask::resolve_num_levels(
10124            Some(&rep_decompressor),
10125            Some(&rep_frame),
10126            Some(&def_decompressor),
10127            Some(&def_frame),
10128            7,
10129        )
10130        .unwrap();
10131        assert_eq!(num_levels, actual_num_levels as u64);
10132
10133        let rep =
10134            DecodeMiniBlockTask::decode_levels(&rep_decompressor, rep_frame, num_levels).unwrap();
10135        let def =
10136            DecodeMiniBlockTask::decode_levels(&def_decompressor, def_frame, num_levels).unwrap();
10137        assert_eq!(rep.as_ref(), levels);
10138        assert_eq!(def, rep);
10139    }
10140
10141    #[test]
10142    fn miniblock_levels_reject_non_wrapped_count_mismatch() {
10143        let frame = encoded_u16_frame(&[1_u16; 8], RunLengthWidth::U8);
10144        let decompressor = RleDecompressor::new(16);
10145
10146        let error = DecodeMiniBlockTask::resolve_num_levels(
10147            Some(&decompressor),
10148            Some(&frame),
10149            None,
10150            None,
10151            7,
10152        )
10153        .unwrap_err();
10154        assert!(matches!(error, lance_core::Error::InvalidInput { .. }));
10155        assert!(error.to_string().contains("not congruent modulo 65536"));
10156    }
10157
10158    #[test]
10159    fn miniblock_levels_reject_cross_stream_count_disagreement() {
10160        let rep_levels = vec![1_u16; usize::from(u16::MAX) + 8];
10161        let def_levels = vec![1_u16; rep_levels.len() + usize::from(u16::MAX) + 1];
10162        let rep_frame = encoded_u16_frame(&rep_levels, RunLengthWidth::U8);
10163        let def_frame = encoded_u16_frame(&def_levels, RunLengthWidth::U8);
10164        let decompressor = RleDecompressor::new(16);
10165
10166        let error = DecodeMiniBlockTask::resolve_num_levels(
10167            Some(&decompressor),
10168            Some(&rep_frame),
10169            Some(&decompressor),
10170            Some(&def_frame),
10171            7,
10172        )
10173        .unwrap_err();
10174        assert!(matches!(error, lance_core::Error::InvalidInput { .. }));
10175        assert!(
10176            error
10177                .to_string()
10178                .contains("structural streams disagree on the level count")
10179        );
10180    }
10181
10182    fn physical_levels(levels: &[u16]) -> LazyLevels {
10183        LazyLevels::Runs(Arc::new(RunStorage::Physical(
10184            encoded_u16_runs(levels, RunLengthWidth::U8).into_owned(),
10185        )))
10186    }
10187
10188    fn coalesced_levels(levels: &[u16]) -> LazyLevels {
10189        let mut values = Vec::new();
10190        let mut ends = RunEndsBuilder::with_capacity(levels.len(), levels.len());
10191        for (index, &value) in levels.iter().enumerate() {
10192            if values.last() == Some(&value) {
10193                ends.set_last(index + 1).unwrap();
10194            } else {
10195                values.push(value);
10196                ends.push(index + 1).unwrap();
10197            }
10198        }
10199        LazyLevels::Runs(Arc::new(RunStorage::Coalesced {
10200            values: values.into_boxed_slice(),
10201            ends: ends.finish(),
10202        }))
10203    }
10204
10205    #[test]
10206    fn lazy_levels_runs_match_dense() {
10207        // Runs: 3x2, 1x1, 3x3, 0x2  =>  [3,3,1,3,3,3,0,0]
10208        let expanded: Vec<u16> = vec![3, 3, 1, 3, 3, 3, 0, 0];
10209        let coalesced = coalesced_levels(&expanded);
10210        let physical = physical_levels(&expanded);
10211        let dense = LazyLevels::Dense(ScalarBuffer::<u16>::from(expanded.clone()));
10212        let n = expanded.len();
10213
10214        assert_eq!(coalesced.len(), n);
10215        assert_eq!(physical.len(), n);
10216        assert_eq!(dense.len(), n);
10217
10218        // Rows begin at each `max_rep` (3) position; row `num_rows` maps to `len`.
10219        let max_rep = 3u16;
10220        let row_starts: Vec<usize> = (0..n).filter(|&i| expanded[i] == max_rep).collect();
10221        for target in 0..=row_starts.len() as u64 {
10222            let want = row_starts.get(target as usize).copied().unwrap_or(n);
10223            for runs in [&coalesced, &physical] {
10224                let mut cursor = LevelCursor::default();
10225                assert_eq!(
10226                    runs.seek_row_start(&mut cursor, target, max_rep).unwrap(),
10227                    want,
10228                    "seek_row_start({target})"
10229                );
10230            }
10231            let mut c_dense = LevelCursor::default();
10232            assert_eq!(
10233                dense.seek_row_start(&mut c_dense, target, max_rep).unwrap(),
10234                want
10235            );
10236        }
10237
10238        // `count_le_cursor` (fresh cursor per range) and `extend_into` agree with
10239        // the dense reference on every sub-range.
10240        for start in 0..=n {
10241            for end in start..=n {
10242                for max in [0u16, 1, 2, 3] {
10243                    let want = expanded[start..end].iter().filter(|&&d| d <= max).count() as u64;
10244                    for runs in [&coalesced, &physical] {
10245                        let mut cursor = RunPosition::default();
10246                        assert_eq!(
10247                            runs.count_le_cursor(&mut cursor, start..end, max).0,
10248                            want,
10249                            "count_le_cursor({start}..{end}, {max})"
10250                        );
10251                    }
10252                    let mut d_cur = RunPosition::default();
10253                    assert_eq!(dense.count_le_cursor(&mut d_cur, start..end, max).0, want);
10254                }
10255                for runs in [&coalesced, &physical] {
10256                    let mut got = Vec::new();
10257                    runs.extend_into(start..end, RunPosition::default(), &mut got);
10258                    assert_eq!(
10259                        got,
10260                        expanded[start..end].to_vec(),
10261                        "extend_into({start}..{end})"
10262                    );
10263                }
10264                let mut got_dense = Vec::new();
10265                dense.extend_into(start..end, RunPosition::default(), &mut got_dense);
10266                assert_eq!(got_dense, expanded[start..end].to_vec());
10267            }
10268        }
10269    }
10270
10271    #[test]
10272    fn physical_run_hints_support_deferred_materialization() {
10273        let expanded: Vec<u16> = vec![3, 3, 1, 1, 2, 2, 0, 0];
10274        let physical = physical_levels(&expanded);
10275        let LazyLevels::Runs(runs) = &physical else {
10276            panic!("expected physical runs");
10277        };
10278        let mut first_hint = RunPosition::default();
10279        runs.seek(&mut first_hint, 2);
10280        let mut second_hint = RunPosition::default();
10281        runs.seek(&mut second_hint, 6);
10282
10283        let mut second = Vec::new();
10284        physical.extend_into(6..8, second_hint, &mut second);
10285        let mut first = Vec::new();
10286        physical.extend_into(2..4, first_hint, &mut first);
10287        assert_eq!(second, expanded[6..8]);
10288        assert_eq!(first, expanded[2..4]);
10289    }
10290
10291    /// Fuzz parity for the run-oriented complex-all-null drain: the cursor walk
10292    /// over `LazyLevels` must yield the exact level slices and visible
10293    /// count that a brute-force reference over the fully expanded levels does, for
10294    /// dense, physical-run, and coalesced-run forms and arbitrarily shaped range requests.
10295    mod complex_all_null_drain_parity {
10296        use std::ops::Range;
10297
10298        use arrow_buffer::ScalarBuffer;
10299        use proptest::prelude::*;
10300
10301        use super::super::{LazyLevels, LevelCursor, RunPosition};
10302        use super::{coalesced_levels, physical_levels};
10303        use crate::Result;
10304
10305        #[derive(Debug, Clone)]
10306        struct DrainInput {
10307            max_rep: u16,
10308            max_visible: u16,
10309            rep: Option<Vec<u16>>,
10310            def: Option<Vec<u16>>,
10311            ranges: Vec<Range<u64>>,
10312        }
10313
10314        fn dense_levels(levels: &[u16]) -> LazyLevels {
10315            LazyLevels::Dense(ScalarBuffer::from(levels.to_vec()))
10316        }
10317
10318        fn rle_levels(levels: &[u16]) -> LazyLevels {
10319            coalesced_levels(levels)
10320        }
10321
10322        fn seek(
10323            rep: Option<&LazyLevels>,
10324            cursor: &mut LevelCursor,
10325            row: u64,
10326            max_rep: u16,
10327        ) -> Result<usize> {
10328            match rep {
10329                Some(rep) => rep.seek_row_start(cursor, row, max_rep),
10330                None => {
10331                    cursor.row = row;
10332                    cursor.level = row as usize;
10333                    Ok(row as usize)
10334                }
10335            }
10336        }
10337
10338        /// Mirror of `ComplexAllNullPageDecoder::drain`, driving the real
10339        /// `seek_row_start` / `count_le_cursor` with monotonic cursors.
10340        fn simulate_drain(
10341            rep: Option<&LazyLevels>,
10342            def: Option<&LazyLevels>,
10343            max_rep: u16,
10344            max_visible: u16,
10345            ranges: &[Range<u64>],
10346        ) -> Result<(Vec<Range<usize>>, u64)> {
10347            let mut rep_cursor = LevelCursor::default();
10348            let mut def_run_cursor = RunPosition::default();
10349            let mut slices: Vec<Range<usize>> = Vec::new();
10350            let mut visible = 0u64;
10351            for range in ranges {
10352                let level_start = seek(rep, &mut rep_cursor, range.start, max_rep)?;
10353                let level_end = seek(rep, &mut rep_cursor, range.end, max_rep)?;
10354                visible += match def {
10355                    Some(def) => {
10356                        def.count_le_cursor(
10357                            &mut def_run_cursor,
10358                            level_start..level_end,
10359                            max_visible,
10360                        )
10361                        .0
10362                    }
10363                    None => (level_end - level_start) as u64,
10364                };
10365                match slices.last_mut() {
10366                    Some(last) if last.end == level_start => last.end = level_end,
10367                    _ => slices.push(level_start..level_end),
10368                }
10369            }
10370            Ok((slices, visible))
10371        }
10372
10373        /// Independent brute-force reference over fully expanded levels.
10374        fn reference_drain(
10375            rep: Option<&[u16]>,
10376            def: Option<&[u16]>,
10377            max_rep: u16,
10378            max_visible: u16,
10379            ranges: &[Range<u64>],
10380        ) -> (Vec<Range<usize>>, u64) {
10381            let total_levels = rep
10382                .map(|r| r.len())
10383                .or_else(|| def.map(|d| d.len()))
10384                .unwrap_or(0);
10385            // Level index where each row starts (or `total_levels` for the end row).
10386            let row_starts: Vec<usize> = match rep {
10387                Some(rep) => (0..rep.len()).filter(|&i| rep[i] == max_rep).collect(),
10388                None => (0..total_levels).collect(),
10389            };
10390            let level_of_row = |row: u64| {
10391                row_starts
10392                    .get(row as usize)
10393                    .copied()
10394                    .unwrap_or(total_levels)
10395            };
10396
10397            let mut slices: Vec<Range<usize>> = Vec::new();
10398            let mut visible = 0u64;
10399            for range in ranges {
10400                let ls = level_of_row(range.start);
10401                let le = level_of_row(range.end);
10402                visible += match def {
10403                    Some(def) => def[ls..le].iter().filter(|&&d| d <= max_visible).count() as u64,
10404                    None => (le - ls) as u64,
10405                };
10406                match slices.last_mut() {
10407                    Some(last) if last.end == ls => last.end = le,
10408                    _ => slices.push(ls..le),
10409                }
10410            }
10411            (slices, visible)
10412        }
10413
10414        fn ranges_strategy(num_rows: u64) -> BoxedStrategy<Vec<Range<u64>>> {
10415            if num_rows == 0 {
10416                return Just(Vec::new()).boxed();
10417            }
10418            // (gap, len) pairs; a zero gap yields ranges adjacent in row space,
10419            // which exercises the level-slice coalescing path.
10420            proptest::collection::vec((0u64..=3, 1u64..=4), 0..=8)
10421                .prop_map(move |pairs| {
10422                    let mut ranges = Vec::new();
10423                    let mut pos = 0u64;
10424                    for (gap, len) in pairs {
10425                        pos = pos.saturating_add(gap);
10426                        if pos >= num_rows {
10427                            break;
10428                        }
10429                        let end = (pos + len).min(num_rows);
10430                        ranges.push(pos..end);
10431                        pos = end;
10432                    }
10433                    ranges
10434                })
10435                .boxed()
10436        }
10437
10438        fn drain_input() -> impl Strategy<Value = DrainInput> {
10439            (
10440                1u16..=3,
10441                0u16..=3,
10442                any::<bool>(),
10443                any::<bool>(),
10444                1usize..=48,
10445            )
10446                .prop_flat_map(|(max_rep, max_visible, has_rep, has_def, len)| {
10447                    // Complex-all-null always has definition levels when there is
10448                    // no repetition, so force `def` present in that case.
10449                    let has_def = has_def || !has_rep;
10450                    let rep = if has_rep {
10451                        proptest::collection::vec(0u16..=max_rep, len)
10452                            .prop_map(move |mut v| {
10453                                // Row 0 must start at a max-rep boundary.
10454                                v[0] = max_rep;
10455                                Some(v)
10456                            })
10457                            .boxed()
10458                    } else {
10459                        Just(None).boxed()
10460                    };
10461                    let def = if has_def {
10462                        proptest::collection::vec(0u16..=(max_visible + 2), len)
10463                            .prop_map(Some)
10464                            .boxed()
10465                    } else {
10466                        Just(None).boxed()
10467                    };
10468                    (Just(max_rep), Just(max_visible), rep, def)
10469                })
10470                .prop_flat_map(|(max_rep, max_visible, rep, def)| {
10471                    let num_rows = match &rep {
10472                        Some(rep) => rep.iter().filter(|&&v| v == max_rep).count() as u64,
10473                        None => def.as_ref().map(|d| d.len() as u64).unwrap_or(0),
10474                    };
10475                    ranges_strategy(num_rows).prop_map(move |ranges| DrainInput {
10476                        max_rep,
10477                        max_visible,
10478                        rep: rep.clone(),
10479                        def: def.clone(),
10480                        ranges,
10481                    })
10482                })
10483        }
10484
10485        proptest! {
10486            #![proptest_config(ProptestConfig::with_cases(512))]
10487
10488            #[test]
10489            fn drain_matches_reference(input in drain_input()) {
10490                let DrainInput { max_rep, max_visible, rep, def, ranges } = input;
10491
10492                let reference =
10493                    reference_drain(rep.as_deref(), def.as_deref(), max_rep, max_visible, &ranges);
10494
10495                let rep_dense = rep.as_deref().map(dense_levels);
10496                let def_dense = def.as_deref().map(dense_levels);
10497                let got_dense =
10498                    simulate_drain(rep_dense.as_ref(), def_dense.as_ref(), max_rep, max_visible, &ranges)
10499                        .unwrap();
10500                prop_assert_eq!(&got_dense, &reference, "dense form diverged from reference");
10501
10502                let rep_rle = rep.as_deref().map(rle_levels);
10503                let def_rle = def.as_deref().map(rle_levels);
10504                let got_rle =
10505                    simulate_drain(rep_rle.as_ref(), def_rle.as_ref(), max_rep, max_visible, &ranges)
10506                        .unwrap();
10507                prop_assert_eq!(&got_rle, &reference, "rle form diverged from reference");
10508
10509                let rep_physical = rep.as_deref().map(physical_levels);
10510                let def_physical = def.as_deref().map(physical_levels);
10511                let got_physical =
10512                    simulate_drain(rep_physical.as_ref(), def_physical.as_ref(), max_rep, max_visible, &ranges)
10513                        .unwrap();
10514                prop_assert_eq!(&got_physical, &reference, "physical form diverged from reference");
10515            }
10516        }
10517    }
10518
10519    #[test]
10520    fn lazy_levels_runs_are_compact() {
10521        let single_run = |n: usize| {
10522            let mut ends = RunEndsBuilder::with_capacity(n, 1);
10523            ends.push(n).unwrap();
10524            LazyLevels::Runs(Arc::new(RunStorage::Coalesced {
10525                values: vec![1u16].into_boxed_slice(),
10526                ends: ends.finish(),
10527            }))
10528        };
10529        // Run-form footprint is independent of the logical length within an end width...
10530        assert_eq!(single_run(100).deep_size(), single_run(10_000).deep_size());
10531        assert!(single_run(10_000_000).deep_size() < 100);
10532        assert_eq!(single_run(10_000_000).len(), 10_000_000);
10533        // ...while Dense pays 2 bytes per value.
10534        assert_eq!(
10535            LazyLevels::Dense(ScalarBuffer::<u16>::from(vec![1u16; 1000])).deep_size(),
10536            2000
10537        );
10538    }
10539
10540    #[test]
10541    fn lazy_levels_selects_smallest_representation() {
10542        let runs = encoded_u16_runs(&[7u16; 10], RunLengthWidth::U8);
10543        assert_eq!(LazyLevels::select_plan(&runs), LevelPlan::Dense);
10544
10545        let equal_size: Vec<u16> = std::iter::repeat_n(0, 256)
10546            .chain(std::iter::repeat_n(1, 100))
10547            .chain(std::iter::repeat_n(2, 100))
10548            .collect();
10549        let runs = encoded_u16_runs(&equal_size, RunLengthWidth::U8);
10550        assert_eq!(LazyLevels::select_plan(&runs), LevelPlan::Coalesced);
10551
10552        let moderate_runs: Vec<u16> = (0..250)
10553            .flat_map(|run| std::iter::repeat_n((run % 2) as u16, 4))
10554            .collect();
10555        let runs = encoded_u16_runs(&moderate_runs, RunLengthWidth::U8);
10556        assert_eq!(LazyLevels::select_plan(&runs), LevelPlan::Physical);
10557
10558        let split_constant = vec![7u16; 5000];
10559        let runs = encoded_u16_runs(&split_constant, RunLengthWidth::U8);
10560        assert_eq!(LazyLevels::select_plan(&runs), LevelPlan::Coalesced);
10561
10562        let high_density: Vec<u16> = (0..70_000).map(|index| (index % 2) as u16).collect();
10563        let runs = encoded_u16_runs(&high_density, RunLengthWidth::U32);
10564        assert_eq!(LazyLevels::select_plan(&runs), LevelPlan::Dense);
10565    }
10566
10567    #[test]
10568    fn physical_runs_detach_from_large_encoded_frame() {
10569        let levels: Vec<u16> = (0..250)
10570            .flat_map(|run| std::iter::repeat_n((run % 2) as u16, 4))
10571            .collect();
10572        let frame = encoded_u16_frame(&levels, RunLengthWidth::U8);
10573        let frame_offset = 4096;
10574        let mut allocation = vec![0; frame_offset + frame.len() + 1_000_000];
10575        allocation[frame_offset..frame_offset + frame.len()].copy_from_slice(frame.as_ref());
10576        let frame = LanceBuffer::from(allocation).slice_with_length(frame_offset, frame.len());
10577        let runs = RleDecompressor::with_run_length_width(16, RunLengthWidth::U8)
10578            .decode_u16_runs(frame, levels.len() as u64)
10579            .unwrap();
10580
10581        assert_eq!(LazyLevels::select_plan(&runs), LevelPlan::Physical);
10582        let cached = LazyLevels::from_rle_runs(runs).unwrap();
10583        assert!(
10584            matches!(cached, LazyLevels::Runs(ref runs) if matches!(runs.as_ref(), RunStorage::Physical(_)))
10585        );
10586        assert_eq!(cached.len(), levels.len());
10587        assert!(cached.deep_size() < 4096);
10588    }
10589
10590    #[test]
10591    fn complex_all_null_levels_reject_invalid_values_and_lengths() {
10592        let invalid_levels = vec![0u16, 3];
10593        for levels in [
10594            LazyLevels::Dense(ScalarBuffer::from(invalid_levels.clone())),
10595            physical_levels(&invalid_levels),
10596            coalesced_levels(&invalid_levels),
10597        ] {
10598            let error = validate_complex_all_null_levels(&None, &Some(levels), 0, 2).unwrap_err();
10599            assert!(matches!(error, lance_core::Error::InvalidInput { .. }));
10600            assert!(error.to_string().contains("Invalid definition level 3"));
10601        }
10602
10603        let rep = Some(LazyLevels::Dense(ScalarBuffer::from(vec![0u16; 2])));
10604        let def = Some(LazyLevels::Dense(ScalarBuffer::from(vec![0u16])));
10605        let error = validate_complex_all_null_levels(&rep, &def, 0, 0).unwrap_err();
10606        assert!(matches!(error, lance_core::Error::InvalidInput { .. }));
10607        assert!(
10608            error
10609                .to_string()
10610                .contains("repetition has 2, definition has 1")
10611        );
10612    }
10613
10614    #[test]
10615    fn block_levels_reject_malformed_payload_size() {
10616        let block = DataBlock::FixedWidth(FixedWidthDataBlock {
10617            data: LanceBuffer::from(vec![0]),
10618            bits_per_value: 16,
10619            num_values: 1,
10620            block_info: BlockInfo::new(),
10621        });
10622        let error = dense_levels_from_block(block, 1, "definition").unwrap_err();
10623        assert!(matches!(error, lance_core::Error::InvalidInput { .. }));
10624        assert!(
10625            error
10626                .to_string()
10627                .contains("expected 2 bytes for 1 values, got 1")
10628        );
10629    }
10630
10631    #[test]
10632    fn complex_all_null_level_codec_validates_rle_metadata() {
10633        let encoding = pb21::CompressiveEncoding {
10634            compression: Some(Compression::Rle(Box::new(pb21::Rle {
10635                values: None,
10636                run_lengths: Some(Box::new(ProtobufUtils21::flat(8, None))),
10637            }))),
10638        };
10639
10640        let error = LevelCodec::try_new(Some(&encoding), &DefaultDecompressionStrategy::default())
10641            .unwrap_err();
10642        assert!(matches!(error, lance_core::Error::InvalidInput { .. }));
10643        assert!(
10644            error
10645                .to_string()
10646                .contains("RLE compression missing values encoding")
10647        );
10648    }
10649
10650    // https://github.com/lance-format/lance/issues/6681
10651    #[tokio::test]
10652    async fn test_sparse_boolean_list_roundtrip() {
10653        use arrow_array::builder::{BooleanBuilder, ListBuilder};
10654
10655        let mut list_builder = ListBuilder::new(BooleanBuilder::new());
10656        for i in 0..1000i32 {
10657            if i % 64 == 0 {
10658                // Alternate true/false so the array is not constant (constant path avoids the bug).
10659                list_builder.values().append_value(i % 128 == 0);
10660                list_builder.append(true);
10661            } else {
10662                list_builder.append(false);
10663            }
10664        }
10665        let list_array = Arc::new(list_builder.finish());
10666
10667        let test_cases = TestCases::default().with_structural_encodings();
10668        check_round_trip_encoding_of_data(vec![list_array], &test_cases, HashMap::new()).await;
10669    }
10670
10671    fn truncated_tail_details() -> std::sync::Arc<super::FullZipDecodeDetails> {
10672        use crate::compression::VariablePerValueDecompressor;
10673        use crate::encodings::physical::binary::VariableDecoder;
10674        use crate::repdef::{ControlWordParser, DefinitionInterpretation};
10675        use std::sync::Arc;
10676        Arc::new(super::FullZipDecodeDetails {
10677            value_decompressor: super::PerValueDecompressor::Variable(Arc::new(
10678                VariableDecoder::default(),
10679            )
10680                as Arc<dyn VariablePerValueDecompressor>),
10681            def_meaning: vec![DefinitionInterpretation::NullableItem].into(),
10682            ctrl_word_parser: ControlWordParser::new(0, 0),
10683            max_rep: 0,
10684            max_visible_def: 0,
10685        })
10686    }
10687
10688    fn decode_variable_full_zip(
10689        buf: Vec<u8>,
10690        bits_per_offset: u8,
10691    ) -> lance_core::Result<super::VariableFullZipDecoder> {
10692        use std::collections::VecDeque;
10693        let mut data = VecDeque::new();
10694        data.push_back(crate::buffer::LanceBuffer::from(buf));
10695        super::VariableFullZipDecoder::new(
10696            truncated_tail_details(),
10697            data,
10698            1,
10699            bits_per_offset,
10700            bits_per_offset,
10701        )
10702    }
10703
10704    /// A well-formed length prefix decodes without incident, for both widths.
10705    #[test]
10706    fn variable_full_zip_wellformed_length_prefix() {
10707        assert!(decode_variable_full_zip(0u32.to_le_bytes().to_vec(), 32).is_ok());
10708        assert!(decode_variable_full_zip(0u64.to_le_bytes().to_vec(), 64).is_ok());
10709    }
10710
10711    /// A page whose item walk ends with a partial length prefix must surface a
10712    /// corrupt-file error rather than read past the end of the buffer.
10713    ///
10714    /// This asserts the error variant and message rather than merely expecting a
10715    /// panic: before the length prefix was bounds checked, the read was
10716    /// `get_unchecked` behind a `debug_assert!`, so a debug build panicked here
10717    /// (which a `#[should_panic]` test would have accepted as a pass) while a
10718    /// release build read up to 8 bytes out of a 4 byte allocation.
10719    #[test]
10720    fn variable_full_zip_truncated_length_prefix_is_corrupt_file() {
10721        use lance_core::Error;
10722
10723        for (bits, buf_len) in [(32u8, 3usize), (64u8, 4usize)] {
10724            let err = decode_variable_full_zip(vec![0xAA; buf_len], bits)
10725                .expect_err("a truncated length prefix must not decode");
10726            assert!(
10727                matches!(err, Error::CorruptFile { .. }),
10728                "expected CorruptFile for a {}-bit prefix with {} byte(s), got: {:?}",
10729                bits,
10730                buf_len,
10731                err
10732            );
10733            let msg = err.to_string();
10734            assert!(
10735                msg.contains("truncated length prefix"),
10736                "error should say what is wrong, got: {msg}"
10737            );
10738        }
10739    }
10740}