Skip to main content

lance_encoding/encodings/logical/primitive/
miniblock_decode.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::any::Any;
5use std::collections::VecDeque;
6use std::fmt::Debug;
7use std::ops::Range;
8use std::sync::Arc;
9use std::{iter, vec};
10
11use arrow_buffer::ScalarBuffer;
12use bytes::Bytes;
13use futures::FutureExt;
14use futures::future::BoxFuture;
15use itertools::Itertools;
16use lance_core::Result;
17use lance_core::cache::{Context, DeepSizeOf};
18use lance_core::error::Error;
19use lance_core::utils::bit::pad_bytes;
20
21use crate::EncodingsIo;
22use crate::buffer::LanceBuffer;
23use crate::compression::{BlockDecompressor, DecompressionStrategy, MiniBlockDecompressor};
24use crate::data::{DataBlock, DataBlockBuilder, DictionaryDataBlock};
25use crate::decoder::{DecodePageTask, DecodedPage, StructuralPageDecoder};
26use crate::format::metadata::compressive_encoding::Compression;
27use crate::format::{EncodingUtils, metadata};
28use crate::repdef::{DefinitionInterpretation, LevelBuffer, RepDefUnraveler, max_visible_level};
29
30use super::chunk_index::{
31    ItemCounts, MiniBlockChunkIndex, PrefixSums, RowMapping, parse_nested_rep,
32};
33use super::{
34    CachedPageData, LoadedChunk, MAX_LEGACY_RLE_LEVELS, MINIBLOCK_ALIGNMENT, PageInitialization,
35    PageInitializationBuffers, PageLoadTask, StructuralPageScheduler,
36};
37
38/// A mini-block chunk that has been decoded and decompressed
39#[derive(Debug, Clone)]
40struct DecodedMiniBlockChunk {
41    rep: Option<ScalarBuffer<u16>>,
42    def: Option<ScalarBuffer<u16>>,
43    values: DataBlock,
44}
45
46/// A task to decode a one or more mini-blocks of data into an output batch
47///
48/// Note: Two batches might share the same mini-block of data.  When this happens
49/// then each batch gets a copy of the block and each batch decodes the block independently.
50///
51/// This means we have duplicated work but it is necessary to avoid having to synchronize
52/// the decoding of the block. (TODO: test this theory)
53#[derive(Debug)]
54struct DecodeMiniBlockTask {
55    rep_decompressor: Option<Arc<dyn BlockDecompressor>>,
56    def_decompressor: Option<Arc<dyn BlockDecompressor>>,
57    value_decompressor: Arc<dyn MiniBlockDecompressor>,
58    dictionary_data: Option<Arc<DataBlock>>,
59    def_meaning: Arc<[DefinitionInterpretation]>,
60    num_buffers: u64,
61    max_visible_level: u16,
62    instructions: Vec<(ChunkDrainInstructions, LoadedChunk)>,
63    has_large_chunk: bool,
64}
65
66impl DecodeMiniBlockTask {
67    fn decoded_size_bytes(&self) -> Option<u64> {
68        if self.rep_decompressor.is_some() || self.def_decompressor.is_some() {
69            return None;
70        }
71        let num_values = self
72            .instructions
73            .iter()
74            .try_fold(0_u64, |total, (instruction, _)| {
75                total.checked_add(instruction.rows_to_take)
76            })?;
77        self.value_decompressor.decoded_size_bytes(num_values)
78    }
79
80    fn resolve_num_levels(
81        rep_decompressor: Option<&dyn BlockDecompressor>,
82        rep_levels: Option<&LanceBuffer>,
83        def_decompressor: Option<&dyn BlockDecompressor>,
84        def_levels: Option<&LanceBuffer>,
85        declared_num_levels: u16,
86    ) -> Result<u64> {
87        let rep_num_levels = match (rep_decompressor, rep_levels) {
88            (Some(decompressor), Some(levels)) => decompressor.infer_num_values(levels)?,
89            (None, None) => None,
90            _ => {
91                return Err(Error::invalid_input_source(
92                    "miniblock repetition codec and payload presence disagree".into(),
93                ));
94            }
95        };
96        let def_num_levels = match (def_decompressor, def_levels) {
97            (Some(decompressor), Some(levels)) => decompressor.infer_num_values(levels)?,
98            (None, None) => None,
99            _ => {
100                return Err(Error::invalid_input_source(
101                    "miniblock definition codec and payload presence disagree".into(),
102                ));
103            }
104        };
105
106        let inferred_num_levels = match (rep_num_levels, def_num_levels) {
107            (Some(rep_num_levels), Some(def_num_levels)) if rep_num_levels != def_num_levels => {
108                return Err(Error::invalid_input_source(
109                    format!(
110                        "miniblock structural streams disagree on the level count: repetition inferred {rep_num_levels}, definition inferred {def_num_levels}"
111                    )
112                    .into(),
113                ));
114            }
115            (Some(num_levels), _) | (_, Some(num_levels)) => num_levels,
116            (None, None) => return Ok(u64::from(declared_num_levels)),
117        };
118
119        let declared_num_levels = u64::from(declared_num_levels);
120        if inferred_num_levels == declared_num_levels {
121            return Ok(inferred_num_levels);
122        }
123        let u16_modulus = u64::from(u16::MAX) + 1;
124        if inferred_num_levels <= declared_num_levels
125            || inferred_num_levels % u16_modulus != declared_num_levels
126        {
127            return Err(Error::invalid_input_source(
128                format!(
129                    "miniblock payload proves {inferred_num_levels} levels but the header declared {declared_num_levels}; the counts are not congruent modulo 65536"
130                )
131                .into(),
132            ));
133        }
134        if inferred_num_levels > MAX_LEGACY_RLE_LEVELS {
135            return Err(Error::invalid_input_source(
136                format!(
137                    "miniblock payload proves {inferred_num_levels} levels, exceeding the legacy RLE payload bound of {MAX_LEGACY_RLE_LEVELS}"
138                )
139                .into(),
140            ));
141        }
142        Ok(inferred_num_levels)
143    }
144
145    fn decode_levels(
146        decompressor: &dyn BlockDecompressor,
147        levels: LanceBuffer,
148        expected_num_levels: u64,
149    ) -> Result<ScalarBuffer<u16>> {
150        let levels = decompressor.decompress(Some(levels), expected_num_levels)?;
151        let levels = levels.as_fixed_width().ok_or_else(|| {
152            Error::invalid_input_source(
153                "miniblock levels did not decode to fixed-width data".into(),
154            )
155        })?;
156        if levels.num_values != expected_num_levels {
157            return Err(Error::invalid_input_source(
158                format!(
159                    "miniblock levels decoded {} values, expected {expected_num_levels}",
160                    levels.num_values
161                )
162                .into(),
163            ));
164        }
165        if levels.bits_per_value != 16 {
166            return Err(Error::invalid_input_source(
167                format!(
168                    "miniblock levels decoded {} bits per value, expected 16",
169                    levels.bits_per_value
170                )
171                .into(),
172            ));
173        }
174        let expected_bytes = expected_num_levels.checked_mul(2).ok_or_else(|| {
175            Error::invalid_input_source(
176                format!("miniblock level byte count overflowed for {expected_num_levels} levels")
177                    .into(),
178            )
179        })?;
180        if levels.data.len() as u64 != expected_bytes {
181            return Err(Error::invalid_input_source(
182                format!(
183                    "miniblock levels decoded {} bytes, expected {expected_bytes}",
184                    levels.data.len()
185                )
186                .into(),
187            ));
188        }
189        Ok(levels.data.borrow_to_typed_slice::<u16>())
190    }
191
192    // We are building a LevelBuffer (levels) and want to copy into it `total_len`
193    // values from `level_buf` starting at `offset`.
194    //
195    // We need to handle both the case where `levels` is None (no nulls encountered
196    // yet) and the case where `level_buf` is None (the input we are copying from has
197    // no nulls)
198    fn extend_levels(
199        range: Range<u64>,
200        levels: &mut Option<LevelBuffer>,
201        level_buf: &Option<impl AsRef<[u16]>>,
202        dest_offset: usize,
203    ) {
204        if let Some(level_buf) = level_buf {
205            if levels.is_none() {
206                // This is the first non-empty def buf we've hit, fill in the past
207                // with 0 (valid)
208                let mut new_levels_vec =
209                    LevelBuffer::with_capacity(dest_offset + (range.end - range.start) as usize);
210                new_levels_vec.extend(iter::repeat_n(0, dest_offset));
211                *levels = Some(new_levels_vec);
212            }
213            levels.as_mut().unwrap().extend(
214                level_buf.as_ref()[range.start as usize..range.end as usize]
215                    .iter()
216                    .copied(),
217            );
218        } else if let Some(levels) = levels {
219            let num_values = (range.end - range.start) as usize;
220            // This is an all-valid level_buf but we had nulls earlier and so we
221            // need to materialize it
222            levels.extend(iter::repeat_n(0, num_values));
223        }
224    }
225
226    /// Maps a range of rows to a range of items and a range of levels
227    ///
228    /// If there is no repetition information this just returns the range as-is.
229    ///
230    /// If there is repetition information then we need to do some work to figure out what
231    /// range of items corresponds to the requested range of rows.
232    ///
233    /// For example, if the data is [[1, 2, 3], [4, 5], [6, 7]] and the range is 1..2 (i.e. just row
234    /// 1) then the user actually wants items 3..5.  In the above case the rep levels would be:
235    ///
236    /// Idx: 0 1 2 3 4 5 6
237    /// Rep: 1 0 0 1 0 1 0
238    ///
239    /// So the start (1) maps to the second 1 (idx=3) and the end (2) maps to the third 1 (idx=5)
240    ///
241    /// If there are invisible items then we don't count them when calculating the range of items we
242    /// are interested in but we do count them when calculating the range of levels we are interested
243    /// in.  As a result we have to return both the item range (first return value) and the level range
244    /// (second return value).
245    ///
246    /// For example, if the data is [[1, 2, 3], [4, 5], NULL, [6, 7, 8]] and the range is 2..4 then the
247    /// user wants items 5..8 but they want levels 5..9.  In the above case the rep/def levels would be:
248    ///
249    /// Idx: 0 1 2 3 4 5 6 7 8
250    /// Rep: 1 0 0 1 0 1 1 0 0
251    /// Def: 0 0 0 0 0 1 0 0 0
252    /// Itm: 1 2 3 4 5 6 7 8
253    ///
254    /// Finally, we have to contend with the fact that chunks may or may not start with a "preamble" of
255    /// trailing values that finish up a list from the previous chunk.  In this case the first item does
256    /// not start at max_rep because it is a continuation of the previous chunk.  For our purposes we do
257    /// not consider this a "row" and so the range 0..1 will refer to the first row AFTER the preamble.
258    ///
259    /// We have a separate parameter (`preamble_action`) to control whether we want the preamble or not.
260    ///
261    /// Note that the "trailer" is considered a "row" and if we want it we should include it in the range.
262    fn map_range(
263        range: Range<u64>,
264        rep: Option<&impl AsRef<[u16]>>,
265        def: Option<&impl AsRef<[u16]>>,
266        max_rep: u16,
267        max_visible_def: u16,
268        // The total number of items (not rows) in the chunk.  This is not quite the same as
269        // rep.len() / def.len() because it doesn't count invisible items
270        total_items: u64,
271        preamble_action: PreambleAction,
272    ) -> (Range<u64>, Range<u64>) {
273        if let Some(rep) = rep {
274            let mut rep = rep.as_ref();
275            // If there is a preamble and we need to skip it then do that first.  The work is the same
276            // whether there is def information or not
277            let mut items_in_preamble = 0_u64;
278            let first_row_start = match preamble_action {
279                PreambleAction::Skip | PreambleAction::Take => {
280                    let first_row_start = if let Some(def) = def.as_ref() {
281                        let mut first_row_start = None;
282                        for (idx, (rep, def)) in rep.iter().zip(def.as_ref()).enumerate() {
283                            if *rep == max_rep {
284                                first_row_start = Some(idx as u64);
285                                break;
286                            }
287                            if *def <= max_visible_def {
288                                items_in_preamble += 1;
289                            }
290                        }
291                        first_row_start
292                    } else {
293                        let first_row_start =
294                            rep.iter().position(|&r| r == max_rep).map(|r| r as u64);
295                        items_in_preamble = first_row_start.unwrap_or(rep.len() as u64);
296                        first_row_start
297                    };
298                    // It is possible for a chunk to be entirely partial values but if it is then it
299                    // should never show up as a preamble to skip
300                    if first_row_start.is_none() {
301                        assert!(preamble_action == PreambleAction::Take);
302                        return (0..total_items, 0..rep.len() as u64);
303                    }
304                    let first_row_start = first_row_start.unwrap();
305                    rep = &rep[first_row_start as usize..];
306                    first_row_start
307                }
308                PreambleAction::Absent => {
309                    debug_assert!(rep[0] == max_rep);
310                    0
311                }
312            };
313
314            // We hit this case when all we needed was the preamble
315            if range.start == range.end {
316                debug_assert!(preamble_action == PreambleAction::Take);
317                debug_assert!(items_in_preamble <= total_items);
318                return (0..items_in_preamble, 0..first_row_start);
319            }
320            assert!(range.start < range.end);
321
322            let mut rows_seen = 0;
323            let mut new_start = 0;
324            let mut new_levels_start = 0;
325
326            if let Some(def) = def {
327                let def = &def.as_ref()[first_row_start as usize..];
328
329                // range.start == 0 always maps to 0 (even with invis items), otherwise we need to walk
330                let mut lead_invis_seen = 0;
331
332                if range.start > 0 {
333                    if def[0] > max_visible_def {
334                        lead_invis_seen += 1;
335                    }
336                    for (idx, (rep, def)) in rep.iter().zip(def).skip(1).enumerate() {
337                        if *rep == max_rep {
338                            rows_seen += 1;
339                            if rows_seen == range.start {
340                                new_start = idx as u64 + 1 - lead_invis_seen;
341                                new_levels_start = idx as u64 + 1;
342                                break;
343                            }
344                        }
345                        if *def > max_visible_def {
346                            lead_invis_seen += 1;
347                        }
348                    }
349                }
350
351                rows_seen += 1;
352
353                let mut new_end = u64::MAX;
354                let mut new_levels_end = rep.len() as u64;
355                let new_start_is_visible = def[new_levels_start as usize] <= max_visible_def;
356                let mut tail_invis_seen = if new_start_is_visible { 0 } else { 1 };
357                for (idx, (rep, def)) in rep[(new_levels_start + 1) as usize..]
358                    .iter()
359                    .zip(&def[(new_levels_start + 1) as usize..])
360                    .enumerate()
361                {
362                    if *rep == max_rep {
363                        rows_seen += 1;
364                        if rows_seen == range.end + 1 {
365                            new_end = idx as u64 + new_start + 1 - tail_invis_seen;
366                            new_levels_end = idx as u64 + new_levels_start + 1;
367                            break;
368                        }
369                    }
370                    if *def > max_visible_def {
371                        tail_invis_seen += 1;
372                    }
373                }
374
375                if new_end == u64::MAX {
376                    new_levels_end = rep.len() as u64;
377                    let total_invis_seen = lead_invis_seen + tail_invis_seen;
378                    new_end = rep.len() as u64 - total_invis_seen;
379                }
380
381                assert_ne!(new_end, u64::MAX);
382
383                // Adjust for any skipped preamble
384                if preamble_action == PreambleAction::Skip {
385                    new_start += items_in_preamble;
386                    new_end += items_in_preamble;
387                    new_levels_start += first_row_start;
388                    new_levels_end += first_row_start;
389                } else if preamble_action == PreambleAction::Take {
390                    debug_assert_eq!(new_start, 0);
391                    debug_assert_eq!(new_levels_start, 0);
392                    new_end += items_in_preamble;
393                    new_levels_end += first_row_start;
394                }
395
396                debug_assert!(new_end <= total_items);
397                (new_start..new_end, new_levels_start..new_levels_end)
398            } else {
399                // Easy case, there are no invisible items, so we don't need to check for them
400                // The items range and levels range will be the same.  We do still need to walk
401                // the rep levels to find the row boundaries
402
403                // range.start == 0 always maps to 0, otherwise we need to walk
404                if range.start > 0 {
405                    for (idx, rep) in rep.iter().skip(1).enumerate() {
406                        if *rep == max_rep {
407                            rows_seen += 1;
408                            if rows_seen == range.start {
409                                new_start = idx as u64 + 1;
410                                break;
411                            }
412                        }
413                    }
414                }
415                let mut new_end = rep.len() as u64;
416                // range.end == max_items always maps to rep.len(), otherwise we need to walk
417                if range.end < total_items {
418                    for (idx, rep) in rep[(new_start + 1) as usize..].iter().enumerate() {
419                        if *rep == max_rep {
420                            rows_seen += 1;
421                            if rows_seen == range.end {
422                                new_end = idx as u64 + new_start + 1;
423                                break;
424                            }
425                        }
426                    }
427                }
428
429                // Adjust for any skipped preamble
430                if preamble_action == PreambleAction::Skip {
431                    new_start += first_row_start;
432                    new_end += first_row_start;
433                } else if preamble_action == PreambleAction::Take {
434                    debug_assert_eq!(new_start, 0);
435                    new_end += first_row_start;
436                }
437
438                debug_assert!(new_end <= total_items);
439                (new_start..new_end, new_start..new_end)
440            }
441        } else {
442            // No repetition info, easy case, just use the range as-is and the item
443            // and level ranges are the same
444            (range.clone(), range)
445        }
446    }
447
448    // read `num_buffers` buffer sizes from `buf` starting at `offset`
449    fn read_buffer_sizes<const LARGE: bool>(
450        buf: &[u8],
451        offset: &mut usize,
452        num_buffers: u64,
453    ) -> Vec<u32> {
454        let read_size = if LARGE { 4 } else { 2 };
455        (0..num_buffers)
456            .map(|_| {
457                let bytes = &buf[*offset..*offset + read_size];
458                let size = if LARGE {
459                    u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
460                } else {
461                    // the buffer size is read from u16 but is stored as u32 after decoding for consistency
462                    u16::from_le_bytes([bytes[0], bytes[1]]) as u32
463                };
464                *offset += read_size;
465                size
466            })
467            .collect()
468    }
469
470    // Unserialize a miniblock into a collection of vectors
471    fn decode_miniblock_chunk(
472        &self,
473        buf: &LanceBuffer,
474        items_in_chunk: u64,
475    ) -> Result<DecodedMiniBlockChunk> {
476        let mut offset = 0;
477        let num_levels = u16::from_le_bytes([buf[offset], buf[offset + 1]]);
478        offset += 2;
479
480        let rep_size = if self.rep_decompressor.is_some() {
481            let rep_size = u16::from_le_bytes([buf[offset], buf[offset + 1]]);
482            offset += 2;
483            Some(rep_size)
484        } else {
485            None
486        };
487        let def_size = if self.def_decompressor.is_some() {
488            let def_size = u16::from_le_bytes([buf[offset], buf[offset + 1]]);
489            offset += 2;
490            Some(def_size)
491        } else {
492            None
493        };
494
495        let buffer_sizes = if self.has_large_chunk {
496            Self::read_buffer_sizes::<true>(buf, &mut offset, self.num_buffers)
497        } else {
498            Self::read_buffer_sizes::<false>(buf, &mut offset, self.num_buffers)
499        };
500
501        offset += pad_bytes::<MINIBLOCK_ALIGNMENT>(offset);
502
503        let rep = rep_size.map(|rep_size| {
504            let rep = buf.slice_with_length(offset, rep_size as usize);
505            offset += rep_size as usize;
506            offset += pad_bytes::<MINIBLOCK_ALIGNMENT>(offset);
507            rep
508        });
509
510        let def = def_size.map(|def_size| {
511            let def = buf.slice_with_length(offset, def_size as usize);
512            offset += def_size as usize;
513            offset += pad_bytes::<MINIBLOCK_ALIGNMENT>(offset);
514            def
515        });
516
517        let num_levels = Self::resolve_num_levels(
518            self.rep_decompressor.as_deref(),
519            rep.as_ref(),
520            self.def_decompressor.as_deref(),
521            def.as_ref(),
522            num_levels,
523        )?;
524
525        let buffers = buffer_sizes
526            .into_iter()
527            .map(|buf_size| {
528                let buf = buf.slice_with_length(offset, buf_size as usize);
529                offset += buf_size as usize;
530                offset += pad_bytes::<MINIBLOCK_ALIGNMENT>(offset);
531                buf
532            })
533            .collect::<Vec<_>>();
534
535        let values = self
536            .value_decompressor
537            .decompress(buffers, items_in_chunk)?;
538
539        let rep = rep
540            .map(|rep| {
541                Self::decode_levels(
542                    self.rep_decompressor.as_ref().unwrap().as_ref(),
543                    rep,
544                    num_levels,
545                )
546            })
547            .transpose()?;
548        let def = def
549            .map(|def| {
550                Self::decode_levels(
551                    self.def_decompressor.as_ref().unwrap().as_ref(),
552                    def,
553                    num_levels,
554                )
555            })
556            .transpose()?;
557
558        if let (Some(rep), Some(def)) = (&rep, &def)
559            && rep.len() != def.len()
560        {
561            return Err(Error::invalid_input_source(
562                format!(
563                    "miniblock structural streams decoded different level counts: repetition {}, definition {}",
564                    rep.len(),
565                    def.len()
566                )
567                .into(),
568            ));
569        }
570
571        Ok(DecodedMiniBlockChunk { rep, def, values })
572    }
573}
574
575impl DecodePageTask for DecodeMiniBlockTask {
576    fn decode(self: Box<Self>) -> Result<DecodedPage> {
577        // First, we create output buffers for the rep and def and data
578        let mut repbuf: Option<LevelBuffer> = None;
579        let mut defbuf: Option<LevelBuffer> = None;
580
581        let max_rep = self.def_meaning.iter().filter(|l| l.is_list()).count() as u16;
582
583        let estimated_size_bytes = self.decoded_size_bytes().unwrap_or_else(|| {
584            // Variable-width and rep/def encoded output sizes are not known before decoding.
585            self.instructions
586                .iter()
587                .map(|(_, chunk)| chunk.data.len() as u64)
588                .sum::<u64>()
589                * 2
590        });
591        let mut data_builder = DataBlockBuilder::with_capacity_estimate(estimated_size_bytes);
592
593        // We need to keep track of the offset into repbuf/defbuf that we are building up
594        let mut level_offset = 0;
595
596        // Pre-compute caching needs for each chunk by checking if the next chunk is the same
597        let needs_caching: Vec<bool> = self
598            .instructions
599            .windows(2)
600            .map(|w| w[0].1.chunk_idx == w[1].1.chunk_idx)
601            .chain(std::iter::once(false)) // the last one never needs caching
602            .collect();
603
604        // Cache for storing decoded chunks when beneficial
605        let mut chunk_cache: Option<(usize, DecodedMiniBlockChunk)> = None;
606
607        // Now we iterate through each instruction and process it
608        for (idx, (instructions, chunk)) in self.instructions.iter().enumerate() {
609            let should_cache_this_chunk = needs_caching[idx];
610
611            let decoded_chunk = match &chunk_cache {
612                Some((cached_chunk_idx, cached_chunk)) if *cached_chunk_idx == chunk.chunk_idx => {
613                    // Clone only when we have a cache hit (much cheaper than decoding)
614                    cached_chunk.clone()
615                }
616                _ => {
617                    // Cache miss, need to decode
618                    let decoded = self.decode_miniblock_chunk(&chunk.data, chunk.items_in_chunk)?;
619
620                    // Only update cache if this chunk will benefit the next access
621                    if should_cache_this_chunk {
622                        chunk_cache = Some((chunk.chunk_idx, decoded.clone()));
623                    }
624                    decoded
625                }
626            };
627
628            let DecodedMiniBlockChunk { rep, def, values } = decoded_chunk;
629
630            // Our instructions tell us which rows we want to take from this chunk
631            let row_range_start =
632                instructions.rows_to_skip + instructions.chunk_instructions.rows_to_skip;
633            let row_range_end = row_range_start + instructions.rows_to_take;
634
635            // We use the rep info to map the row range to an item range / levels range
636            let (item_range, level_range) = Self::map_range(
637                row_range_start..row_range_end,
638                rep.as_ref(),
639                def.as_ref(),
640                max_rep,
641                self.max_visible_level,
642                chunk.items_in_chunk,
643                instructions.preamble_action,
644            );
645            if item_range.end - item_range.start > chunk.items_in_chunk {
646                return Err(lance_core::Error::internal(format!(
647                    "Item range {:?} is greater than chunk items in chunk {:?}",
648                    item_range, chunk.items_in_chunk
649                )));
650            }
651
652            // Now we append the data to the output buffers
653            Self::extend_levels(level_range.clone(), &mut repbuf, &rep, level_offset);
654            Self::extend_levels(level_range.clone(), &mut defbuf, &def, level_offset);
655            level_offset += (level_range.end - level_range.start) as usize;
656            data_builder.append(&values, item_range)?;
657        }
658
659        let mut data = data_builder.finish();
660
661        let unraveler =
662            RepDefUnraveler::new(repbuf, defbuf, self.def_meaning.clone(), data.num_values());
663
664        if let Some(dictionary) = &self.dictionary_data {
665            // Don't decode here, that happens later (if needed)
666            let DataBlock::FixedWidth(indices) = data else {
667                return Err(lance_core::Error::internal(format!(
668                    "Expected FixedWidth DataBlock for dictionary indices, got {:?}",
669                    data
670                )));
671            };
672            data = DataBlock::Dictionary(DictionaryDataBlock::from_parts(
673                indices,
674                dictionary.as_ref().clone(),
675            ));
676        }
677
678        Ok(DecodedPage {
679            data,
680            repdef: unraveler,
681        })
682    }
683}
684
685/// Decodes mini-block formatted data.  See [`PrimitiveStructuralEncoder`] for more
686/// details on the different layouts.
687///
688/// [`PrimitiveStructuralEncoder`]: crate::encodings::logical::primitive::PrimitiveStructuralEncoder
689#[derive(Debug)]
690struct MiniBlockDecoder {
691    rep_decompressor: Option<Arc<dyn BlockDecompressor>>,
692    def_decompressor: Option<Arc<dyn BlockDecompressor>>,
693    value_decompressor: Arc<dyn MiniBlockDecompressor>,
694    def_meaning: Arc<[DefinitionInterpretation]>,
695    loaded_chunks: VecDeque<LoadedChunk>,
696    instructions: VecDeque<ChunkInstructions>,
697    offset_in_current_chunk: u64,
698    num_rows: u64,
699    num_buffers: u64,
700    dictionary: Option<Arc<DataBlock>>,
701    has_large_chunk: bool,
702}
703
704/// See [`MiniBlockScheduler`] for more details on the scheduling and decoding
705/// process for miniblock encoded data.
706impl StructuralPageDecoder for MiniBlockDecoder {
707    fn drain(&mut self, num_rows: u64) -> Result<Box<dyn DecodePageTask>> {
708        let mut items_desired = num_rows;
709        let mut need_preamble = false;
710        let mut skip_in_chunk = self.offset_in_current_chunk;
711        let mut drain_instructions = Vec::new();
712        while items_desired > 0 || need_preamble {
713            let (instructions, consumed) = self
714                .instructions
715                .front()
716                .unwrap()
717                .drain_from_instruction(&mut items_desired, &mut need_preamble, &mut skip_in_chunk);
718
719            while self.loaded_chunks.front().unwrap().chunk_idx
720                != instructions.chunk_instructions.chunk_idx
721            {
722                self.loaded_chunks.pop_front();
723            }
724            drain_instructions.push((instructions, self.loaded_chunks.front().unwrap().clone()));
725            if consumed {
726                self.instructions.pop_front();
727            }
728        }
729        // We can throw away need_preamble here because it must be false.  If it were true it would mean
730        // we were still in the middle of loading rows.  We do need to latch skip_in_chunk though.
731        self.offset_in_current_chunk = skip_in_chunk;
732
733        let max_visible_level = max_visible_level(&self.def_meaning);
734
735        Ok(Box::new(DecodeMiniBlockTask {
736            instructions: drain_instructions,
737            def_decompressor: self.def_decompressor.clone(),
738            rep_decompressor: self.rep_decompressor.clone(),
739            value_decompressor: self.value_decompressor.clone(),
740            dictionary_data: self.dictionary.clone(),
741            def_meaning: self.def_meaning.clone(),
742            num_buffers: self.num_buffers,
743            max_visible_level,
744            has_large_chunk: self.has_large_chunk,
745        }))
746    }
747
748    fn num_rows(&self) -> u64 {
749        self.num_rows
750    }
751}
752
753#[derive(Debug, Clone)]
754struct MiniBlockSchedulerDictionary {
755    // These come from the protobuf
756    dictionary_decompressor: Arc<dyn BlockDecompressor>,
757    dictionary_buf_position_and_size: (u64, u64),
758    dictionary_data_alignment: u64,
759    num_dictionary_items: u64,
760}
761
762/// State that is loaded once and cached for future lookups
763#[derive(Debug)]
764struct MiniBlockCacheableState {
765    /// Compact per-chunk index (byte ranges + row/item mapping) for the page
766    chunk_index: MiniBlockChunkIndex,
767    /// The dictionary for the page, if any
768    dictionary: Option<Arc<DataBlock>>,
769}
770
771impl DeepSizeOf for MiniBlockCacheableState {
772    fn deep_size_of_children(&self, context: &mut Context) -> usize {
773        self.chunk_index.deep_size_of_children(context)
774            + self
775                .dictionary
776                .as_ref()
777                .map(|dict| dict.data_size() as usize)
778                .unwrap_or(0)
779    }
780}
781
782impl CachedPageData for MiniBlockCacheableState {
783    fn as_arc_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync + 'static> {
784        self
785    }
786}
787
788/// A scheduler for a page that has been encoded with the mini-block layout
789///
790/// Scheduling mini-block encoded data is simple in concept and somewhat complex
791/// in practice.
792///
793/// First, during initialization, we load the chunk metadata, the repetition index,
794/// and the dictionary (these last two may not be present)
795///
796/// Then, during scheduling, we use the user's requested row ranges and the repetition
797/// index to determine which chunks we need and which rows we need from those chunks.
798///
799/// For example, if the repetition index is: [50, 3], [50, 0], [10, 0] and the range
800/// from the user is 40..60 then we need to:
801///
802///  - Read the first chunk and skip the first 40 rows, then read 10 full rows, and
803///    then read 3 items for the 11th row of our range.
804///  - Read the second chunk and read the remaining items in our 11th row and then read
805///    the remaining 9 full rows.
806///
807/// Then, if we are going to decode that in batches of 5, we need to make decode tasks.
808/// The first two decode tasks will just need the first chunk.  The third decode task will
809/// need the first chunk (for the trailer which has the 11th row in our range) and the second
810/// chunk.  The final decode task will just need the second chunk.
811///
812/// The above prose descriptions are what are represented by `ChunkInstructions` and
813/// `ChunkDrainInstructions`.
814#[derive(Debug)]
815pub struct MiniBlockScheduler {
816    // These come from the protobuf
817    pub(super) buffer_offsets_and_sizes: Vec<(u64, u64)>,
818    priority: u64,
819    items_in_page: u64,
820    repetition_index_depth: u16,
821    num_buffers: u64,
822    rep_decompressor: Option<Arc<dyn BlockDecompressor>>,
823    def_decompressor: Option<Arc<dyn BlockDecompressor>>,
824    value_decompressor: Arc<dyn MiniBlockDecompressor>,
825    def_meaning: Arc<[DefinitionInterpretation]>,
826    dictionary: Option<MiniBlockSchedulerDictionary>,
827    // This is set after initialization
828    page_meta: Option<Arc<MiniBlockCacheableState>>,
829    has_large_chunk: bool,
830}
831
832impl MiniBlockScheduler {
833    pub(super) fn try_new(
834        buffer_offsets_and_sizes: &[(u64, u64)],
835        priority: u64,
836        items_in_page: u64,
837        layout: &metadata::MiniBlockLayout,
838        decompressors: &dyn DecompressionStrategy,
839    ) -> Result<Self> {
840        let rep_decompressor = layout
841            .rep_compression
842            .as_ref()
843            .map(|rep_compression| {
844                decompressors
845                    .create_block_decompressor(rep_compression)
846                    .map(Arc::from)
847            })
848            .transpose()?;
849        let def_decompressor = layout
850            .def_compression
851            .as_ref()
852            .map(|def_compression| {
853                decompressors
854                    .create_block_decompressor(def_compression)
855                    .map(Arc::from)
856            })
857            .transpose()?;
858        let def_meaning = layout
859            .layers
860            .iter()
861            .map(|l| EncodingUtils::repdef_layer_to_def_interp(*l))
862            .collect::<Vec<_>>();
863        let value_decompressor = decompressors.create_miniblock_decompressor(
864            layout.value_compression.as_ref().unwrap(),
865            decompressors,
866        )?;
867
868        let dictionary = if let Some(dictionary_encoding) = layout.dictionary.as_ref() {
869            let num_dictionary_items = layout.num_dictionary_items;
870            let dictionary_decompressor = decompressors
871                .create_block_decompressor(dictionary_encoding)?
872                .into();
873            let dictionary_data_alignment = match dictionary_encoding.compression.as_ref().unwrap()
874            {
875                Compression::Variable(_) => 4,
876                Compression::Flat(_) => 16,
877                Compression::General(_) => 1,
878                Compression::FixedSizeList(_) => 1,
879                // The RLE frame starts with an 8-byte length prefix and slices its
880                // values/run-length sub-buffers at byte offsets, so no stricter
881                // alignment can be assumed (same reasoning as `General`).  Typed
882                // views taken later re-align (copying) as needed.
883                Compression::Rle(_) => 1,
884                Compression::InlineBitpacking(_) | Compression::OutOfLineBitpacking(_) => {
885                    crate::encoder::MIN_PAGE_BUFFER_ALIGNMENT
886                }
887                _ => {
888                    return Err(Error::invalid_input_source(
889                        format!(
890                            "Unsupported mini-block dictionary encoding: {:?}",
891                            dictionary_encoding.compression.as_ref().unwrap()
892                        )
893                        .into(),
894                    ));
895                }
896            };
897            Some(MiniBlockSchedulerDictionary {
898                dictionary_decompressor,
899                dictionary_buf_position_and_size: buffer_offsets_and_sizes[2],
900                dictionary_data_alignment,
901                num_dictionary_items,
902            })
903        } else {
904            None
905        };
906
907        Ok(Self {
908            buffer_offsets_and_sizes: buffer_offsets_and_sizes.to_vec(),
909            rep_decompressor,
910            def_decompressor,
911            value_decompressor: value_decompressor.into(),
912            repetition_index_depth: layout.repetition_index_depth as u16,
913            num_buffers: layout.num_buffers,
914            priority,
915            items_in_page,
916            dictionary,
917            def_meaning: def_meaning.into(),
918            page_meta: None,
919            has_large_chunk: layout.has_large_chunk,
920        })
921    }
922
923    fn lookup_chunks(&self, chunk_indices: &[usize]) -> Vec<LoadedChunk> {
924        let chunk_index = &self.page_meta.as_ref().unwrap().chunk_index;
925        chunk_indices
926            .iter()
927            .map(|&chunk_idx| LoadedChunk {
928                byte_range: chunk_index.byte_range(chunk_idx),
929                items_in_chunk: chunk_index.items_in_chunk(chunk_idx),
930                chunk_idx,
931                data: LanceBuffer::empty(),
932            })
933            .collect()
934    }
935}
936
937#[derive(Debug, PartialEq, Eq, Clone, Copy)]
938enum PreambleAction {
939    Take,
940    Skip,
941    Absent,
942}
943
944// When we schedule a chunk we use the repetition index (or, if none exists, just the # of items
945// in each chunk) to map a user requested range into a set of ChunkInstruction objects which tell
946// us how exactly to read from the chunk.
947//
948// Examples:
949//
950// | Chunk 0     | Chunk 1   | Chunk 2   | Chunk 3 |
951// | xxxxyyyyzzz | zzzzzzzzz | zzzzzzzzz | aaabbcc |
952//
953// Full read (0..6)
954//
955// Chunk 0: (several rows, ends with trailer)
956//   preamble: absent
957//   rows_to_skip: 0
958//   rows_to_take: 3 (x, y, z)
959//   take_trailer: true
960//
961// Chunk 1: (all preamble, ends with trailer)
962//   preamble: take
963//   rows_to_skip: 0
964//   rows_to_take: 0
965//   take_trailer: true
966//
967// Chunk 2: (all preamble, no trailer)
968//   preamble: take
969//   rows_to_skip: 0
970//   rows_to_take: 0
971//   take_trailer: false
972//
973// Chunk 3: (several rows, no trailer or preamble)
974//   preamble: absent
975//   rows_to_skip: 0
976//   rows_to_take: 3 (a, b, c)
977//   take_trailer: false
978#[derive(Clone, Debug, PartialEq, Eq)]
979struct ChunkInstructions {
980    // The index of the chunk to read
981    chunk_idx: usize,
982    // A "preamble" is when a chunk begins with a continuation of the previous chunk's list.  If there
983    // is no repetition index there is never a preamble.
984    //
985    // It's possible for a chunk to be entirely premable.  For example, if there is a really large list
986    // that spans several chunks.
987    preamble: PreambleAction,
988    // How many complete rows (not including the preamble or trailer) to skip
989    //
990    // If this is non-zero then premable must not be Take
991    rows_to_skip: u64,
992    // How many rows to take.  If a row splits across chunks then we will count the row in the first
993    // chunk that contains the row.
994    rows_to_take: u64,
995    // A "trailer" is when a chunk ends with a partial list.  If there is no repetition index there is
996    // never a trailer.
997    //
998    // A chunk that is all preamble may or may not have a trailer.
999    //
1000    // If this is true then we want to include the trailer
1001    take_trailer: bool,
1002}
1003
1004// First, we schedule a bunch of [`ChunkInstructions`] based on the users ranges.  Then we
1005// start decoding them, based on a batch size, which might not align with what we scheduled.
1006//
1007// This results in `ChunkDrainInstructions` which targets a contiguous slice of a `ChunkInstructions`
1008//
1009// So if `ChunkInstructions` is "skip preamble, skip 10, take 50, take trailer" and we are decoding in
1010// batches of size 10 we might have a `ChunkDrainInstructions` that targets that chunk and has its own
1011// skip of 17 and take of 10.  This would mean we decode the chunk, skip the preamble and 27 rows, and
1012// then take 10 rows.
1013//
1014// One very confusing bit is that `rows_to_take` includes the trailer.  So if we have two chunks:
1015//  -no preamble, skip 5, take 10, take trailer
1016//  -take preamble, skip 0, take 50, no trailer
1017//
1018// and we are draining 20 rows then the drain instructions for the first batch will be:
1019//  - no preamble, skip 0 (from chunk 0), take 11 (from chunk 0)
1020//  - take preamble (from chunk 1), skip 0 (from chunk 1), take 9 (from chunk 1)
1021#[derive(Debug, PartialEq, Eq)]
1022struct ChunkDrainInstructions {
1023    chunk_instructions: ChunkInstructions,
1024    rows_to_skip: u64,
1025    rows_to_take: u64,
1026    preamble_action: PreambleAction,
1027}
1028
1029impl ChunkInstructions {
1030    // Given a repetition index and a set of user ranges we need to figure out how to read from the chunks
1031    //
1032    // We assume that `user_ranges` are in sorted order and non-overlapping
1033    //
1034    // The output will be a set of `ChunkInstructions` which tell us how to read from the chunks
1035    fn schedule_instructions(
1036        chunk_index: &MiniBlockChunkIndex,
1037        user_ranges: &[Range<u64>],
1038    ) -> Vec<Self> {
1039        // Bind the per-page chunk count once; re-deriving it each iteration
1040        // costs a width match plus a length read.
1041        let num_chunks = chunk_index.num_chunks();
1042        // This is an in-exact capacity guess but pretty good.  The actual capacity can be
1043        // smaller if instructions are merged.  It can be larger if there are multiple instructions
1044        // per row which can happen with lists.
1045        let mut chunk_instructions = Vec::with_capacity(user_ranges.len());
1046
1047        for user_range in user_ranges {
1048            let mut rows_needed = user_range.end - user_range.start;
1049            let mut need_preamble = false;
1050
1051            // Need to find the first chunk with a first row >= user_range.start.  If there are
1052            // multiple chunks with the same first row we need to take the first one.
1053            let mut block_index = chunk_index.find_chunk(user_range.start);
1054
1055            let mut to_skip = user_range.start - chunk_index.first_row(block_index);
1056
1057            while rows_needed > 0 || need_preamble {
1058                // Check if we've gone past the last block (should not happen)
1059                if block_index >= num_chunks {
1060                    log::warn!(
1061                        "schedule_instructions inconsistency: block_index >= num_chunks, exiting early"
1062                    );
1063                    break;
1064                }
1065
1066                let starts_including_trailer = chunk_index.rows_in_chunk(block_index);
1067                let has_preamble = chunk_index.has_preamble(block_index);
1068                let has_trailer = chunk_index.has_trailer(block_index);
1069                let rows_avail = starts_including_trailer.saturating_sub(to_skip);
1070
1071                // Handle blocks that are entirely preamble (rows_avail = 0)
1072                // These blocks have no rows to take but may have a preamble we need
1073                // We only look for preamble if to_skip == 0 (we're not skipping rows)
1074                if rows_avail == 0 && to_skip == 0 {
1075                    // Only process if this chunk has a preamble we need
1076                    if has_preamble && need_preamble {
1077                        chunk_instructions.push(Self {
1078                            chunk_idx: block_index,
1079                            preamble: PreambleAction::Take,
1080                            rows_to_skip: 0,
1081                            rows_to_take: 0,
1082                            // We still need to look at has_trailer to distinguish between "all preamble
1083                            // and row ends at end of chunk" and "all preamble and row bleeds into next
1084                            // chunk".  Both cases will have 0 rows available.
1085                            take_trailer: has_trailer,
1086                        });
1087                        // Only set need_preamble = false if the chunk has at least one row,
1088                        // Or we are reaching the last block,
1089                        // Otherwise, the chunk is entirely preamble and we need the next chunk's preamble too
1090                        if starts_including_trailer > 0 || block_index == num_chunks - 1 {
1091                            need_preamble = false;
1092                        }
1093                    }
1094                    // Move to next block
1095                    block_index += 1;
1096                    continue;
1097                }
1098
1099                // Edge case: if rows_avail == 0 but to_skip > 0
1100                // This theoretically shouldn't happen (binary search should avoid it)
1101                // but handle it for safety
1102                if rows_avail == 0 && to_skip > 0 {
1103                    // This block doesn't have enough rows to skip, move to next block
1104                    // Adjust to_skip by the number of rows in this block
1105                    to_skip -= starts_including_trailer;
1106                    block_index += 1;
1107                    continue;
1108                }
1109
1110                let rows_to_take = rows_avail.min(rows_needed);
1111                rows_needed -= rows_to_take;
1112
1113                let mut take_trailer = false;
1114                let preamble = if has_preamble {
1115                    if need_preamble {
1116                        PreambleAction::Take
1117                    } else {
1118                        PreambleAction::Skip
1119                    }
1120                } else {
1121                    PreambleAction::Absent
1122                };
1123
1124                // Are we taking the trailer?  If so, make sure we mark that we need the preamble
1125                if rows_to_take == rows_avail && has_trailer {
1126                    take_trailer = true;
1127                    need_preamble = true;
1128                } else {
1129                    need_preamble = false;
1130                };
1131
1132                chunk_instructions.push(Self {
1133                    preamble,
1134                    chunk_idx: block_index,
1135                    rows_to_skip: to_skip,
1136                    rows_to_take,
1137                    take_trailer,
1138                });
1139
1140                to_skip = 0;
1141                block_index += 1;
1142            }
1143        }
1144
1145        // If there were multiple ranges we may have multiple instructions for a single chunk.  Merge them now if they
1146        // are _adjacent_ (i.e. don't merge "take first row of chunk 0" and "take third row of chunk 0" into "take 2
1147        // rows of chunk 0 starting at 0")
1148        if user_ranges.len() > 1 {
1149            // Merge adjacent instructions in place.  `write` indexes the last
1150            // retained instruction; each following instruction is either folded
1151            // into it (contiguous within the same chunk) or compacted forward.
1152            let mut write = 0;
1153            for read in 1..chunk_instructions.len() {
1154                let merges = {
1155                    let last = &chunk_instructions[write];
1156                    let candidate = &chunk_instructions[read];
1157                    last.chunk_idx == candidate.chunk_idx
1158                        && last.rows_to_take + last.rows_to_skip == candidate.rows_to_skip
1159                };
1160                if merges {
1161                    let rows_to_take = chunk_instructions[read].rows_to_take;
1162                    let take_trailer = chunk_instructions[read].take_trailer;
1163                    let last = &mut chunk_instructions[write];
1164                    last.rows_to_take += rows_to_take;
1165                    last.take_trailer |= take_trailer;
1166                } else {
1167                    write += 1;
1168                    if write != read {
1169                        chunk_instructions.swap(write, read);
1170                    }
1171                }
1172            }
1173            chunk_instructions.truncate(write + 1);
1174        }
1175        chunk_instructions
1176    }
1177
1178    fn drain_from_instruction(
1179        &self,
1180        rows_desired: &mut u64,
1181        need_preamble: &mut bool,
1182        skip_in_chunk: &mut u64,
1183    ) -> (ChunkDrainInstructions, bool) {
1184        // If we need the premable then we shouldn't be skipping anything
1185        debug_assert!(!*need_preamble || *skip_in_chunk == 0);
1186        let rows_avail = self.rows_to_take - *skip_in_chunk;
1187        let has_preamble = self.preamble != PreambleAction::Absent;
1188        let preamble_action = match (*need_preamble, has_preamble) {
1189            (true, true) => PreambleAction::Take,
1190            (true, false) => panic!("Need preamble but there isn't one"),
1191            (false, true) => PreambleAction::Skip,
1192            (false, false) => PreambleAction::Absent,
1193        };
1194
1195        // How many rows are we actually taking in this take step (including the preamble
1196        // and trailer both as individual rows)
1197        let rows_taking = if *rows_desired >= rows_avail {
1198            // We want all the rows.  If there is a trailer we are grabbing it and will need
1199            // the preamble of the next chunk
1200            // If there is a trailer and we are taking all the rows then we need the preamble
1201            // of the next chunk.
1202            //
1203            // Also, if this chunk is entirely preamble (rows_avail == 0 && !take_trailer) then we
1204            // need the preamble of the next chunk.
1205            *need_preamble = self.take_trailer;
1206            rows_avail
1207        } else {
1208            // We aren't taking all the rows.  Even if there is a trailer we aren't taking
1209            // it so we will not need the preamble
1210            *need_preamble = false;
1211            *rows_desired
1212        };
1213        let rows_skipped = *skip_in_chunk;
1214
1215        // Update the state for the next iteration
1216        let consumed_chunk = if *rows_desired >= rows_avail {
1217            *rows_desired -= rows_avail;
1218            *skip_in_chunk = 0;
1219            true
1220        } else {
1221            *skip_in_chunk += *rows_desired;
1222            *rows_desired = 0;
1223            false
1224        };
1225
1226        (
1227            ChunkDrainInstructions {
1228                chunk_instructions: self.clone(),
1229                rows_to_skip: rows_skipped,
1230                rows_to_take: rows_taking,
1231                preamble_action,
1232            },
1233            consumed_chunk,
1234        )
1235    }
1236}
1237
1238enum Words {
1239    U16(ScalarBuffer<u16>),
1240    U32(ScalarBuffer<u32>),
1241}
1242
1243struct WordsIter<'a> {
1244    iter: Box<dyn Iterator<Item = u32> + 'a>,
1245}
1246
1247impl Words {
1248    pub fn len(&self) -> usize {
1249        match self {
1250            Self::U16(b) => b.len(),
1251            Self::U32(b) => b.len(),
1252        }
1253    }
1254
1255    pub fn iter(&self) -> WordsIter<'_> {
1256        match self {
1257            Self::U16(buf) => WordsIter {
1258                iter: Box::new(buf.iter().map(|&x| x as u32)),
1259            },
1260            Self::U32(buf) => WordsIter {
1261                iter: Box::new(buf.iter().copied()),
1262            },
1263        }
1264    }
1265
1266    pub fn from_bytes(bytes: Bytes, has_large_chunk: bool) -> Result<Self> {
1267        let bytes_per_value = if has_large_chunk { 4 } else { 2 };
1268        assert_eq!(bytes.len() % bytes_per_value, 0);
1269        let buffer = LanceBuffer::from_bytes(bytes, bytes_per_value as u64);
1270        if has_large_chunk {
1271            Ok(Self::U32(buffer.borrow_to_typed_slice::<u32>()))
1272        } else {
1273            Ok(Self::U16(buffer.borrow_to_typed_slice::<u16>()))
1274        }
1275    }
1276}
1277
1278impl<'a> Iterator for WordsIter<'a> {
1279    type Item = u32;
1280
1281    fn next(&mut self) -> Option<Self::Item> {
1282        self.iter.next()
1283    }
1284}
1285
1286/// Per-chunk leaf value-count analysis derived from the metadata words.
1287///
1288/// `values_per_chunk` is the count shared by every non-last chunk (meaningful
1289/// when `uniform`), and `last_chunk_values` is the final chunk's count.
1290struct FlatValueCounts {
1291    logs: Vec<u8>,
1292    uniform: bool,
1293    values_per_chunk: u64,
1294    last_chunk_values: u64,
1295}
1296
1297fn analyze_value_counts(words: &Words, items_in_page: u64) -> Result<FlatValueCounts> {
1298    let num_chunks = words.len();
1299    let logs = words.iter().map(|w| (w & 0x0F) as u8).collect::<Vec<_>>();
1300    let mut counted = 0u64;
1301    for (chunk_index, &log) in logs.iter().take(num_chunks.saturating_sub(1)).enumerate() {
1302        if log == 0 {
1303            return Err(Error::corrupt_file_named(
1304                "miniblock_metadata",
1305                format!(
1306                    "non-final chunk {chunk_index} of {num_chunks} has invalid log_num_values=0"
1307                ),
1308            ));
1309        }
1310        counted = counted.checked_add(1u64 << log).ok_or_else(|| {
1311            Error::corrupt_file_named(
1312                "miniblock_metadata",
1313                format!(
1314                    "value count overflow at chunk {chunk_index}: counted_values={counted}, \
1315                     log_num_values={log}, items_in_page={items_in_page}"
1316                ),
1317            )
1318        })?;
1319    }
1320    let last_chunk_values = items_in_page.checked_sub(counted).ok_or_else(|| {
1321        Error::corrupt_file_named(
1322            "miniblock_metadata",
1323            format!(
1324                "non-final chunks account for counted_values={counted}, exceeding \
1325                 items_in_page={items_in_page}"
1326            ),
1327        )
1328    })?;
1329    if let Some(&last_log) = logs.last()
1330        && last_log != 0
1331        && (1u64 << last_log) != last_chunk_values
1332    {
1333        return Err(Error::corrupt_file_named(
1334            "miniblock_metadata",
1335            format!(
1336                "final chunk log_num_values={last_log} does not match \
1337                 last_chunk_values={last_chunk_values}: counted_values={counted}, \
1338                 items_in_page={items_in_page}"
1339            ),
1340        ));
1341    }
1342    let uniform = num_chunks <= 1 || logs[..num_chunks - 1].iter().all(|&log| log == logs[0]);
1343    // A single-chunk page has no "non-last" chunk to derive a stride from; use the
1344    // page item count (min 1 so it stays a valid divisor in `find_chunk`).
1345    let values_per_chunk = if num_chunks <= 1 {
1346        items_in_page.max(1)
1347    } else {
1348        1u64 << logs[0]
1349    };
1350    Ok(FlatValueCounts {
1351        logs,
1352        uniform,
1353        values_per_chunk,
1354        last_chunk_values,
1355    })
1356}
1357
1358/// Iterator over per-chunk value counts for a non-uniform flat page.  Non-last
1359/// chunks yield `1 << log`; the last yields the validated remaining item count.
1360fn flat_value_counts_iter(logs: &[u8], last_chunk_values: u64) -> impl Iterator<Item = u64> + '_ {
1361    let num_chunks = logs.len();
1362    (0..num_chunks).map(move |i| {
1363        if i + 1 < num_chunks {
1364            1u64 << logs[i]
1365        } else {
1366            last_chunk_values
1367        }
1368    })
1369}
1370
1371/// Builds the compact per-chunk index from the metadata words and, for nested
1372/// pages, the raw repetition-index bytes.  The row axis is picked by page shape:
1373/// `UniformFlat` when all non-last chunks share a value count (fixed-width /
1374/// bitpacking), `Flat` for non-uniform flat pages (RLE / FSST), else `Nested`.
1375fn build_chunk_index(
1376    words: &Words,
1377    items_in_page: u64,
1378    base: u64,
1379    data_buf_size: u64,
1380    rep_index_bytes: Option<&[u8]>,
1381    repetition_index_depth: u16,
1382) -> Result<MiniBlockChunkIndex> {
1383    let num_chunks = words.len();
1384    // Validate item counts before byte sizes because both share a metadata word,
1385    // and an invalid count must not reach the final-chunk subtraction.
1386    let value_counts = analyze_value_counts(words, items_in_page)?;
1387
1388    // Each chunk stores `(divided_bytes + 1) * MINIBLOCK_ALIGNMENT` bytes, so the
1389    // deltas are the chunk sizes and their grand total is the data buffer size.
1390    let byte_starts = PrefixSums::from_deltas(
1391        words
1392            .iter()
1393            .map(|word| ((word >> 4) as u64 + 1) * MINIBLOCK_ALIGNMENT as u64),
1394        num_chunks,
1395        data_buf_size,
1396    );
1397
1398    // Nested pages track rows via the repetition index and keep leaf item counts
1399    // separately; flat pages have row == value index, so value counts are rows.
1400    let rows = if let Some(rep_index_data) = rep_index_bytes {
1401        assert!(rep_index_data.len() % 8 == 0);
1402        let stride = repetition_index_depth as usize + 1;
1403        let (row_starts, has_trailer) = parse_nested_rep(rep_index_data, stride);
1404        let item_counts = if value_counts.uniform {
1405            ItemCounts::Uniform {
1406                values_per_chunk: value_counts.values_per_chunk,
1407                last_chunk_values: value_counts.last_chunk_values,
1408            }
1409        } else {
1410            ItemCounts::PerChunkLog {
1411                logs: value_counts.logs,
1412                last_chunk_values: value_counts.last_chunk_values,
1413            }
1414        };
1415        RowMapping::Nested {
1416            row_starts,
1417            has_trailer,
1418            item_counts,
1419        }
1420    } else {
1421        if value_counts.uniform {
1422            RowMapping::UniformFlat {
1423                values_per_chunk: value_counts.values_per_chunk,
1424                last_chunk_values: value_counts.last_chunk_values,
1425                num_chunks,
1426            }
1427        } else {
1428            let value_starts = PrefixSums::from_deltas(
1429                flat_value_counts_iter(&value_counts.logs, value_counts.last_chunk_values),
1430                num_chunks,
1431                items_in_page,
1432            );
1433            RowMapping::Flat { value_starts }
1434        }
1435    };
1436
1437    Ok(MiniBlockChunkIndex::new(base, byte_starts, rows))
1438}
1439
1440impl StructuralPageScheduler for MiniBlockScheduler {
1441    fn init_layout(&self) -> Result<PageInitialization> {
1442        // We always need to fetch chunk metadata.  We may also need to fetch a dictionary and
1443        // we may also need to fetch the repetition index.  Here, we gather what buffers we
1444        // need.
1445        let (meta_buf_position, meta_buf_size) = self.buffer_offsets_and_sizes[0];
1446        let dictionary_range = self.dictionary.as_ref().map(|dictionary| {
1447            dictionary.dictionary_buf_position_and_size.0
1448                ..dictionary.dictionary_buf_position_and_size.0
1449                    + dictionary.dictionary_buf_position_and_size.1
1450        });
1451        let repetition_index_range = if self.repetition_index_depth > 0 {
1452            let (rep_index_pos, rep_index_size) =
1453                self.buffer_offsets_and_sizes.last().ok_or_else(|| {
1454                    Error::invalid_input_source(
1455                        "Mini-block page has a repetition index but no buffers".into(),
1456                    )
1457                })?;
1458            Some(*rep_index_pos..*rep_index_pos + *rep_index_size)
1459        } else {
1460            None
1461        };
1462        Ok(PageInitialization::new([
1463            Some(meta_buf_position..meta_buf_position + meta_buf_size),
1464            dictionary_range,
1465            repetition_index_range,
1466        ]))
1467    }
1468
1469    fn init_from_buffers<'a>(
1470        &'a mut self,
1471        buffers: PageInitializationBuffers,
1472        _io: &Arc<dyn EncodingsIo>,
1473    ) -> BoxFuture<'a, Result<Arc<dyn CachedPageData>>> {
1474        let base = self.buffer_offsets_and_sizes[1].0;
1475        let data_buf_size = self.buffer_offsets_and_sizes[1].1;
1476
1477        async move {
1478            let [meta_bytes, dictionary_bytes, rep_index_bytes] =
1479                buffers.try_into_array("mini-block")?;
1480            let meta_bytes = meta_bytes.ok_or_else(|| {
1481                Error::internal("Mini-block page initialization is missing chunk metadata")
1482            })?;
1483
1484            let words = Words::from_bytes(meta_bytes, self.has_large_chunk)?;
1485            let chunk_index = build_chunk_index(
1486                &words,
1487                self.items_in_page,
1488                base,
1489                data_buf_size,
1490                rep_index_bytes.as_deref(),
1491                self.repetition_index_depth,
1492            )?;
1493
1494            // decode dictionary
1495            let dictionary = match (self.dictionary.as_ref(), dictionary_bytes) {
1496                (Some(dictionary), Some(dictionary_data)) => {
1497                    Some(Arc::new(dictionary.dictionary_decompressor.decompress(
1498                        Some(LanceBuffer::from_bytes(
1499                            dictionary_data,
1500                            dictionary.dictionary_data_alignment,
1501                        )),
1502                        dictionary.num_dictionary_items,
1503                    )?))
1504                }
1505                (None, None) => None,
1506                _ => {
1507                    return Err(Error::internal(
1508                        "Mini-block page initialization dictionary slot does not match its layout",
1509                    ));
1510                }
1511            };
1512
1513            let page_meta = Arc::new(MiniBlockCacheableState {
1514                chunk_index,
1515                dictionary,
1516            });
1517            Ok(page_meta as Arc<dyn CachedPageData>)
1518        }
1519        .boxed()
1520    }
1521
1522    fn try_load(&mut self, data: &Arc<dyn CachedPageData>) -> Result<()> {
1523        self.page_meta = Some(data.clone().as_arc_any().downcast().map_err(|_| {
1524            Error::invalid_input_source("Cached mini-block page data has an unexpected type".into())
1525        })?);
1526        Ok(())
1527    }
1528
1529    fn schedule_ranges(
1530        &self,
1531        ranges: &[Range<u64>],
1532        io: &Arc<dyn EncodingsIo>,
1533    ) -> Result<Vec<PageLoadTask>> {
1534        let num_rows = ranges.iter().map(|r| r.end - r.start).sum();
1535
1536        let page_meta = self.page_meta.as_ref().unwrap();
1537
1538        let chunk_instructions =
1539            ChunkInstructions::schedule_instructions(&page_meta.chunk_index, ranges);
1540
1541        debug_assert_eq!(
1542            num_rows,
1543            chunk_instructions
1544                .iter()
1545                .map(|ci| ci.rows_to_take)
1546                .sum::<u64>()
1547        );
1548
1549        let chunks_needed = chunk_instructions
1550            .iter()
1551            .map(|ci| ci.chunk_idx)
1552            .unique()
1553            .collect::<Vec<_>>();
1554
1555        let mut loaded_chunks = self.lookup_chunks(&chunks_needed);
1556        let chunk_ranges = loaded_chunks
1557            .iter()
1558            .map(|c| c.byte_range.clone())
1559            .collect::<Vec<_>>();
1560        let loaded_chunk_data = io.submit_request(chunk_ranges, self.priority);
1561
1562        let rep_decompressor = self.rep_decompressor.clone();
1563        let def_decompressor = self.def_decompressor.clone();
1564        let value_decompressor = self.value_decompressor.clone();
1565        let num_buffers = self.num_buffers;
1566        let has_large_chunk = self.has_large_chunk;
1567        let dictionary = page_meta
1568            .dictionary
1569            .as_ref()
1570            .map(|dictionary| dictionary.clone());
1571        let def_meaning = self.def_meaning.clone();
1572
1573        let res = async move {
1574            let loaded_chunk_data = loaded_chunk_data.await?;
1575            for (loaded_chunk, chunk_data) in loaded_chunks.iter_mut().zip(loaded_chunk_data) {
1576                loaded_chunk.data = LanceBuffer::from_bytes(chunk_data, 1);
1577            }
1578
1579            Ok(Box::new(MiniBlockDecoder {
1580                rep_decompressor,
1581                def_decompressor,
1582                value_decompressor,
1583                def_meaning,
1584                loaded_chunks: VecDeque::from_iter(loaded_chunks),
1585                instructions: VecDeque::from(chunk_instructions),
1586                offset_in_current_chunk: 0,
1587                dictionary,
1588                num_rows,
1589                num_buffers,
1590                has_large_chunk,
1591            }) as Box<dyn StructuralPageDecoder>)
1592        }
1593        .boxed();
1594        let page_load_task = PageLoadTask {
1595            decoder_fut: res,
1596            num_rows,
1597        };
1598        Ok(vec![page_load_task])
1599    }
1600}
1601
1602#[cfg(test)]
1603#[allow(clippy::single_range_in_vec_init)]
1604mod tests {
1605    use std::collections::VecDeque;
1606    use std::sync::Arc;
1607
1608    use bytes::Bytes;
1609    use lance_core::cache::{Context, DeepSizeOf};
1610    use rstest::rstest;
1611
1612    use crate::buffer::LanceBuffer;
1613    use crate::compression::{DefaultDecompressionStrategy, MiniBlockDecompressor};
1614    use crate::data::{BlockInfo, DataBlock, FixedWidthDataBlock};
1615    use crate::decoder::DecodePageTask;
1616    use crate::encodings::logical::primitive::chunk_index::{
1617        MiniBlockChunkIndex, PrefixSums, RowMapping,
1618    };
1619    use crate::encodings::logical::primitive::tests::encoded_u16_frame;
1620    use crate::encodings::logical::primitive::{LoadedChunk, MINIBLOCK_ALIGNMENT};
1621    use crate::encodings::physical::rle::{RleDecompressor, RunLengthWidth};
1622    use crate::encodings::physical::value::ValueDecompressor;
1623    use crate::format::{EncodingUtils, metadata};
1624
1625    use super::{
1626        ChunkDrainInstructions, ChunkInstructions, DecodeMiniBlockTask, PreambleAction, Words,
1627        build_chunk_index,
1628    };
1629
1630    #[test]
1631    fn test_miniblock_decode_uses_exact_fixed_width_output_size() {
1632        #[derive(Debug)]
1633        struct FixedWidthMiniBlockDecompressor;
1634
1635        impl MiniBlockDecompressor for FixedWidthMiniBlockDecompressor {
1636            fn decompress(
1637                &self,
1638                data: Vec<LanceBuffer>,
1639                num_values: u64,
1640            ) -> crate::Result<DataBlock> {
1641                assert_eq!(data.len(), 1);
1642                Ok(DataBlock::FixedWidth(FixedWidthDataBlock {
1643                    data: data.into_iter().next().unwrap(),
1644                    bits_per_value: 32,
1645                    num_values,
1646                    block_info: BlockInfo::new(),
1647                }))
1648            }
1649
1650            fn decoded_size_bytes(&self, num_values: u64) -> Option<u64> {
1651                num_values.checked_mul(4)
1652            }
1653        }
1654
1655        let num_rows = 512;
1656        let expected_size = num_rows * 4;
1657        let mut chunk_data = Vec::new();
1658        chunk_data.extend_from_slice(&0_u16.to_le_bytes());
1659        chunk_data.extend_from_slice(&(expected_size as u16).to_le_bytes());
1660        let header_padding =
1661            lance_core::utils::bit::pad_bytes::<{ super::MINIBLOCK_ALIGNMENT }>(chunk_data.len());
1662        chunk_data.resize(chunk_data.len() + header_padding, 0);
1663        chunk_data.resize(chunk_data.len() + expected_size as usize, 7);
1664
1665        let task = DecodeMiniBlockTask {
1666            rep_decompressor: None,
1667            def_decompressor: None,
1668            value_decompressor: Arc::new(FixedWidthMiniBlockDecompressor),
1669            dictionary_data: None,
1670            def_meaning: Arc::from([]),
1671            num_buffers: 1,
1672            max_visible_level: 0,
1673            instructions: vec![(
1674                ChunkDrainInstructions {
1675                    chunk_instructions: ChunkInstructions {
1676                        chunk_idx: 0,
1677                        preamble: PreambleAction::Absent,
1678                        rows_to_skip: 0,
1679                        rows_to_take: num_rows,
1680                        take_trailer: false,
1681                    },
1682                    rows_to_skip: 0,
1683                    rows_to_take: num_rows,
1684                    preamble_action: PreambleAction::Absent,
1685                },
1686                LoadedChunk {
1687                    byte_range: 0..chunk_data.len() as u64,
1688                    data: LanceBuffer::from(chunk_data),
1689                    items_in_chunk: num_rows,
1690                    chunk_idx: 0,
1691                },
1692            )],
1693            has_large_chunk: false,
1694        };
1695
1696        let decoded = Box::new(task).decode().unwrap();
1697        let values = decoded.data.as_fixed_width_ref().unwrap();
1698        assert_eq!(values.data.len(), expected_size as usize);
1699        assert_eq!(
1700            values.data.clone().into_buffer().capacity(),
1701            expected_size as usize
1702        );
1703    }
1704
1705    #[test]
1706    fn test_map_range() {
1707        // Null in the middle
1708        // [[A, B, C], [D, E], NULL, [F, G, H]]
1709        let rep = Some(vec![1, 0, 0, 1, 0, 1, 1, 0, 0]);
1710        let def = Some(vec![0, 0, 0, 0, 0, 1, 0, 0, 0]);
1711        let max_visible_def = 0;
1712        let total_items = 8;
1713        let max_rep = 1;
1714
1715        let check = |range, expected_item_range, expected_level_range| {
1716            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
1717                range,
1718                rep.as_ref(),
1719                def.as_ref(),
1720                max_rep,
1721                max_visible_def,
1722                total_items,
1723                PreambleAction::Absent,
1724            );
1725            assert_eq!(item_range, expected_item_range);
1726            assert_eq!(level_range, expected_level_range);
1727        };
1728
1729        check(0..1, 0..3, 0..3);
1730        check(1..2, 3..5, 3..5);
1731        check(2..3, 5..5, 5..6);
1732        check(3..4, 5..8, 6..9);
1733        check(0..2, 0..5, 0..5);
1734        check(1..3, 3..5, 3..6);
1735        check(2..4, 5..8, 5..9);
1736        check(0..3, 0..5, 0..6);
1737        check(1..4, 3..8, 3..9);
1738        check(0..4, 0..8, 0..9);
1739
1740        // Null at start
1741        // [NULL, [A, B], [C]]
1742        let rep = Some(vec![1, 1, 0, 1]);
1743        let def = Some(vec![1, 0, 0, 0]);
1744        let max_visible_def = 0;
1745        let total_items = 3;
1746
1747        let check = |range, expected_item_range, expected_level_range| {
1748            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
1749                range,
1750                rep.as_ref(),
1751                def.as_ref(),
1752                max_rep,
1753                max_visible_def,
1754                total_items,
1755                PreambleAction::Absent,
1756            );
1757            assert_eq!(item_range, expected_item_range);
1758            assert_eq!(level_range, expected_level_range);
1759        };
1760
1761        check(0..1, 0..0, 0..1);
1762        check(1..2, 0..2, 1..3);
1763        check(2..3, 2..3, 3..4);
1764        check(0..2, 0..2, 0..3);
1765        check(1..3, 0..3, 1..4);
1766        check(0..3, 0..3, 0..4);
1767
1768        // Null at end
1769        // [[A], [B, C], NULL]
1770        let rep = Some(vec![1, 1, 0, 1]);
1771        let def = Some(vec![0, 0, 0, 1]);
1772        let max_visible_def = 0;
1773        let total_items = 3;
1774
1775        let check = |range, expected_item_range, expected_level_range| {
1776            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
1777                range,
1778                rep.as_ref(),
1779                def.as_ref(),
1780                max_rep,
1781                max_visible_def,
1782                total_items,
1783                PreambleAction::Absent,
1784            );
1785            assert_eq!(item_range, expected_item_range);
1786            assert_eq!(level_range, expected_level_range);
1787        };
1788
1789        check(0..1, 0..1, 0..1);
1790        check(1..2, 1..3, 1..3);
1791        check(2..3, 3..3, 3..4);
1792        check(0..2, 0..3, 0..3);
1793        check(1..3, 1..3, 1..4);
1794        check(0..3, 0..3, 0..4);
1795
1796        // No nulls, with repetition
1797        // [[A, B], [C, D], [E, F]]
1798        let rep = Some(vec![1, 0, 1, 0, 1, 0]);
1799        let def: Option<&[u16]> = None;
1800        let max_visible_def = 0;
1801        let total_items = 6;
1802
1803        let check = |range, expected_item_range, expected_level_range| {
1804            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
1805                range,
1806                rep.as_ref(),
1807                def.as_ref(),
1808                max_rep,
1809                max_visible_def,
1810                total_items,
1811                PreambleAction::Absent,
1812            );
1813            assert_eq!(item_range, expected_item_range);
1814            assert_eq!(level_range, expected_level_range);
1815        };
1816
1817        check(0..1, 0..2, 0..2);
1818        check(1..2, 2..4, 2..4);
1819        check(2..3, 4..6, 4..6);
1820        check(0..2, 0..4, 0..4);
1821        check(1..3, 2..6, 2..6);
1822        check(0..3, 0..6, 0..6);
1823
1824        // No repetition, with nulls (this case is trivial)
1825        // [A, B, NULL, C]
1826        let rep: Option<&[u16]> = None;
1827        let def = Some(vec![0, 0, 1, 0]);
1828        let max_visible_def = 1;
1829        let total_items = 4;
1830
1831        let check = |range, expected_item_range, expected_level_range| {
1832            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
1833                range,
1834                rep.as_ref(),
1835                def.as_ref(),
1836                max_rep,
1837                max_visible_def,
1838                total_items,
1839                PreambleAction::Absent,
1840            );
1841            assert_eq!(item_range, expected_item_range);
1842            assert_eq!(level_range, expected_level_range);
1843        };
1844
1845        check(0..1, 0..1, 0..1);
1846        check(1..2, 1..2, 1..2);
1847        check(2..3, 2..3, 2..3);
1848        check(0..2, 0..2, 0..2);
1849        check(1..3, 1..3, 1..3);
1850        check(0..3, 0..3, 0..3);
1851
1852        // Tricky case, this chunk is a continuation and starts with a rep-index = 0
1853        // [[..., A] [B, C], NULL]
1854        //
1855        // What we do will depend on the preamble action
1856        let rep = Some(vec![0, 1, 0, 1]);
1857        let def = Some(vec![0, 0, 0, 1]);
1858        let max_visible_def = 0;
1859        let total_items = 3;
1860
1861        let check = |range, expected_item_range, expected_level_range| {
1862            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
1863                range,
1864                rep.as_ref(),
1865                def.as_ref(),
1866                max_rep,
1867                max_visible_def,
1868                total_items,
1869                PreambleAction::Take,
1870            );
1871            assert_eq!(item_range, expected_item_range);
1872            assert_eq!(level_range, expected_level_range);
1873        };
1874
1875        // If we are taking the preamble then the range must start at 0
1876        check(0..1, 0..3, 0..3);
1877        check(0..2, 0..3, 0..4);
1878
1879        let check = |range, expected_item_range, expected_level_range| {
1880            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
1881                range,
1882                rep.as_ref(),
1883                def.as_ref(),
1884                max_rep,
1885                max_visible_def,
1886                total_items,
1887                PreambleAction::Skip,
1888            );
1889            assert_eq!(item_range, expected_item_range);
1890            assert_eq!(level_range, expected_level_range);
1891        };
1892
1893        check(0..1, 1..3, 1..3);
1894        check(1..2, 3..3, 3..4);
1895        check(0..2, 1..3, 1..4);
1896
1897        // Another preamble case but now it doesn't end with a new list
1898        // [[..., A], NULL, [D, E]]
1899        //
1900        // What we do will depend on the preamble action
1901        let rep = Some(vec![0, 1, 1, 0]);
1902        let def = Some(vec![0, 1, 0, 0]);
1903        let max_visible_def = 0;
1904        let total_items = 4;
1905
1906        let check = |range, expected_item_range, expected_level_range| {
1907            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
1908                range,
1909                rep.as_ref(),
1910                def.as_ref(),
1911                max_rep,
1912                max_visible_def,
1913                total_items,
1914                PreambleAction::Take,
1915            );
1916            assert_eq!(item_range, expected_item_range);
1917            assert_eq!(level_range, expected_level_range);
1918        };
1919
1920        // If we are taking the preamble then the range must start at 0
1921        check(0..1, 0..1, 0..2);
1922        check(0..2, 0..3, 0..4);
1923
1924        let check = |range, expected_item_range, expected_level_range| {
1925            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
1926                range,
1927                rep.as_ref(),
1928                def.as_ref(),
1929                max_rep,
1930                max_visible_def,
1931                total_items,
1932                PreambleAction::Skip,
1933            );
1934            assert_eq!(item_range, expected_item_range);
1935            assert_eq!(level_range, expected_level_range);
1936        };
1937
1938        // If we are taking the preamble then the range must start at 0
1939        check(0..1, 1..1, 1..2);
1940        check(1..2, 1..3, 2..4);
1941        check(0..2, 1..3, 1..4);
1942
1943        // Now a preamble case without any definition levels
1944        // [[..., A] [B, C], [D]]
1945        let rep = Some(vec![0, 1, 0, 1]);
1946        let def: Option<Vec<u16>> = None;
1947        let max_visible_def = 0;
1948        let total_items = 4;
1949
1950        let check = |range, expected_item_range, expected_level_range| {
1951            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
1952                range,
1953                rep.as_ref(),
1954                def.as_ref(),
1955                max_rep,
1956                max_visible_def,
1957                total_items,
1958                PreambleAction::Take,
1959            );
1960            assert_eq!(item_range, expected_item_range);
1961            assert_eq!(level_range, expected_level_range);
1962        };
1963
1964        // If we are taking the preamble then the range must start at 0
1965        check(0..1, 0..3, 0..3);
1966        check(0..2, 0..4, 0..4);
1967
1968        let check = |range, expected_item_range, expected_level_range| {
1969            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
1970                range,
1971                rep.as_ref(),
1972                def.as_ref(),
1973                max_rep,
1974                max_visible_def,
1975                total_items,
1976                PreambleAction::Skip,
1977            );
1978            assert_eq!(item_range, expected_item_range);
1979            assert_eq!(level_range, expected_level_range);
1980        };
1981
1982        check(0..1, 1..3, 1..3);
1983        check(1..2, 3..4, 3..4);
1984        check(0..2, 1..4, 1..4);
1985
1986        // If we have nested lists then non-top level lists may be empty/null
1987        // and we need to make sure we still handle them as invisible items (we
1988        // failed to do this previously)
1989        let rep = Some(vec![2, 1, 2, 0, 1, 2]);
1990        let def = Some(vec![0, 1, 2, 0, 0, 0]);
1991        let max_rep = 2;
1992        let max_visible_def = 0;
1993        let total_items = 4;
1994
1995        let check = |range, expected_item_range, expected_level_range| {
1996            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
1997                range,
1998                rep.as_ref(),
1999                def.as_ref(),
2000                max_rep,
2001                max_visible_def,
2002                total_items,
2003                PreambleAction::Absent,
2004            );
2005            assert_eq!(item_range, expected_item_range);
2006            assert_eq!(level_range, expected_level_range);
2007        };
2008
2009        check(0..3, 0..4, 0..6);
2010        check(0..1, 0..1, 0..2);
2011        check(1..2, 1..3, 2..5);
2012        check(2..3, 3..4, 5..6);
2013
2014        // Invisible items in a preamble that we are taking (regressing a previous failure)
2015        let rep = Some(vec![0, 0, 1, 0, 1, 1]);
2016        let def = Some(vec![0, 1, 0, 0, 0, 0]);
2017        let max_rep = 1;
2018        let max_visible_def = 0;
2019        let total_items = 5;
2020
2021        let check = |range, expected_item_range, expected_level_range| {
2022            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
2023                range,
2024                rep.as_ref(),
2025                def.as_ref(),
2026                max_rep,
2027                max_visible_def,
2028                total_items,
2029                PreambleAction::Take,
2030            );
2031            assert_eq!(item_range, expected_item_range);
2032            assert_eq!(level_range, expected_level_range);
2033        };
2034
2035        check(0..0, 0..1, 0..2);
2036        check(0..1, 0..3, 0..4);
2037        check(0..2, 0..4, 0..5);
2038
2039        // Skip preamble (with invis items) and skip a few rows (with invis items)
2040        // and then take a few rows but not all the rows
2041        let rep = Some(vec![0, 1, 0, 1, 0, 1, 0, 1]);
2042        let def = Some(vec![1, 0, 1, 1, 0, 0, 0, 0]);
2043        let max_rep = 1;
2044        let max_visible_def = 0;
2045        let total_items = 5;
2046
2047        let check = |range, expected_item_range, expected_level_range| {
2048            let (item_range, level_range) = DecodeMiniBlockTask::map_range(
2049                range,
2050                rep.as_ref(),
2051                def.as_ref(),
2052                max_rep,
2053                max_visible_def,
2054                total_items,
2055                PreambleAction::Skip,
2056            );
2057            assert_eq!(item_range, expected_item_range);
2058            assert_eq!(level_range, expected_level_range);
2059        };
2060
2061        check(2..3, 2..4, 5..7);
2062    }
2063
2064    #[test]
2065    fn test_schedule_instructions() {
2066        // Convert repetition index to bytes for testing
2067        let rep_data: Vec<u64> = vec![5, 2, 3, 0, 4, 7, 2, 0];
2068        let rep_bytes: Vec<u8> = rep_data.iter().flat_map(|v| v.to_le_bytes()).collect();
2069        let chunk_index = MiniBlockChunkIndex::new_nested_for_test(&rep_bytes, 2);
2070
2071        let check = |user_ranges, expected_instructions| {
2072            let instructions = ChunkInstructions::schedule_instructions(&chunk_index, user_ranges);
2073            assert_eq!(instructions, expected_instructions);
2074        };
2075
2076        // The instructions we expect if we're grabbing the whole range
2077        let expected_take_all = vec![
2078            ChunkInstructions {
2079                chunk_idx: 0,
2080                preamble: PreambleAction::Absent,
2081                rows_to_skip: 0,
2082                rows_to_take: 6,
2083                take_trailer: true,
2084            },
2085            ChunkInstructions {
2086                chunk_idx: 1,
2087                preamble: PreambleAction::Take,
2088                rows_to_skip: 0,
2089                rows_to_take: 2,
2090                take_trailer: false,
2091            },
2092            ChunkInstructions {
2093                chunk_idx: 2,
2094                preamble: PreambleAction::Absent,
2095                rows_to_skip: 0,
2096                rows_to_take: 5,
2097                take_trailer: true,
2098            },
2099            ChunkInstructions {
2100                chunk_idx: 3,
2101                preamble: PreambleAction::Take,
2102                rows_to_skip: 0,
2103                rows_to_take: 1,
2104                take_trailer: false,
2105            },
2106        ];
2107
2108        // Take all as 1 range
2109        check(&[0..14], expected_take_all.clone());
2110
2111        // Take all a individual rows
2112        check(
2113            &[
2114                0..1,
2115                1..2,
2116                2..3,
2117                3..4,
2118                4..5,
2119                5..6,
2120                6..7,
2121                7..8,
2122                8..9,
2123                9..10,
2124                10..11,
2125                11..12,
2126                12..13,
2127                13..14,
2128            ],
2129            expected_take_all,
2130        );
2131
2132        // Test some partial takes
2133
2134        // 2 rows in the same chunk but not contiguous
2135        check(
2136            &[0..1, 3..4],
2137            vec![
2138                ChunkInstructions {
2139                    chunk_idx: 0,
2140                    preamble: PreambleAction::Absent,
2141                    rows_to_skip: 0,
2142                    rows_to_take: 1,
2143                    take_trailer: false,
2144                },
2145                ChunkInstructions {
2146                    chunk_idx: 0,
2147                    preamble: PreambleAction::Absent,
2148                    rows_to_skip: 3,
2149                    rows_to_take: 1,
2150                    take_trailer: false,
2151                },
2152            ],
2153        );
2154
2155        // Taking just a trailer/preamble
2156        check(
2157            &[5..6],
2158            vec![
2159                ChunkInstructions {
2160                    chunk_idx: 0,
2161                    preamble: PreambleAction::Absent,
2162                    rows_to_skip: 5,
2163                    rows_to_take: 1,
2164                    take_trailer: true,
2165                },
2166                ChunkInstructions {
2167                    chunk_idx: 1,
2168                    preamble: PreambleAction::Take,
2169                    rows_to_skip: 0,
2170                    rows_to_take: 0,
2171                    take_trailer: false,
2172                },
2173            ],
2174        );
2175
2176        // Skipping an entire chunk
2177        check(
2178            &[7..10],
2179            vec![
2180                ChunkInstructions {
2181                    chunk_idx: 1,
2182                    preamble: PreambleAction::Skip,
2183                    rows_to_skip: 1,
2184                    rows_to_take: 1,
2185                    take_trailer: false,
2186                },
2187                ChunkInstructions {
2188                    chunk_idx: 2,
2189                    preamble: PreambleAction::Absent,
2190                    rows_to_skip: 0,
2191                    rows_to_take: 2,
2192                    take_trailer: false,
2193                },
2194            ],
2195        );
2196    }
2197
2198    #[test]
2199    fn test_drain_instructions() {
2200        fn drain_from_instructions(
2201            instructions: &mut VecDeque<ChunkInstructions>,
2202            mut rows_desired: u64,
2203            need_preamble: &mut bool,
2204            skip_in_chunk: &mut u64,
2205        ) -> Vec<ChunkDrainInstructions> {
2206            // Note: instructions.len() is an upper bound, we typically take much fewer
2207            let mut drain_instructions = Vec::with_capacity(instructions.len());
2208            while rows_desired > 0 || *need_preamble {
2209                let (next_instructions, consumed_chunk) = instructions
2210                    .front()
2211                    .unwrap()
2212                    .drain_from_instruction(&mut rows_desired, need_preamble, skip_in_chunk);
2213                if consumed_chunk {
2214                    instructions.pop_front();
2215                }
2216                drain_instructions.push(next_instructions);
2217            }
2218            drain_instructions
2219        }
2220
2221        // Convert repetition index to bytes for testing
2222        let rep_data: Vec<u64> = vec![5, 2, 3, 0, 4, 7, 2, 0];
2223        let rep_bytes: Vec<u8> = rep_data.iter().flat_map(|v| v.to_le_bytes()).collect();
2224        let chunk_index = MiniBlockChunkIndex::new_nested_for_test(&rep_bytes, 2);
2225        let user_ranges = vec![1..7, 10..14];
2226
2227        // First, schedule the ranges
2228        let scheduled = ChunkInstructions::schedule_instructions(&chunk_index, &user_ranges);
2229
2230        let mut to_drain = VecDeque::from(scheduled.clone());
2231
2232        // Now we drain in batches of 4
2233
2234        let mut need_preamble = false;
2235        let mut skip_in_chunk = 0;
2236
2237        let next_batch =
2238            drain_from_instructions(&mut to_drain, 4, &mut need_preamble, &mut skip_in_chunk);
2239
2240        assert!(!need_preamble);
2241        assert_eq!(skip_in_chunk, 4);
2242        assert_eq!(
2243            next_batch,
2244            vec![ChunkDrainInstructions {
2245                chunk_instructions: scheduled[0].clone(),
2246                rows_to_take: 4,
2247                rows_to_skip: 0,
2248                preamble_action: PreambleAction::Absent,
2249            }]
2250        );
2251
2252        let next_batch =
2253            drain_from_instructions(&mut to_drain, 4, &mut need_preamble, &mut skip_in_chunk);
2254
2255        assert!(!need_preamble);
2256        assert_eq!(skip_in_chunk, 2);
2257
2258        assert_eq!(
2259            next_batch,
2260            vec![
2261                ChunkDrainInstructions {
2262                    chunk_instructions: scheduled[0].clone(),
2263                    rows_to_take: 1,
2264                    rows_to_skip: 4,
2265                    preamble_action: PreambleAction::Absent,
2266                },
2267                ChunkDrainInstructions {
2268                    chunk_instructions: scheduled[1].clone(),
2269                    rows_to_take: 1,
2270                    rows_to_skip: 0,
2271                    preamble_action: PreambleAction::Take,
2272                },
2273                ChunkDrainInstructions {
2274                    chunk_instructions: scheduled[2].clone(),
2275                    rows_to_take: 2,
2276                    rows_to_skip: 0,
2277                    preamble_action: PreambleAction::Absent,
2278                }
2279            ]
2280        );
2281
2282        let next_batch =
2283            drain_from_instructions(&mut to_drain, 2, &mut need_preamble, &mut skip_in_chunk);
2284
2285        assert!(!need_preamble);
2286        assert_eq!(skip_in_chunk, 0);
2287
2288        assert_eq!(
2289            next_batch,
2290            vec![
2291                ChunkDrainInstructions {
2292                    chunk_instructions: scheduled[2].clone(),
2293                    rows_to_take: 1,
2294                    rows_to_skip: 2,
2295                    preamble_action: PreambleAction::Absent,
2296                },
2297                ChunkDrainInstructions {
2298                    chunk_instructions: scheduled[3].clone(),
2299                    rows_to_take: 1,
2300                    rows_to_skip: 0,
2301                    preamble_action: PreambleAction::Take,
2302                },
2303            ]
2304        );
2305
2306        // Regression case.  Need a chunk with preamble, rows, and trailer (the middle chunk here)
2307        let rep_data: Vec<u64> = vec![5, 2, 3, 3, 20, 0];
2308        let rep_bytes: Vec<u8> = rep_data.iter().flat_map(|v| v.to_le_bytes()).collect();
2309        let chunk_index = MiniBlockChunkIndex::new_nested_for_test(&rep_bytes, 2);
2310        let user_ranges = vec![0..28];
2311
2312        // First, schedule the ranges
2313        let scheduled = ChunkInstructions::schedule_instructions(&chunk_index, &user_ranges);
2314
2315        let mut to_drain = VecDeque::from(scheduled.clone());
2316
2317        // Drain first chunk and some of second chunk
2318
2319        let mut need_preamble = false;
2320        let mut skip_in_chunk = 0;
2321
2322        let next_batch =
2323            drain_from_instructions(&mut to_drain, 7, &mut need_preamble, &mut skip_in_chunk);
2324
2325        assert_eq!(
2326            next_batch,
2327            vec![
2328                ChunkDrainInstructions {
2329                    chunk_instructions: scheduled[0].clone(),
2330                    rows_to_take: 6,
2331                    rows_to_skip: 0,
2332                    preamble_action: PreambleAction::Absent,
2333                },
2334                ChunkDrainInstructions {
2335                    chunk_instructions: scheduled[1].clone(),
2336                    rows_to_take: 1,
2337                    rows_to_skip: 0,
2338                    preamble_action: PreambleAction::Take,
2339                },
2340            ]
2341        );
2342
2343        assert!(!need_preamble);
2344        assert_eq!(skip_in_chunk, 1);
2345
2346        // Now, the tricky part.  We drain the second chunk, including the trailer, and need to make sure
2347        // we get a drain task to take the preamble of the third chunk (and nothing else)
2348        let next_batch =
2349            drain_from_instructions(&mut to_drain, 2, &mut need_preamble, &mut skip_in_chunk);
2350
2351        assert_eq!(
2352            next_batch,
2353            vec![
2354                ChunkDrainInstructions {
2355                    chunk_instructions: scheduled[1].clone(),
2356                    rows_to_take: 2,
2357                    rows_to_skip: 1,
2358                    preamble_action: PreambleAction::Skip,
2359                },
2360                ChunkDrainInstructions {
2361                    chunk_instructions: scheduled[2].clone(),
2362                    rows_to_take: 0,
2363                    rows_to_skip: 0,
2364                    preamble_action: PreambleAction::Take,
2365                },
2366            ]
2367        );
2368
2369        assert!(!need_preamble);
2370        assert_eq!(skip_in_chunk, 0);
2371    }
2372
2373    /// Builds a `Words` metadata buffer (u16 words) from `(log_num_values, num_bytes)`
2374    /// pairs, returning the words and the total data-buffer size.
2375    fn words_from(entries: &[(u32, u32)]) -> (Words, u64) {
2376        let mut raw = Vec::with_capacity(entries.len() * 2);
2377        let mut total = 0u64;
2378        for &(log, num_bytes) in entries {
2379            assert!(num_bytes > 0 && num_bytes % MINIBLOCK_ALIGNMENT as u32 == 0);
2380            let divided = num_bytes / MINIBLOCK_ALIGNMENT as u32 - 1;
2381            let word = (divided << 4) | log;
2382            assert!(word <= u16::MAX as u32, "test word {word} exceeds u16");
2383            raw.extend_from_slice(&(word as u16).to_le_bytes());
2384            total += num_bytes as u64;
2385        }
2386        (Words::from_bytes(Bytes::from(raw), false).unwrap(), total)
2387    }
2388
2389    fn rep_bytes_from(values: &[u64]) -> Vec<u8> {
2390        values.iter().flat_map(|v| v.to_le_bytes()).collect()
2391    }
2392
2393    #[rstest]
2394    // Two full chunks of 8 values (log 3) plus a partial last chunk; byte sizes vary
2395    // independently of value counts.
2396    #[case::uniform_partial_last(&[(3, 16), (3, 24), (0, 8)], 19, "uniform_flat", 8, 3)]
2397    // Single chunk covers the whole page.
2398    #[case::single_chunk(&[(0, 24)], 5, "uniform_flat", 5, 5)]
2399    // Last chunk is also full (exact multiple).
2400    #[case::exact_multiple(&[(3, 16), (3, 16)], 16, "uniform_flat", 8, 8)]
2401    // Non-last chunks differ in size, so this is a non-uniform flat page.
2402    #[case::non_uniform(&[(4, 16), (2, 16), (0, 8)], 21, "flat", 16, 1)]
2403    fn test_flat_detection(
2404        #[case] entries: &[(u32, u32)],
2405        #[case] items_in_page: u64,
2406        #[case] expected_kind: &str,
2407        #[case] expected_first_items: u64,
2408        #[case] expected_last_items: u64,
2409    ) {
2410        let base = 100u64;
2411        let (words, data_buf_size) = words_from(entries);
2412        let index = build_chunk_index(&words, items_in_page, base, data_buf_size, None, 0).unwrap();
2413
2414        assert_eq!(index.row_mapping_debug(), expected_kind);
2415        assert_eq!(index.num_chunks(), entries.len());
2416        assert_eq!(index.items_in_chunk(0), expected_first_items);
2417        assert_eq!(index.items_in_chunk(entries.len() - 1), expected_last_items);
2418
2419        // Byte ranges are absolute, contiguous, and exactly cover the data buffer.
2420        let mut expected_start = base;
2421        for (i, &(_, num_bytes)) in entries.iter().enumerate() {
2422            let range = index.byte_range(i);
2423            assert_eq!(range.start, expected_start);
2424            assert_eq!(range.end - range.start, num_bytes as u64);
2425            expected_start = range.end;
2426        }
2427        assert_eq!(expected_start, base + data_buf_size);
2428
2429        // For flat pages rows == items, so the per-chunk items sum to the page total.
2430        let total_items: u64 = (0..index.num_chunks())
2431            .map(|i| index.items_in_chunk(i))
2432            .sum();
2433        assert_eq!(total_items, items_in_page);
2434    }
2435
2436    #[test]
2437    fn test_nested_detection_and_axes() {
2438        // Repetition index (stride 2): three chunks holding 5, 4, 3 rows, no trailers.
2439        let rep = rep_bytes_from(&[5, 0, 4, 0, 3, 0]);
2440
2441        // Uniform leaf chunking: value counts 4, 4, 2.
2442        let (words, data_buf_size) = words_from(&[(2, 8), (2, 8), (0, 8)]);
2443        let index = build_chunk_index(&words, 10, 0, data_buf_size, Some(&rep), 1).unwrap();
2444        assert_eq!(index.row_mapping_debug(), "nested");
2445        assert_eq!(index.num_chunks(), 3);
2446        // Rows come from the repetition index, not the value counts.
2447        assert_eq!(index.first_row(0), 0);
2448        assert_eq!(index.rows_in_chunk(0), 5);
2449        assert_eq!(index.first_row(1), 5);
2450        assert_eq!(index.rows_in_chunk(1), 4);
2451        assert_eq!(index.first_row(2), 9);
2452        assert_eq!(index.rows_in_chunk(2), 3);
2453        // Items come from the value words.
2454        assert_eq!(index.items_in_chunk(0), 4);
2455        assert_eq!(index.items_in_chunk(1), 4);
2456        assert_eq!(index.items_in_chunk(2), 2);
2457
2458        // Non-uniform leaf chunking: value counts 8, 2, 5.
2459        let (words_nu, dbs_nu) = words_from(&[(3, 8), (1, 8), (0, 8)]);
2460        let index_nu = build_chunk_index(&words_nu, 15, 0, dbs_nu, Some(&rep), 1).unwrap();
2461        assert_eq!(index_nu.row_mapping_debug(), "nested");
2462        assert_eq!(index_nu.items_in_chunk(0), 8);
2463        assert_eq!(index_nu.items_in_chunk(1), 2);
2464        assert_eq!(index_nu.items_in_chunk(2), 5);
2465        // The row axis is unchanged by the leaf chunking.
2466        assert_eq!(index_nu.rows_in_chunk(0), 5);
2467    }
2468
2469    #[test]
2470    fn test_uniform_flat_matches_prefix_sum_flat() {
2471        // Distribution: 4 chunks of 4 values, last chunk 3 (15 items total).
2472        let (words, data_buf_size) = words_from(&[(2, 8), (2, 8), (2, 8), (0, 8)]);
2473        let uniform = build_chunk_index(&words, 15, 0, data_buf_size, None, 0).unwrap();
2474        assert_eq!(uniform.row_mapping_debug(), "uniform_flat");
2475
2476        // The same distribution expressed as a non-uniform Flat prefix-sum index.
2477        let byte_starts = PrefixSums::from_deltas([8u64, 8, 8, 8].into_iter(), 4, 32);
2478        let value_starts = PrefixSums::from_deltas([4u64, 4, 4, 3].into_iter(), 4, 15);
2479        let flat = MiniBlockChunkIndex::new(0, byte_starts, RowMapping::Flat { value_starts });
2480        assert_eq!(flat.row_mapping_debug(), "flat");
2481
2482        // Lookup parity: identical byte ranges and item counts.
2483        for i in 0..4 {
2484            assert_eq!(uniform.byte_range(i), flat.byte_range(i));
2485            assert_eq!(uniform.items_in_chunk(i), flat.items_in_chunk(i));
2486        }
2487
2488        // Scheduler parity across scan / single-row / partial / scattered multi-range.
2489        let range_sets: Vec<Vec<std::ops::Range<u64>>> = vec![
2490            vec![0..15],
2491            vec![0..1],
2492            vec![7..8],
2493            vec![14..15],
2494            vec![3..10],
2495            vec![0..2, 5..6, 12..15],
2496        ];
2497        for ranges in &range_sets {
2498            let from_uniform = ChunkInstructions::schedule_instructions(&uniform, ranges);
2499            let from_flat = ChunkInstructions::schedule_instructions(&flat, ranges);
2500            assert_eq!(from_uniform, from_flat, "mismatch for ranges {ranges:?}");
2501        }
2502
2503        // A full scan yields one Absent, no-trailer instruction per chunk.
2504        let full = ChunkInstructions::schedule_instructions(&uniform, &[0..15]);
2505        assert_eq!(full.len(), 4);
2506        for (i, inst) in full.iter().enumerate() {
2507            assert_eq!(inst.chunk_idx, i);
2508            assert_eq!(inst.preamble, PreambleAction::Absent);
2509            assert_eq!(inst.rows_to_skip, 0);
2510            assert!(!inst.take_trailer);
2511        }
2512        assert_eq!(full.iter().map(|i| i.rows_to_take).sum::<u64>(), 15);
2513    }
2514
2515    #[test]
2516    fn test_deep_size_per_variant_below_legacy() {
2517        // The previous representation cached 48 bytes per chunk (24 for ChunkMeta plus
2518        // 24 for a rep-index block); every variant's heap must be well below that.
2519        const LEGACY_PER_CHUNK: usize = 48;
2520        let num_chunks = 3;
2521        let heap = |index: &MiniBlockChunkIndex| index.deep_size_of_children(&mut Context::new());
2522
2523        let (uniform_words, uniform_dbs) = words_from(&[(2, 8), (2, 8), (0, 8)]);
2524        let uniform = build_chunk_index(&uniform_words, 10, 0, uniform_dbs, None, 0).unwrap();
2525        assert_eq!(uniform.row_mapping_debug(), "uniform_flat");
2526        assert!(heap(&uniform) < LEGACY_PER_CHUNK * num_chunks);
2527
2528        let (flat_words, flat_dbs) = words_from(&[(3, 8), (1, 8), (0, 8)]);
2529        let flat = build_chunk_index(&flat_words, 11, 0, flat_dbs, None, 0).unwrap();
2530        assert_eq!(flat.row_mapping_debug(), "flat");
2531        assert!(heap(&flat) < LEGACY_PER_CHUNK * num_chunks);
2532        // Flat carries a value-starts array that UniformFlat derives arithmetically.
2533        assert!(heap(&flat) > heap(&uniform));
2534
2535        let rep = rep_bytes_from(&[4, 0, 3, 0, 3, 0]);
2536        let (nested_words, nested_dbs) = words_from(&[(2, 8), (2, 8), (0, 8)]);
2537        let nested = build_chunk_index(&nested_words, 10, 0, nested_dbs, Some(&rep), 1).unwrap();
2538        assert_eq!(nested.row_mapping_debug(), "nested");
2539        assert!(heap(&nested) < LEGACY_PER_CHUNK * num_chunks);
2540    }
2541
2542    #[test]
2543    fn test_miniblock_dictionary_out_of_line_bitpacking_decode() {
2544        let rows = 10_000;
2545        let unique_values = 2_000;
2546
2547        let dictionary_encoding =
2548            EncodingUtils::out_of_line_bitpacking(64, EncodingUtils::flat(11, None));
2549        let layout = metadata::MiniBlockLayout {
2550            rep_compression: None,
2551            def_compression: None,
2552            value_compression: Some(EncodingUtils::flat(64, None)),
2553            dictionary: Some(dictionary_encoding),
2554            num_dictionary_items: unique_values,
2555            layers: vec![metadata::RepDefLayer::RepdefAllValidItem as i32],
2556            num_buffers: 1,
2557            repetition_index_depth: 0,
2558            num_items: rows,
2559            has_large_chunk: false,
2560        };
2561
2562        let buffer_offsets_and_sizes = vec![(0, 0), (0, 0), (0, 0)];
2563        let scheduler = super::MiniBlockScheduler::try_new(
2564            &buffer_offsets_and_sizes,
2565            /*priority=*/ 0,
2566            /*items_in_page=*/ rows,
2567            &layout,
2568            &DefaultDecompressionStrategy::default(),
2569        )
2570        .unwrap();
2571
2572        let dictionary = scheduler.dictionary.unwrap();
2573        assert_eq!(dictionary.num_dictionary_items, unique_values);
2574        assert_eq!(
2575            dictionary.dictionary_data_alignment,
2576            crate::encoder::MIN_PAGE_BUFFER_ALIGNMENT
2577        );
2578    }
2579
2580    #[test]
2581    fn test_miniblock_dictionary_rle_decode() {
2582        let rows = 10_000;
2583
2584        // `create_block_compressor` picks RLE for dictionary values when the
2585        // merged values buffer is dominated by runs (e.g. several batches each
2586        // contributing the same single dictionary value into one page).  The
2587        // reader must accept the same encoding the writer emits.
2588        let dictionary_encoding =
2589            EncodingUtils::rle(EncodingUtils::flat(64, None), EncodingUtils::flat(8, None));
2590        let layout = metadata::MiniBlockLayout {
2591            rep_compression: None,
2592            def_compression: None,
2593            value_compression: Some(EncodingUtils::flat(64, None)),
2594            dictionary: Some(dictionary_encoding),
2595            num_dictionary_items: 1,
2596            layers: vec![metadata::RepDefLayer::RepdefAllValidItem as i32],
2597            num_buffers: 1,
2598            repetition_index_depth: 0,
2599            num_items: rows,
2600            has_large_chunk: false,
2601        };
2602
2603        let buffer_offsets_and_sizes = vec![(0, 0), (0, 0), (0, 0)];
2604        let scheduler = super::MiniBlockScheduler::try_new(
2605            &buffer_offsets_and_sizes,
2606            /*priority=*/ 0,
2607            /*items_in_page=*/ rows,
2608            &layout,
2609            &DefaultDecompressionStrategy::default(),
2610        )
2611        .unwrap();
2612
2613        let dictionary = scheduler.dictionary.unwrap();
2614        assert_eq!(dictionary.num_dictionary_items, 1);
2615        assert_eq!(dictionary.dictionary_data_alignment, 1);
2616    }
2617
2618    #[test]
2619    fn miniblock_levels_use_one_count_for_mixed_codecs() {
2620        let actual_num_levels = usize::from(u16::MAX) + 8;
2621        let levels = vec![1_u16; actual_num_levels];
2622        let rep_frame = encoded_u16_frame(&levels, RunLengthWidth::U8);
2623        let rep_decompressor = RleDecompressor::new(16);
2624        let def_frame = LanceBuffer::reinterpret_slice(Arc::from(levels.clone()));
2625        let def_decompressor = ValueDecompressor::from_flat(&metadata::Flat {
2626            bits_per_value: 16,
2627            data: None,
2628        });
2629
2630        let num_levels = DecodeMiniBlockTask::resolve_num_levels(
2631            Some(&rep_decompressor),
2632            Some(&rep_frame),
2633            Some(&def_decompressor),
2634            Some(&def_frame),
2635            7,
2636        )
2637        .unwrap();
2638        assert_eq!(num_levels, actual_num_levels as u64);
2639
2640        let rep =
2641            DecodeMiniBlockTask::decode_levels(&rep_decompressor, rep_frame, num_levels).unwrap();
2642        let def =
2643            DecodeMiniBlockTask::decode_levels(&def_decompressor, def_frame, num_levels).unwrap();
2644        assert_eq!(rep.as_ref(), levels);
2645        assert_eq!(def, rep);
2646    }
2647
2648    #[test]
2649    fn miniblock_levels_reject_non_wrapped_count_mismatch() {
2650        let frame = encoded_u16_frame(&[1_u16; 8], RunLengthWidth::U8);
2651        let decompressor = RleDecompressor::new(16);
2652
2653        let error = DecodeMiniBlockTask::resolve_num_levels(
2654            Some(&decompressor),
2655            Some(&frame),
2656            None,
2657            None,
2658            7,
2659        )
2660        .unwrap_err();
2661        assert!(matches!(error, lance_core::Error::InvalidInput { .. }));
2662        assert!(error.to_string().contains("not congruent modulo 65536"));
2663    }
2664
2665    #[test]
2666    fn miniblock_levels_reject_cross_stream_count_disagreement() {
2667        let rep_levels = vec![1_u16; usize::from(u16::MAX) + 8];
2668        let def_levels = vec![1_u16; rep_levels.len() + usize::from(u16::MAX) + 1];
2669        let rep_frame = encoded_u16_frame(&rep_levels, RunLengthWidth::U8);
2670        let def_frame = encoded_u16_frame(&def_levels, RunLengthWidth::U8);
2671        let decompressor = RleDecompressor::new(16);
2672
2673        let error = DecodeMiniBlockTask::resolve_num_levels(
2674            Some(&decompressor),
2675            Some(&rep_frame),
2676            Some(&decompressor),
2677            Some(&def_frame),
2678            7,
2679        )
2680        .unwrap_err();
2681        assert!(matches!(error, lance_core::Error::InvalidInput { .. }));
2682        assert!(
2683            error
2684                .to_string()
2685                .contains("structural streams disagree on the level count")
2686        );
2687    }
2688}