Skip to main content

lance_encoding/encodings/logical/primitive/
field.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::collections::VecDeque;
5use std::fmt::Debug;
6use std::ops::Range;
7use std::sync::Arc;
8use std::vec;
9
10use arrow_array::{Array, cast::AsArray, make_array};
11use arrow_schema::{DataType, Field as ArrowField};
12use bytes::Bytes;
13use futures::future::BoxFuture;
14use futures::stream::FuturesOrdered;
15use futures::{FutureExt, TryStreamExt};
16use itertools::Itertools;
17use lance_arrow::{DataTypeExt, r#struct::StructArrayExt};
18use lance_core::Result;
19use lance_core::cache::LanceCache;
20use lance_core::datatypes::Field;
21use lance_core::error::{Error, LanceOptionExt};
22use log::{debug, trace};
23
24use crate::EncodingsIo;
25use crate::compression::DecompressionStrategy;
26use crate::decoder::{
27    ColumnInfo, DecodePageTask, DecodedArray, FilterExpression, IO_REQUEST_BATCH_BYTES,
28    LoadedPageShard, MessageType, PageEncoding, PageInfo, RequestBatch, ScheduledScanLine,
29    SchedulerContext, StructuralDecodeArrayTask, StructuralFieldDecoder, StructuralFieldScheduler,
30    StructuralPageDecoder, StructuralSchedulingJob, UnloadedPageShard,
31};
32use crate::encodings::logical::primitive::blob::{BlobDescriptionPageScheduler, BlobPageScheduler};
33use crate::format::metadata::PageLayout;
34use crate::format::{EncodingUtils, metadata};
35use crate::repdef::{CompositeRepDefUnraveler, DefinitionInterpretation};
36
37use super::{
38    CachedPage, CachedPageData, ComplexAllNullScheduler, FullZipScheduler, LevelCodec,
39    MiniBlockScheduler, NoCachedPageData, PageDataCacheKey, PageInitialization,
40    SimpleAllNullScheduler, StructuralPageScheduler,
41};
42use super::{constant, planar, sparse};
43
44#[derive(Debug)]
45struct StructuralPrimitiveFieldSchedulingJob<'a> {
46    scheduler: &'a StructuralPrimitiveFieldScheduler,
47    mappings: std::vec::IntoIter<PageRangeMapping>,
48}
49
50impl<'a> StructuralPrimitiveFieldSchedulingJob<'a> {
51    pub fn try_new(
52        scheduler: &'a StructuralPrimitiveFieldScheduler,
53        ranges: Vec<Range<u64>>,
54    ) -> Result<Self> {
55        let mappings = map_ranges_to_pages(&scheduler.page_row_ends, &ranges)?.into_iter();
56        Ok(Self {
57            scheduler,
58            mappings,
59        })
60    }
61}
62
63impl StructuralSchedulingJob for StructuralPrimitiveFieldSchedulingJob<'_> {
64    fn schedule_next(&mut self, context: &mut SchedulerContext) -> Result<Vec<ScheduledScanLine>> {
65        let Some(first_mapping) = self.mappings.next() else {
66            return Ok(Vec::new());
67        };
68        // Pages are scheduled in row order until their reads add up to a
69        // batch, then submitted together so the I/O scheduler can coalesce
70        // neighbouring pages.
71        let batch = Arc::new(RequestBatch::new(context.io().clone()));
72        let batch_io: Arc<dyn EncodingsIo> = batch.clone();
73        let cur_path = context.current_path();
74        let mut scan_lines = Vec::new();
75        let mut mapping = Some(first_mapping);
76        while let Some(cur_mapping) = mapping.take() {
77            let cur_page = self.scheduler.built_page(cur_mapping.page_idx)?;
78
79            trace!(
80                "Scheduling {} rows across {} ranges from page with {} rows (column_index={}, page_index={})",
81                cur_mapping
82                    .ranges_in_page
83                    .iter()
84                    .map(|r| r.end - r.start)
85                    .sum::<u64>(),
86                cur_mapping.ranges_in_page.len(),
87                cur_page.row_range.end - cur_page.row_range.start,
88                self.scheduler.column_index,
89                cur_page.page_index,
90            );
91
92            let page_decoders = cur_page
93                .scheduler
94                .schedule_ranges(&cur_mapping.ranges_in_page, &batch_io)?;
95
96            scan_lines.extend(page_decoders.into_iter().map(|page_load_task| {
97                let cur_path = cur_path.clone();
98                let page_decoder = page_load_task.decoder_fut;
99                let unloaded_page = async move {
100                    let page_decoder = page_decoder.await?;
101                    Ok(LoadedPageShard {
102                        decoder: page_decoder,
103                        path: cur_path,
104                    })
105                }
106                .boxed();
107                ScheduledScanLine {
108                    decoders: vec![MessageType::UnloadedPage(UnloadedPageShard(unloaded_page))],
109                    rows_scheduled: page_load_task.num_rows,
110                }
111            }));
112
113            if batch.pending_bytes() < IO_REQUEST_BATCH_BYTES {
114                mapping = self.mappings.next();
115            }
116        }
117        batch.flush();
118        Ok(scan_lines)
119    }
120}
121
122#[derive(Debug)]
123struct PageInfoAndScheduler {
124    page_index: usize,
125    row_range: Range<u64>,
126    scheduler: Box<dyn StructuralPageScheduler>,
127}
128
129/// Everything needed to build a page scheduler from its `PageInfo` on demand.
130///
131/// Building a page scheduler clones buffer tables and instantiates
132/// decompressors, so a column with hundreds of thousands of pages must not pay
133/// for every page on every read. The field scheduler keeps this source and
134/// builds only the pages `initialize` is asked to cover.
135#[derive(Debug)]
136struct PageSchedulerSource {
137    column_info: Arc<ColumnInfo>,
138    decompressors: Arc<dyn DecompressionStrategy>,
139    cache_repetition_index: bool,
140    target_field: Field,
141}
142
143/// A scheduler for a leaf node
144///
145/// Here we look at the layout of the various pages and delegate scheduling to a scheduler
146/// appropriate for the layout of the page.
147#[derive(Debug)]
148pub struct StructuralPrimitiveFieldScheduler {
149    /// Exclusive end row of every page in the column, in page order, so page
150    /// `i` covers `page_row_ends[i - 1]..page_row_ends[i]` (page 0 starts at 0).
151    page_row_ends: Vec<u64>,
152    /// Page schedulers built so far, ordered by `page_index`. After
153    /// `initialize` this holds exactly the pages the requested ranges touch,
154    /// or every page when no ranges were given.
155    page_schedulers: Vec<PageInfoAndScheduler>,
156    /// Metadata to build pages that are not in `page_schedulers` yet. `None`
157    /// once every page has been built.
158    page_source: Option<PageSchedulerSource>,
159    column_index: u32,
160    target_data_type_tag: Arc<str>,
161}
162
163impl StructuralPrimitiveFieldScheduler {
164    /// Creates the scheduler and builds every page scheduler up front.
165    ///
166    /// This pays for all pages of the column on every read. Prefer
167    /// [`Self::try_new_lazy`], which builds only the pages a read touches.
168    #[deprecated(
169        since = "13.0.0",
170        note = "builds every page scheduler eagerly; use try_new_lazy"
171    )]
172    pub fn try_new(
173        column_info: &ColumnInfo,
174        decompressors: &dyn DecompressionStrategy,
175        cache_repetition_index: bool,
176        target_field: &Field,
177    ) -> Result<Self> {
178        let page_row_ends = Self::page_row_ends(column_info)?;
179        let mut page_schedulers = Vec::with_capacity(page_row_ends.len());
180        let mut row_start = 0_u64;
181        for (page_index, (page_info, &row_end)) in column_info
182            .page_infos
183            .iter()
184            .zip(&page_row_ends)
185            .enumerate()
186        {
187            page_schedulers.push(Self::page_info_to_scheduler(
188                page_info,
189                page_index,
190                row_start..row_end,
191                decompressors,
192                cache_repetition_index,
193                target_field,
194            )?);
195            row_start = row_end;
196        }
197        Ok(Self {
198            page_row_ends,
199            page_schedulers,
200            page_source: None,
201            column_index: column_info.index,
202            target_data_type_tag: format!("{:?}", target_field.data_type()).into(),
203        })
204    }
205
206    /// Creates the scheduler without building any page schedulers.
207    ///
208    /// Only the per-page row boundaries are computed here. Page schedulers
209    /// are built in [`StructuralFieldScheduler::initialize`] for the pages the
210    /// requested ranges touch (or for every page when no ranges are given), so
211    /// a small read from a column with many pages stays cheap.
212    pub fn try_new_lazy(
213        column_info: Arc<ColumnInfo>,
214        decompressors: Arc<dyn DecompressionStrategy>,
215        cache_repetition_index: bool,
216        target_field: &Field,
217    ) -> Result<Self> {
218        Ok(Self {
219            page_row_ends: Self::page_row_ends(&column_info)?,
220            page_schedulers: Vec::new(),
221            column_index: column_info.index,
222            target_data_type_tag: format!("{:?}", target_field.data_type()).into(),
223            page_source: Some(PageSchedulerSource {
224                column_info,
225                decompressors,
226                cache_repetition_index,
227                target_field: target_field.clone(),
228            }),
229        })
230    }
231
232    /// Exclusive end row of every page of `column_info`, in page order.
233    fn page_row_ends(column_info: &ColumnInfo) -> Result<Vec<u64>> {
234        let mut page_row_ends = Vec::with_capacity(column_info.page_infos.len());
235        let mut row_start = 0_u64;
236        for (page_index, page_info) in column_info.page_infos.iter().enumerate() {
237            let row_end = row_start.checked_add(page_info.num_rows).ok_or_else(|| {
238                Error::invalid_input_source(
239                    format!(
240                        "Page row range overflowed for column {} at page {page_index}: start={row_start}, rows={}",
241                        column_info.index, page_info.num_rows
242                    )
243                    .into(),
244                )
245            })?;
246            page_row_ends.push(row_end);
247            row_start = row_end;
248        }
249        Ok(page_row_ends)
250    }
251
252    /// Wraps already-built page schedulers; nothing is built lazily.
253    #[cfg(test)]
254    fn from_page_schedulers(
255        page_schedulers: Vec<PageInfoAndScheduler>,
256        column_index: u32,
257        target_data_type_tag: Arc<str>,
258    ) -> Self {
259        Self {
260            page_row_ends: page_schedulers
261                .iter()
262                .map(|page| page.row_range.end)
263                .collect(),
264            page_schedulers,
265            page_source: None,
266            column_index,
267            target_data_type_tag,
268        }
269    }
270
271    fn page_row_range(&self, page_index: usize) -> Range<u64> {
272        let start = match page_index.checked_sub(1) {
273            Some(previous) => self.page_row_ends[previous],
274            None => 0,
275        };
276        start..self.page_row_ends[page_index]
277    }
278
279    /// Position of `page_index` in `page_schedulers` if it has been built.
280    fn built_page_position(&self, page_index: usize) -> Option<usize> {
281        self.page_schedulers
282            .binary_search_by_key(&page_index, |page| page.page_index)
283            .ok()
284    }
285
286    fn built_page(&self, page_index: usize) -> Result<&PageInfoAndScheduler> {
287        let position = self.built_page_position(page_index).ok_or_else(|| {
288            Error::internal(format!(
289                "Page {page_index} of column {} was scheduled but not initialized; the scheduled ranges must be covered by the ranges passed to initialize",
290                self.column_index
291            ))
292        })?;
293        Ok(&self.page_schedulers[position])
294    }
295
296    /// Builds the schedulers for `page_indices` (ascending) that do not exist
297    /// yet and returns the positions of all of them in `page_schedulers`.
298    fn build_pages(&mut self, page_indices: &[usize]) -> Result<Vec<usize>> {
299        if let Some(source) = &self.page_source {
300            let mut missing = Vec::new();
301            for &page_index in page_indices {
302                if self.built_page_position(page_index).is_some() {
303                    continue;
304                }
305                let page_info = source
306                    .column_info
307                    .page_infos
308                    .get(page_index)
309                    .ok_or_else(|| {
310                        Error::internal(format!(
311                            "Page {page_index} is out of bounds for column {} with {} pages",
312                            self.column_index,
313                            source.column_info.page_infos.len()
314                        ))
315                    })?;
316                missing.push(Self::page_info_to_scheduler(
317                    page_info,
318                    page_index,
319                    self.page_row_range(page_index),
320                    source.decompressors.as_ref(),
321                    source.cache_repetition_index,
322                    &source.target_field,
323                )?);
324            }
325            if !missing.is_empty() {
326                self.page_schedulers = std::mem::take(&mut self.page_schedulers)
327                    .into_iter()
328                    .merge_by(missing, |built, new| built.page_index < new.page_index)
329                    .collect();
330            }
331            if self.page_schedulers.len() == self.page_row_ends.len() {
332                self.page_source = None;
333            }
334        }
335        page_indices
336            .iter()
337            .map(|&page_index| {
338                self.built_page_position(page_index).ok_or_else(|| {
339                    Error::internal(format!(
340                        "Page {page_index} of column {} is missing after construction",
341                        self.column_index
342                    ))
343                })
344            })
345            .collect()
346    }
347
348    fn page_layout_to_scheduler(
349        page_info: &PageInfo,
350        page_layout: &PageLayout,
351        decompressors: &dyn DecompressionStrategy,
352        cache_repetition_index: bool,
353        target_field: &Field,
354    ) -> Result<Box<dyn StructuralPageScheduler>> {
355        use metadata::page_layout::Layout;
356        Ok(match page_layout.layout.as_ref().expect_ok()? {
357            Layout::MiniBlockLayout(mini_block) => Box::new(MiniBlockScheduler::try_new(
358                &page_info.buffer_offsets_and_sizes,
359                page_info.priority,
360                mini_block.num_items,
361                mini_block,
362                decompressors,
363            )?),
364            Layout::FullZipLayout(full_zip) => {
365                let mut scheduler = FullZipScheduler::try_new(
366                    &page_info.buffer_offsets_and_sizes,
367                    page_info.priority,
368                    page_info.num_rows,
369                    full_zip,
370                    decompressors,
371                )?;
372                scheduler.enable_cache = cache_repetition_index;
373                Box::new(scheduler)
374            }
375            Layout::ConstantLayout(constant_layout) => {
376                let def_meaning = constant_layout
377                    .layers
378                    .iter()
379                    .map(|l| EncodingUtils::repdef_layer_to_def_interp(*l))
380                    .collect::<Vec<_>>();
381                let has_scalar_value = constant_layout.inline_value.is_some()
382                    || page_info.buffer_offsets_and_sizes.len() == 1
383                    || page_info.buffer_offsets_and_sizes.len() == 3;
384                if has_scalar_value {
385                    Box::new(constant::ConstantPageScheduler::try_new(
386                        page_info.buffer_offsets_and_sizes.clone(),
387                        constant_layout.inline_value.clone(),
388                        target_field.data_type(),
389                        def_meaning.into(),
390                    )?) as Box<dyn StructuralPageScheduler>
391                } else if def_meaning.len() == 1
392                    && def_meaning[0] == DefinitionInterpretation::NullableItem
393                {
394                    Box::new(SimpleAllNullScheduler::default()) as Box<dyn StructuralPageScheduler>
395                } else {
396                    // RLE levels select a validated cache representation; other
397                    // block compressions keep flowing through the eager decompressor.
398                    let rep_codec = LevelCodec::try_new(
399                        constant_layout.rep_compression.as_ref(),
400                        decompressors,
401                    )?;
402                    let def_codec = LevelCodec::try_new(
403                        constant_layout.def_compression.as_ref(),
404                        decompressors,
405                    )?;
406
407                    Box::new(ComplexAllNullScheduler::new(
408                        page_info.buffer_offsets_and_sizes.clone(),
409                        def_meaning.into(),
410                        rep_codec,
411                        def_codec,
412                        constant_layout.num_rep_values,
413                        constant_layout.num_def_values,
414                    )) as Box<dyn StructuralPageScheduler>
415                }
416            }
417            Layout::BlobLayout(blob) => {
418                let inner_scheduler = Self::page_layout_to_scheduler(
419                    page_info,
420                    blob.inner_layout.as_ref().expect_ok()?.as_ref(),
421                    decompressors,
422                    cache_repetition_index,
423                    target_field,
424                )?;
425                let def_meaning = blob
426                    .layers
427                    .iter()
428                    .map(|l| EncodingUtils::repdef_layer_to_def_interp(*l))
429                    .collect::<Vec<_>>();
430                if matches!(target_field.data_type(), DataType::Struct(_)) {
431                    // User wants to decode blob into struct
432                    Box::new(BlobDescriptionPageScheduler::new(
433                        inner_scheduler,
434                        def_meaning.into(),
435                    ))
436                } else {
437                    // User wants to decode blob into binary data
438                    Box::new(BlobPageScheduler::new(
439                        inner_scheduler,
440                        page_info.priority,
441                        page_info.num_rows,
442                        def_meaning.into(),
443                    ))
444                }
445            }
446        })
447    }
448
449    fn page_info_to_scheduler(
450        page_info: &PageInfo,
451        page_index: usize,
452        row_range: Range<u64>,
453        decompressors: &dyn DecompressionStrategy,
454        cache_repetition_index: bool,
455        target_field: &Field,
456    ) -> Result<PageInfoAndScheduler> {
457        if let PageEncoding::Planar(layout) = &page_info.encoding {
458            let scheduler = if layout.sparse().is_some() {
459                Box::new(sparse::planar::PlanarSparseScheduler::try_new(
460                    page_info,
461                    layout,
462                    target_field,
463                    decompressors,
464                )?) as Box<dyn StructuralPageScheduler>
465            } else {
466                planar::validate_page(page_info, layout, target_field, decompressors)?;
467                Self::page_layout_to_scheduler(
468                    page_info,
469                    &layout.dense_execution_layout()?,
470                    decompressors,
471                    cache_repetition_index,
472                    target_field,
473                )?
474            };
475            return Ok(PageInfoAndScheduler {
476                page_index,
477                row_range,
478                scheduler,
479            });
480        }
481        let page_layout = page_info.encoding.as_structural();
482        let scheduler = Self::page_layout_to_scheduler(
483            page_info,
484            page_layout,
485            decompressors,
486            cache_repetition_index,
487            target_field,
488        )?;
489        Ok(PageInfoAndScheduler {
490            page_index,
491            row_range,
492            scheduler,
493        })
494    }
495}
496
497/// One page's slice of a scheduling request: the page's index and the
498/// page-local row sub-ranges (relative to the page's first row) of the request
499/// that fall in it.
500#[derive(Debug)]
501struct PageRangeMapping {
502    page_idx: usize,
503    ranges_in_page: Vec<Range<u64>>,
504}
505
506/// Visits every intersection between ordered row ranges and ordered pages.
507///
508/// `page_row_ends` holds the exclusive end row of each page (see
509/// [`StructuralPrimitiveFieldScheduler::page_row_ends`]). Each range
510/// binary-searches its first overlapping page and then visits only the pages
511/// it touches. For `R` ranges, `P` pages, and `T` intersections this takes
512/// `O(R log P + T)` time without scanning sparse gaps.
513fn for_each_page_range(
514    page_row_ends: &[u64],
515    ranges: &[Range<u64>],
516    mut visit: impl FnMut(usize, Range<u64>),
517) -> Result<()> {
518    for (range_idx, range) in ranges.iter().enumerate() {
519        if range.start > range.end {
520            return Err(Error::invalid_input_source(
521                format!(
522                    "Requested row range {range_idx} is reversed: {}..{}",
523                    range.start, range.end
524                )
525                .into(),
526            ));
527        }
528        if range_idx > 0 {
529            let previous = &ranges[range_idx - 1];
530            if previous.start >= range.start || previous.end > range.start {
531                return Err(Error::invalid_input_source(
532                    format!(
533                        "Requested row ranges overlap or are not strictly ordered at index {range_idx}: {previous:?}, then {range:?}"
534                    )
535                    .into(),
536                ));
537            }
538        }
539    }
540
541    for range in ranges {
542        let mut page_idx = page_row_ends.partition_point(|page_end| *page_end <= range.start);
543        let mut page_start = match page_idx.checked_sub(1) {
544            Some(previous) => page_row_ends[previous],
545            None => 0,
546        };
547        while let Some(&page_end) = page_row_ends.get(page_idx) {
548            if page_start >= range.end {
549                break;
550            }
551            let start_in_page = range.start.max(page_start) - page_start;
552            let end_in_page = range.end.min(page_end) - page_start;
553            if start_in_page < end_in_page {
554                visit(page_idx, start_in_page..end_in_page);
555            }
556            page_idx += 1;
557            page_start = page_end;
558        }
559    }
560    Ok(())
561}
562
563/// Maps global row `ranges` onto a column's pages, yielding the page-local
564/// sub-ranges for every page any range touches.
565///
566/// `ranges` must be ascending and non-overlapping (the precondition
567/// `schedule_ranges` relies on).
568fn map_ranges_to_pages(
569    page_row_ends: &[u64],
570    ranges: &[Range<u64>],
571) -> Result<Vec<PageRangeMapping>> {
572    let mut result: Vec<PageRangeMapping> =
573        Vec::with_capacity(ranges.len().min(page_row_ends.len()));
574    for_each_page_range(page_row_ends, ranges, |page_idx, range_in_page| {
575        if let Some(mapping) = result.last_mut()
576            && mapping.page_idx == page_idx
577        {
578            mapping.ranges_in_page.push(range_in_page);
579        } else {
580            result.push(PageRangeMapping {
581                page_idx,
582                ranges_in_page: vec![range_in_page],
583            });
584        }
585    })?;
586    Ok(result)
587}
588
589fn pages_overlapping_ranges(page_row_ends: &[u64], ranges: &[Range<u64>]) -> Result<Vec<usize>> {
590    let mut result = Vec::with_capacity(ranges.len().min(page_row_ends.len()));
591    for_each_page_range(page_row_ends, ranges, |page_idx, _| {
592        if result.last().copied() != Some(page_idx) {
593            result.push(page_idx);
594        }
595    })?;
596    Ok(result)
597}
598
599#[derive(Debug)]
600struct PageCacheMiss {
601    /// Position of the page in `StructuralPrimitiveFieldScheduler::page_schedulers`,
602    /// which holds only the built pages and so may differ from `page_index`.
603    page_idx: usize,
604    cache_key: PageDataCacheKey,
605    initialization: PageInitialization,
606}
607
608async fn read_page_initialization_buffers(
609    io: &Arc<dyn EncodingsIo>,
610    all_ranges: Vec<Range<u64>>,
611) -> Result<Vec<Bytes>> {
612    // `submit_request` coalesces (and later un-coalesces) in one forward
613    // pass, so submit ranges in file order. Page layouts are not required
614    // to declare their initialization ranges in that order, so retain each
615    // range's original position and restore it before initializing pages.
616    let mut sorted_ranges = all_ranges.into_iter().enumerate().collect::<Vec<_>>();
617    sorted_ranges.sort_unstable_by_key(|(_, range)| (range.start, range.end));
618    let sorted_requests = sorted_ranges
619        .iter()
620        .map(|(_, range)| range.clone())
621        .collect::<Vec<_>>();
622    let expected_buffer_count = sorted_requests.len();
623    let sorted_buffers = if sorted_requests.is_empty() {
624        Vec::new()
625    } else {
626        io.submit_request(sorted_requests, 0).await?
627    };
628    if sorted_buffers.len() != expected_buffer_count {
629        return Err(Error::invalid_input_source(
630            format!(
631                "Page initialization I/O returned {} buffers for {} ranges",
632                sorted_buffers.len(),
633                expected_buffer_count
634            )
635            .into(),
636        ));
637    }
638    let mut buffers = vec![None; expected_buffer_count];
639    for ((original_idx, _), buffer) in sorted_ranges.into_iter().zip(sorted_buffers) {
640        buffers[original_idx] = Some(buffer);
641    }
642    buffers
643        .into_iter()
644        .map(|buffer| {
645            buffer.ok_or_else(|| Error::internal("Page initialization buffer was not assigned"))
646        })
647        .collect()
648}
649
650async fn cache_initialized_page(
651    cache: Arc<LanceCache>,
652    cache_key: PageDataCacheKey,
653    data: Arc<dyn CachedPageData>,
654) {
655    if data
656        .clone()
657        .as_arc_any()
658        .downcast::<NoCachedPageData>()
659        .is_err()
660    {
661        cache
662            .insert_with_key(&cache_key, Arc::new(CachedPage { data }))
663            .await;
664    }
665}
666
667impl StructuralPrimitiveFieldScheduler {
668    /// Checks the built pages at `positions` (ascending indices into
669    /// `page_schedulers`) against the cache and returns those still needing
670    /// initialization I/O.
671    async fn find_cache_misses(
672        &mut self,
673        positions: impl ExactSizeIterator<Item = usize>,
674        cache: &Arc<LanceCache>,
675    ) -> Result<Vec<PageCacheMiss>> {
676        // Serve cache hits in place; the rest are misses we must read.
677        let mut misses = Vec::with_capacity(positions.len());
678        for page_idx in positions {
679            self.page_schedulers[page_idx]
680                .scheduler
681                .set_cache(cache.clone());
682            let page = &self.page_schedulers[page_idx];
683            if !page.scheduler.needs_initialization() {
684                continue;
685            }
686            let page_index = u32::try_from(page.page_index).map_err(|_| {
687                Error::invalid_input_source(
688                    format!("Page index does not fit u32: {}", page.page_index).into(),
689                )
690            })?;
691            let cache_key = PageDataCacheKey {
692                column_index: self.column_index,
693                page_index,
694                view: page.scheduler.cache_view(),
695                target_data_type_tag: self.target_data_type_tag.clone(),
696            };
697            if let Some(cached_page) = cache.get_with_key(&cache_key).await {
698                match self.page_schedulers[page_idx]
699                    .scheduler
700                    .try_load(&cached_page.data)
701                {
702                    Ok(()) => continue,
703                    Err(error) => {
704                        debug!("Replacing invalid cached page data for {cache_key:?}: {error}");
705                    }
706                }
707            }
708            let initialization = self.page_schedulers[page_idx].scheduler.init_layout()?;
709            misses.push(PageCacheMiss {
710                page_idx,
711                cache_key,
712                initialization,
713            });
714        }
715        Ok(misses)
716    }
717
718    fn initialization_ranges(misses: &[PageCacheMiss]) -> Vec<Range<u64>> {
719        let num_ranges = misses
720            .iter()
721            .map(|miss| miss.initialization.num_required_ranges())
722            .sum();
723        let mut all_ranges = Vec::with_capacity(num_ranges);
724        for miss in misses {
725            all_ranges.extend(miss.initialization.required_ranges().cloned());
726        }
727        all_ranges
728    }
729
730    async fn initialize_cache_misses(
731        &mut self,
732        misses: Vec<PageCacheMiss>,
733        buffers: Vec<Bytes>,
734        cache: Arc<LanceCache>,
735        io: &Arc<dyn EncodingsIo>,
736    ) -> Result<()> {
737        let mut buffers = buffers.into_iter();
738
739        // Initialize each missing page from its slice of the buffers, in file
740        // order. `init_from_buffers` can perform further I/O (e.g. loading blob
741        // descriptions), so run pages concurrently; peel disjoint `&mut` page
742        // borrows off the slice to satisfy the borrow checker without a lookup
743        // table.
744        let mut tail = self.page_schedulers.as_mut_slice();
745        let mut next_page = 0;
746        let mut page_inits = FuturesOrdered::new();
747        for miss in misses {
748            let PageCacheMiss {
749                page_idx,
750                cache_key,
751                initialization,
752            } = miss;
753            let relative_idx = page_idx
754                .checked_sub(next_page)
755                .ok_or_else(|| Error::internal("Page initialization indices are not ordered"))?;
756            if relative_idx >= tail.len() {
757                return Err(Error::internal(format!(
758                    "Page initialization index {page_idx} is out of bounds"
759                )));
760            }
761            let (_, rest) = tail.split_at_mut(relative_idx);
762            let (page, rest) = rest.split_first_mut().ok_or_else(|| {
763                Error::internal(format!(
764                    "Page initialization index {page_idx} is out of bounds"
765                ))
766            })?;
767            tail = rest;
768            next_page = page_idx + 1;
769
770            let count = initialization.num_required_ranges();
771            let page_buffers = buffers.by_ref().take(count).collect::<Vec<_>>();
772            if page_buffers.len() != count {
773                return Err(Error::invalid_input_source(
774                    format!(
775                        "Page {page_idx} received {} initialization buffers, expected {count}",
776                        page_buffers.len()
777                    )
778                    .into(),
779                ));
780            }
781            let page_buffers = initialization.with_buffers(page_buffers)?;
782            let cache = cache.clone();
783            let scheduler = &mut page.scheduler;
784            page_inits.push_back(async move {
785                let cached = scheduler.init_from_buffers(page_buffers, io).await?;
786                scheduler.try_load(&cached)?;
787                cache_initialized_page(cache, cache_key, cached).await;
788                Ok::<(), Error>(())
789            });
790        }
791        if buffers.next().is_some() {
792            return Err(Error::invalid_input_source(
793                "Page initialization returned unassigned buffers".into(),
794            ));
795        }
796
797        page_inits.try_collect::<Vec<()>>().await?;
798        Ok(())
799    }
800
801    async fn initialize_pages(
802        &mut self,
803        requested_ranges: Option<&[Range<u64>]>,
804        context: &SchedulerContext,
805    ) -> Result<()> {
806        let cache = context.cache().clone();
807        let page_indices = match requested_ranges {
808            None => (0..self.page_row_ends.len()).collect::<Vec<_>>(),
809            Some(ranges) => pages_overlapping_ranges(&self.page_row_ends, ranges)?,
810        };
811        let positions = self.build_pages(&page_indices)?;
812        let misses = self
813            .find_cache_misses(positions.into_iter(), &cache)
814            .await?;
815
816        if misses.is_empty() {
817            return Ok(());
818        }
819
820        // Concatenate the misses' metadata ranges into one request so adjacent
821        // ranges coalesce into shared GETs instead of one request per page.
822        let all_ranges = Self::initialization_ranges(&misses);
823        let buffers = read_page_initialization_buffers(context.io(), all_ranges).await?;
824        self.initialize_cache_misses(misses, buffers, cache, context.io())
825            .await
826    }
827}
828
829impl StructuralFieldScheduler for StructuralPrimitiveFieldScheduler {
830    fn initialize<'a>(
831        &'a mut self,
832        requested_ranges: Option<&'a [Range<u64>]>,
833        _filter: &'a FilterExpression,
834        context: &'a SchedulerContext,
835    ) -> BoxFuture<'a, Result<()>> {
836        self.initialize_pages(requested_ranges, context).boxed()
837    }
838
839    fn schedule_ranges<'a>(
840        &'a self,
841        ranges: &[Range<u64>],
842        _filter: &FilterExpression,
843    ) -> Result<Box<dyn StructuralSchedulingJob + 'a>> {
844        let ranges = ranges.to_vec();
845        Ok(Box::new(StructuralPrimitiveFieldSchedulingJob::try_new(
846            self, ranges,
847        )?))
848    }
849}
850
851/// Takes the output from several pages decoders and
852/// concatenates them.
853#[derive(Debug)]
854pub struct StructuralCompositeDecodeArrayTask {
855    tasks: Vec<Box<dyn DecodePageTask>>,
856    should_validate: bool,
857    data_type: DataType,
858}
859
860impl StructuralCompositeDecodeArrayTask {
861    fn restore_validity(
862        array: Arc<dyn Array>,
863        unraveler: &mut CompositeRepDefUnraveler,
864    ) -> Result<Arc<dyn Array>> {
865        let validity = unraveler.unravel_validity(array.len())?;
866        let Some(validity) = validity else {
867            return Ok(array);
868        };
869        if array.data_type() == &DataType::Null {
870            // We unravel from a null array but we don't add the null buffer because arrow-rs doesn't like it
871            return Ok(array);
872        }
873        if validity.len() != array.len() {
874            return Err(Error::invalid_input_source(
875                format!(
876                    "Structural validity has {} entries for an array with {} values",
877                    validity.len(),
878                    array.len()
879                )
880                .into(),
881            ));
882        }
883        // SAFETY: The array buffers have already been validated and the null buffer length
884        // matches the array. We are only attaching the null buffer here.
885        let array = make_array(unsafe {
886            array
887                .to_data()
888                .into_builder()
889                .nulls(Some(validity))
890                .build_unchecked()
891        });
892        if let Some(struct_array) = array.as_struct_opt() {
893            // Packed structs (including blob descriptors) restore validity at the
894            // parent level. Field access reads the child arrays directly, so those
895            // arrays must also mask values beneath a null parent.
896            Ok(Arc::new(struct_array.pushdown_nulls()?))
897        } else {
898            Ok(array)
899        }
900    }
901}
902
903impl StructuralDecodeArrayTask for StructuralCompositeDecodeArrayTask {
904    fn decode(self: Box<Self>) -> Result<DecodedArray> {
905        let mut arrays = Vec::with_capacity(self.tasks.len());
906        let mut unravelers = Vec::with_capacity(self.tasks.len());
907        let mut data_size = 0u64;
908        for task in self.tasks {
909            let decoded = task.decode()?;
910            data_size += decoded.data.data_size();
911            unravelers.push(decoded.repdef);
912
913            let array = make_array(
914                decoded
915                    .data
916                    .into_arrow(self.data_type.clone(), self.should_validate)?,
917            );
918
919            arrays.push(array);
920        }
921        let array_refs = arrays.iter().map(|arr| arr.as_ref()).collect::<Vec<_>>();
922        let array = arrow_select::concat::concat(&array_refs)?;
923        let mut repdef = CompositeRepDefUnraveler::new(unravelers);
924
925        let array = Self::restore_validity(array, &mut repdef)?;
926
927        Ok(DecodedArray {
928            array,
929            repdef,
930            data_size,
931        })
932    }
933}
934
935/// Returns the total bytes consumed by validity bitmaps of nested child fields
936/// for `rows` logical rows of `data_type`.
937///
938/// Only `FixedSizeList` types have nested children that contribute additional
939/// bitmaps; all other fixed-width leaf types return 0.
940fn fsl_child_bitmap_bytes(data_type: &arrow_schema::DataType, rows: u64) -> u64 {
941    match data_type {
942        arrow_schema::DataType::FixedSizeList(child_field, dimension) => {
943            let child_rows = rows * *dimension as u64;
944            let own = if child_field.is_nullable() {
945                child_rows.div_ceil(8)
946            } else {
947                0
948            };
949            own + fsl_child_bitmap_bytes(child_field.data_type(), child_rows)
950        }
951        _ => 0,
952    }
953}
954
955#[derive(Debug)]
956pub struct StructuralPrimitiveFieldDecoder {
957    field: Arc<ArrowField>,
958    page_decoders: VecDeque<Box<dyn StructuralPageDecoder>>,
959    should_validate: bool,
960    rows_drained_in_current: u64,
961}
962
963impl StructuralPrimitiveFieldDecoder {
964    pub fn new(field: &Arc<ArrowField>, should_validate: bool) -> Self {
965        Self {
966            field: field.clone(),
967            page_decoders: VecDeque::new(),
968            should_validate,
969            rows_drained_in_current: 0,
970        }
971    }
972
973    fn decoded_bytes_for_rows(&self, rows: u64) -> lance_core::Result<u64> {
974        let mut remaining = rows;
975        let mut total = 0u64;
976        for (page_num, page) in self.page_decoders.iter().enumerate() {
977            if remaining == 0 {
978                break;
979            }
980            let available = if page_num == 0 {
981                page.num_rows().saturating_sub(self.rows_drained_in_current)
982            } else {
983                page.num_rows()
984            };
985            let take = available.min(remaining);
986            total += page.decoded_bytes(take)?;
987            remaining -= take;
988        }
989        if remaining > 0 {
990            return Err(lance_core::Error::not_supported(format!(
991                "plan_decoded_bytes: queued pages cover only {} of {} requested rows",
992                rows - remaining,
993                rows,
994            )));
995        }
996        Ok(total)
997    }
998}
999
1000impl StructuralFieldDecoder for StructuralPrimitiveFieldDecoder {
1001    fn accept_page(&mut self, child: LoadedPageShard) -> Result<()> {
1002        assert!(child.path.is_empty());
1003        self.page_decoders.push_back(child.decoder);
1004        Ok(())
1005    }
1006
1007    fn drain(&mut self, num_rows: u64) -> Result<Box<dyn StructuralDecodeArrayTask>> {
1008        let mut remaining = num_rows;
1009        let mut tasks = Vec::new();
1010        while remaining > 0 {
1011            let queued_pages = self.page_decoders.len();
1012            let Some(cur_page) = self.page_decoders.front_mut() else {
1013                return Err(Error::internal(format!(
1014                    "Primitive decoder missing page decoder while draining field '{}' (data_type={:?}, requested_rows={}, remaining_rows={}, rows_drained_in_current={}, queued_pages={})",
1015                    self.field.name(),
1016                    self.field.data_type(),
1017                    num_rows,
1018                    remaining,
1019                    self.rows_drained_in_current,
1020                    queued_pages
1021                )));
1022            };
1023            let num_in_page = cur_page.num_rows() - self.rows_drained_in_current;
1024            let to_take = num_in_page.min(remaining);
1025
1026            let task = cur_page.drain(to_take)?;
1027            tasks.push(task);
1028
1029            if to_take == num_in_page {
1030                self.page_decoders.pop_front();
1031                self.rows_drained_in_current = 0;
1032            } else {
1033                self.rows_drained_in_current += to_take;
1034            }
1035
1036            remaining -= to_take;
1037        }
1038        Ok(Box::new(StructuralCompositeDecodeArrayTask {
1039            tasks,
1040            should_validate: self.should_validate,
1041            data_type: self.field.data_type().clone(),
1042        }))
1043    }
1044
1045    fn data_type(&self) -> &DataType {
1046        self.field.data_type()
1047    }
1048
1049    fn plan_decoded_bytes(&self, rows_remaining: u64) -> lance_core::Result<[u64; 8]> {
1050        use crate::decoder::CANDIDATE_BATCH_SIZES;
1051        let data_type = self.field.data_type();
1052        let is_nullable = self.field.is_nullable();
1053
1054        // Fixed-width: exact from type metadata, no page inspection needed.
1055        if let Some(byte_width) = data_type.byte_width_opt() {
1056            let mut out = [0u64; 8];
1057            for (i, &c) in CANDIDATE_BATCH_SIZES.iter().enumerate() {
1058                let rows = (c as u64).min(rows_remaining);
1059                out[i] = rows * byte_width as u64;
1060                if is_nullable {
1061                    out[i] += rows.div_ceil(8);
1062                }
1063                out[i] += fsl_child_bitmap_bytes(data_type, rows);
1064            }
1065            return Ok(out);
1066        }
1067
1068        // Boolean: bit-packed, 1 bit per value.
1069        if matches!(data_type, DataType::Boolean) {
1070            let mut out = [0u64; 8];
1071            for (i, &c) in CANDIDATE_BATCH_SIZES.iter().enumerate() {
1072                let rows = (c as u64).min(rows_remaining);
1073                out[i] = rows.div_ceil(8);
1074                if is_nullable {
1075                    out[i] += rows.div_ceil(8);
1076                }
1077            }
1078            return Ok(out);
1079        }
1080
1081        // Null type: no data bytes.
1082        if matches!(data_type, DataType::Null) {
1083            return Ok([0u64; 8]);
1084        }
1085
1086        // Variable-width: walk page decoders for exact byte counts.
1087        let mut out = [0u64; 8];
1088        for (i, &c) in CANDIDATE_BATCH_SIZES.iter().enumerate() {
1089            let rows = (c as u64).min(rows_remaining);
1090            out[i] = self.decoded_bytes_for_rows(rows)?;
1091            if is_nullable {
1092                out[i] += rows.div_ceil(8);
1093            }
1094        }
1095        Ok(out)
1096    }
1097}
1098
1099#[cfg(test)]
1100#[allow(clippy::single_range_in_vec_init)]
1101mod tests {
1102    use std::ops::Range;
1103    use std::sync::Arc;
1104
1105    use arrow_array::{Array, ArrayRef, Int32Array, StructArray, cast::AsArray};
1106    use arrow_schema::{DataType, Field as ArrowField};
1107    use bytes::Bytes;
1108
1109    use crate::EncodingsIo;
1110    use crate::decoder::{
1111        FilterExpression, RequestBatch, SchedulerContext, StructuralFieldDecoder,
1112        StructuralFieldScheduler,
1113    };
1114    use crate::encodings::logical::primitive::{
1115        CachedPage, CachedPageData, PageCacheView, PageDataCacheKey, SimpleAllNullScheduler,
1116        StructuralPageScheduler,
1117    };
1118    use crate::format::metadata;
1119    use crate::repdef::{CompositeRepDefUnraveler, DefinitionInterpretation, RepDefUnraveler};
1120    use crate::testing::SimulatedScheduler;
1121
1122    use super::{
1123        PageInfoAndScheduler, StructuralCompositeDecodeArrayTask, StructuralPrimitiveFieldDecoder,
1124        StructuralPrimitiveFieldScheduler, map_ranges_to_pages, pages_overlapping_ranges,
1125    };
1126
1127    #[derive(Debug)]
1128    struct CacheMarker(u8);
1129
1130    #[test]
1131    fn test_packed_struct_restores_child_validity() {
1132        let array = StructArray::new(
1133            vec![Arc::new(ArrowField::new("value", DataType::Int32, false))].into(),
1134            vec![Arc::new(Int32Array::from(vec![5, 0, 7])) as ArrayRef],
1135            None,
1136        );
1137        let mut repdef = CompositeRepDefUnraveler::new(vec![RepDefUnraveler::new(
1138            None,
1139            Some(vec![0, 1, 0]),
1140            vec![DefinitionInterpretation::NullableItem].into(),
1141            3,
1142        )]);
1143        let result =
1144            StructuralCompositeDecodeArrayTask::restore_validity(Arc::new(array), &mut repdef)
1145                .unwrap();
1146        let result = result.as_struct();
1147        assert_eq!(result.null_count(), 1);
1148        assert_eq!(result.column(0).nulls(), result.nulls());
1149        assert!(result.column(0).is_null(1));
1150        result.to_data().validate_full().unwrap();
1151    }
1152
1153    impl lance_core::cache::DeepSizeOf for CacheMarker {
1154        fn deep_size_of_children(&self, _context: &mut lance_core::cache::Context) -> usize {
1155            0
1156        }
1157    }
1158
1159    impl CachedPageData for CacheMarker {
1160        fn as_arc_any(self: Arc<Self>) -> Arc<dyn std::any::Any + Send + Sync + 'static> {
1161            self
1162        }
1163    }
1164
1165    #[test]
1166    fn test_primitive_decoder_empty_page_queue_returns_error() {
1167        let field = Arc::new(ArrowField::new("vector", DataType::Float32, true));
1168        let mut decoder = StructuralPrimitiveFieldDecoder::new(&field, false);
1169
1170        let err = decoder.drain(1).unwrap_err();
1171        assert!(
1172            matches!(&err, lance_core::Error::Internal { .. }),
1173            "expected internal error, got: {err:?}"
1174        );
1175        let message = err.to_string();
1176        for expected in [
1177            "Primitive decoder missing page decoder",
1178            "field 'vector'",
1179            "data_type=Float32",
1180            "requested_rows=1",
1181            "remaining_rows=1",
1182            "rows_drained_in_current=0",
1183            "queued_pages=0",
1184        ] {
1185            assert!(
1186                message.contains(expected),
1187                "expected error to contain {expected:?}, got: {message}"
1188            );
1189        }
1190    }
1191
1192    #[test]
1193    fn test_plan_decoded_bytes_i32() {
1194        use crate::decoder::CANDIDATE_BATCH_SIZES;
1195
1196        let field = Arc::new(ArrowField::new("x", DataType::Int32, false));
1197        let decoder = StructuralPrimitiveFieldDecoder::new(&field, false);
1198
1199        let rows_remaining = 100u64;
1200        let bytes = decoder.plan_decoded_bytes(rows_remaining).unwrap();
1201
1202        for (i, &candidate) in CANDIDATE_BATCH_SIZES.iter().enumerate() {
1203            let rows = (candidate as u64).min(rows_remaining);
1204            assert_eq!(bytes[i], rows * 4, "candidate batch size {candidate}");
1205        }
1206    }
1207
1208    #[test]
1209    fn test_plan_decoded_bytes_i32_with_nulls() {
1210        use arrow_array::Int32Array;
1211
1212        // Use N == CANDIDATE_BATCH_SIZES[5] == 1024.  At this size both the
1213        // values buffer (1024*4 = 4096 bytes) and the validity bitmap
1214        // (ceil(1024/8) = 128 bytes) are naturally aligned to Arrow's 64-byte
1215        // allocation boundary, so plan_decoded_bytes matches
1216        // get_buffer_memory_size exactly without needing alignment arithmetic.
1217        const N: i32 = 1024;
1218        let array = Int32Array::from_iter((0..N).map(|i| if i % 2 == 0 { Some(i) } else { None }));
1219        let expected = array.get_buffer_memory_size() as u64;
1220
1221        let field = Arc::new(ArrowField::new("x", DataType::Int32, true));
1222        let decoder = StructuralPrimitiveFieldDecoder::new(&field, false);
1223
1224        let bytes = decoder.plan_decoded_bytes(N as u64).unwrap();
1225        // bytes[5] corresponds to CANDIDATE_BATCH_SIZES[5] == 1024 == N
1226        assert_eq!(
1227            bytes[5], expected,
1228            "plan_decoded_bytes({N}) = {} but get_buffer_memory_size = {expected}",
1229            bytes[5],
1230        );
1231    }
1232
1233    #[test]
1234    fn test_plan_decoded_bytes_nullable_fsl_with_nullable_items() {
1235        // FixedSizeList<nullable i32, DIM> where the FSL itself is also nullable.
1236        // Arrow allocates three buffers:
1237        //   1. FSL null bitmap:       ceil(rows / 8)
1238        //   2. Child i32 null bitmap: ceil(rows * DIM / 8)
1239        //   3. Child i32 values:      rows * DIM * 4
1240        //
1241        // N=1024, DIM=4 keeps all three naturally 64-byte aligned so the
1242        // comparison against get_buffer_memory_size() is exact.
1243        use arrow_array::{FixedSizeListArray, Int32Array};
1244        use arrow_buffer::NullBuffer;
1245
1246        const N: usize = 1024;
1247        const DIM: i32 = 4;
1248
1249        let child_values = Int32Array::from_iter(
1250            (0..(N as i32 * DIM)).map(|i| if i % 2 == 0 { Some(i) } else { None }),
1251        );
1252        let item_field = Arc::new(ArrowField::new("item", DataType::Int32, true));
1253        let fsl_nulls = NullBuffer::from((0..N).map(|i| i % 3 != 0).collect::<Vec<_>>());
1254        let fsl_array = FixedSizeListArray::new(
1255            item_field.clone(),
1256            DIM,
1257            Arc::new(child_values),
1258            Some(fsl_nulls),
1259        );
1260        let expected = fsl_array.get_buffer_memory_size() as u64;
1261
1262        let fsl_type = DataType::FixedSizeList(item_field, DIM);
1263        let field = Arc::new(ArrowField::new("v", fsl_type, true));
1264        let decoder = StructuralPrimitiveFieldDecoder::new(&field, false);
1265
1266        let bytes = decoder.plan_decoded_bytes(N as u64).unwrap();
1267        // bytes[5] corresponds to CANDIDATE_BATCH_SIZES[5] == 1024 == N
1268        assert_eq!(
1269            bytes[5], expected,
1270            "plan_decoded_bytes({N}) = {} but get_buffer_memory_size = {expected}",
1271            bytes[5],
1272        );
1273    }
1274
1275    #[test]
1276    fn test_plan_decoded_bytes_fixed_size_list_of_f32() {
1277        use crate::decoder::CANDIDATE_BATCH_SIZES;
1278
1279        const DIMENSION: i32 = 8;
1280        // Items are non-nullable, which is the typical case for vector embeddings.
1281        let item_field = Arc::new(ArrowField::new("item", DataType::Float32, false));
1282        let fsl_type = DataType::FixedSizeList(item_field, DIMENSION);
1283        let field = Arc::new(ArrowField::new("vector", fsl_type, true));
1284        let decoder = StructuralPrimitiveFieldDecoder::new(&field, false);
1285
1286        let rows_remaining = 100u64;
1287        let bytes = decoder.plan_decoded_bytes(rows_remaining).unwrap();
1288
1289        // Each FSL row is DIMENSION f32 values = DIMENSION * 4 bytes, plus
1290        // 1 bit of validity bitmap (the field is nullable).
1291        let bytes_per_row = DIMENSION as u64 * 4;
1292        for (i, &candidate) in CANDIDATE_BATCH_SIZES.iter().enumerate() {
1293            let rows = (candidate as u64).min(rows_remaining);
1294            assert_eq!(
1295                bytes[i],
1296                rows * bytes_per_row + rows.div_ceil(8),
1297                "candidate batch size {candidate}"
1298            );
1299        }
1300    }
1301
1302    /// Prove that an N-page cache miss issues one `submit_request` carrying every
1303    /// page's metadata range, rather than one submission per page. The production
1304    /// I/O scheduler can then coalesce adjacent ranges within that batch.
1305    #[tokio::test]
1306    async fn test_initialize_coalesces_missed_page_metadata() {
1307        use std::ops::Range;
1308
1309        use futures::FutureExt;
1310        use futures::future::BoxFuture;
1311
1312        use super::{
1313            CachedPage, CachedPageData, Error, PageDataCacheKey, PageInfoAndScheduler,
1314            PageInitialization, StructuralPrimitiveFieldScheduler,
1315        };
1316        use crate::decoder::{FilterExpression, SchedulerContext, StructuralFieldScheduler};
1317        use crate::encodings::logical::primitive::{
1318            PageCacheView, PageInitializationBuffers, PageLoadTask,
1319        };
1320
1321        // Give each fake page one fixed-size metadata buffer. The ranges are far
1322        // apart because this test observes batching, not the I/O layer's distance-
1323        // based range coalescing.
1324        #[derive(Debug)]
1325        struct FakePageScheduler {
1326            meta_range: Range<u64>,
1327        }
1328        impl StructuralPageScheduler for FakePageScheduler {
1329            fn init_layout(&self) -> crate::Result<PageInitialization> {
1330                Ok(PageInitialization::new([Some(self.meta_range.clone())]))
1331            }
1332            fn init_from_buffers<'a>(
1333                &'a mut self,
1334                buffers: PageInitializationBuffers,
1335                _io: &Arc<dyn EncodingsIo>,
1336            ) -> BoxFuture<'a, crate::Result<Arc<dyn CachedPageData>>> {
1337                let [buffer] = buffers.try_into_array("fake").unwrap();
1338                let buffer = buffer.unwrap();
1339                assert_eq!(
1340                    &buffer[..8],
1341                    &self.meta_range.start.to_le_bytes(),
1342                    "each page gets the buffer for its own range"
1343                );
1344                std::future::ready(Ok(Arc::new(CacheMarker(42)) as Arc<dyn CachedPageData>)).boxed()
1345            }
1346            fn try_load(&mut self, data: &Arc<dyn CachedPageData>) -> crate::Result<()> {
1347                data.clone()
1348                    .as_arc_any()
1349                    .downcast::<CacheMarker>()
1350                    .map(|_| ())
1351                    .map_err(|_| {
1352                        Error::invalid_input_source(
1353                            "Fake page cache data has an unexpected type".into(),
1354                        )
1355                    })
1356            }
1357            fn schedule_ranges(
1358                &self,
1359                _ranges: &[Range<u64>],
1360                _io: &Arc<dyn EncodingsIo>,
1361            ) -> crate::Result<Vec<PageLoadTask>> {
1362                unreachable!("initialize never schedules data reads");
1363            }
1364        }
1365
1366        const NUM_PAGES: u64 = 64;
1367        const META_LEN: u64 = 64;
1368        const STRIDE: u64 = 1024 * 1024;
1369        let page_schedulers = (0..NUM_PAGES)
1370            .map(|i| PageInfoAndScheduler {
1371                page_index: i as usize,
1372                row_range: (i * 100)..((i + 1) * 100),
1373                scheduler: Box::new(FakePageScheduler {
1374                    meta_range: (i * STRIDE)..(i * STRIDE + META_LEN),
1375                }),
1376            })
1377            .collect();
1378        let mut scheduler = StructuralPrimitiveFieldScheduler::from_page_schedulers(
1379            page_schedulers,
1380            0,
1381            Arc::from("test"),
1382        );
1383
1384        let io = Arc::new(RecordingScheduler::default());
1385        // A no-op cache always misses, so every page is a miss.
1386        let cache = Arc::new(lance_core::cache::LanceCache::no_cache());
1387        let context = SchedulerContext::new(io.clone(), cache.clone());
1388
1389        // `None` requests every page; against an always-miss cache that's an
1390        // N-page miss.
1391        let filter = FilterExpression::no_filter();
1392        scheduler.initialize(None, &filter, &context).await.unwrap();
1393
1394        {
1395            let requests = io.requests.lock().unwrap();
1396            assert_eq!(
1397                requests.len(),
1398                1,
1399                "an N-page miss must issue exactly one request, not one per page"
1400            );
1401            assert_eq!(
1402                requests[0].len() as u64,
1403                NUM_PAGES,
1404                "the single request must carry every missed page's metadata range"
1405            );
1406        }
1407
1408        io.requests.lock().unwrap().clear();
1409        let cache_misses_before = cache.stats().await.misses;
1410        let requested_ranges = Arc::<[Range<u64>]>::from([150..151, 6_350..6_351]);
1411        scheduler
1412            .initialize(Some(&requested_ranges), &filter, &context)
1413            .await
1414            .unwrap();
1415        assert_eq!(
1416            cache.stats().await.misses - cache_misses_before,
1417            2,
1418            "range-scoped initialization must only look up touched pages"
1419        );
1420
1421        {
1422            let requests = io.requests.lock().unwrap();
1423            assert_eq!(requests.len(), 1);
1424            assert_eq!(
1425                requests[0],
1426                vec![
1427                    STRIDE..(STRIDE + META_LEN),
1428                    63 * STRIDE..(63 * STRIDE + META_LEN),
1429                ],
1430                "range-scoped initialization must omit untouched pages"
1431            );
1432        }
1433
1434        io.requests.lock().unwrap().clear();
1435        let cache = Arc::new(lance_core::cache::LanceCache::with_capacity(4 * 1024));
1436        let cache_key = PageDataCacheKey {
1437            column_index: 9,
1438            page_index: 0,
1439            view: PageCacheView::Native,
1440            target_data_type_tag: Arc::from("test"),
1441        };
1442        cache
1443            .insert_with_key(
1444                &cache_key,
1445                Arc::new(CachedPage {
1446                    data: Arc::new(super::NoCachedPageData),
1447                }),
1448            )
1449            .await;
1450        let mut recovering_scheduler = StructuralPrimitiveFieldScheduler::from_page_schedulers(
1451            vec![PageInfoAndScheduler {
1452                page_index: 0,
1453                row_range: 0..100,
1454                scheduler: Box::new(FakePageScheduler {
1455                    meta_range: 0..META_LEN,
1456                }),
1457            }],
1458            9,
1459            Arc::from("test"),
1460        );
1461        let recovering_context = SchedulerContext::new(io.clone(), cache.clone());
1462        recovering_scheduler
1463            .initialize(None, &filter, &recovering_context)
1464            .await
1465            .unwrap();
1466        assert_eq!(io.requests.lock().unwrap().as_slice(), &[vec![0..META_LEN]]);
1467
1468        io.requests.lock().unwrap().clear();
1469        let mut warm_scheduler = StructuralPrimitiveFieldScheduler::from_page_schedulers(
1470            vec![PageInfoAndScheduler {
1471                page_index: 0,
1472                row_range: 0..100,
1473                scheduler: Box::new(FakePageScheduler {
1474                    meta_range: 0..META_LEN,
1475                }),
1476            }],
1477            9,
1478            Arc::from("test"),
1479        );
1480        warm_scheduler
1481            .initialize(None, &filter, &recovering_context)
1482            .await
1483            .unwrap();
1484        assert!(
1485            io.requests.lock().unwrap().is_empty(),
1486            "the rebuilt cache entry must be reusable"
1487        );
1488
1489        let mut out_of_order_scheduler = StructuralPrimitiveFieldScheduler::from_page_schedulers(
1490            vec![
1491                PageInfoAndScheduler {
1492                    page_index: 0,
1493                    row_range: 0..100,
1494                    scheduler: Box::new(FakePageScheduler {
1495                        meta_range: STRIDE..(STRIDE + META_LEN),
1496                    }),
1497                },
1498                PageInfoAndScheduler {
1499                    page_index: 1,
1500                    row_range: 100..200,
1501                    scheduler: Box::new(FakePageScheduler {
1502                        meta_range: 0..META_LEN,
1503                    }),
1504                },
1505            ],
1506            0,
1507            Arc::from("test"),
1508        );
1509        out_of_order_scheduler
1510            .initialize(None, &filter, &context)
1511            .await
1512            .unwrap();
1513        assert_eq!(
1514            io.requests.lock().unwrap().as_slice(),
1515            &[vec![0..META_LEN, STRIDE..(STRIDE + META_LEN)]],
1516            "metadata reads must be sorted without changing page buffer assignment"
1517        );
1518    }
1519
1520    #[tokio::test]
1521    async fn page_cache_key_distinguishes_page_view_and_target_type() {
1522        let cache = lance_core::cache::LanceCache::with_capacity(4 * 1024);
1523        let cases = [
1524            (
1525                PageDataCacheKey {
1526                    column_index: 7,
1527                    page_index: 0,
1528                    view: PageCacheView::Native,
1529                    target_data_type_tag: Arc::from("Boolean"),
1530                },
1531                10,
1532            ),
1533            (
1534                PageDataCacheKey {
1535                    column_index: 7,
1536                    page_index: 1,
1537                    view: PageCacheView::Native,
1538                    target_data_type_tag: Arc::from("Boolean"),
1539                },
1540                11,
1541            ),
1542            (
1543                PageDataCacheKey {
1544                    column_index: 7,
1545                    page_index: 0,
1546                    view: PageCacheView::Native,
1547                    target_data_type_tag: Arc::from("UInt8"),
1548                },
1549                12,
1550            ),
1551            (
1552                PageDataCacheKey {
1553                    column_index: 7,
1554                    page_index: 0,
1555                    view: PageCacheView::BlobDescriptor,
1556                    target_data_type_tag: Arc::from("Struct"),
1557                },
1558                20,
1559            ),
1560            (
1561                PageDataCacheKey {
1562                    column_index: 7,
1563                    page_index: 0,
1564                    view: PageCacheView::BlobPayload,
1565                    target_data_type_tag: Arc::from("LargeBinary"),
1566                },
1567                21,
1568            ),
1569        ];
1570
1571        for (key, marker) in &cases {
1572            cache
1573                .insert_with_key(
1574                    key,
1575                    Arc::new(CachedPage {
1576                        data: Arc::new(CacheMarker(*marker)),
1577                    }),
1578                )
1579                .await;
1580        }
1581
1582        assert_eq!(cache.size().await, cases.len());
1583        for (key, expected) in &cases {
1584            let cached = cache.get_with_key(key).await.unwrap();
1585            let marker = cached
1586                .data
1587                .clone()
1588                .as_arc_any()
1589                .downcast::<CacheMarker>()
1590                .unwrap();
1591            assert_eq!(marker.0, *expected);
1592        }
1593    }
1594
1595    #[tokio::test]
1596    async fn no_op_pages_do_not_create_cache_entries() {
1597        let mut scheduler = StructuralPrimitiveFieldScheduler::from_page_schedulers(
1598            vec![PageInfoAndScheduler {
1599                page_index: 0,
1600                row_range: 0..10,
1601                scheduler: Box::new(SimpleAllNullScheduler::default()),
1602            }],
1603            3,
1604            Arc::from("test"),
1605        );
1606        let io: Arc<dyn crate::EncodingsIo> =
1607            Arc::new(SimulatedScheduler::new(bytes::Bytes::new()));
1608        let cache = Arc::new(lance_core::cache::LanceCache::with_capacity(4 * 1024));
1609        let context = SchedulerContext::new(io, cache.clone());
1610
1611        scheduler
1612            .initialize(None, &FilterExpression::no_filter(), &context)
1613            .await
1614            .unwrap();
1615
1616        assert_eq!(cache.size().await, 0);
1617    }
1618
1619    #[tokio::test]
1620    async fn initialize_builds_only_requested_page_schedulers() {
1621        use crate::compression::DefaultDecompressionStrategy;
1622        use crate::decoder::{
1623            ColumnInfo, FilterExpression, PageEncoding, PageInfo, SchedulerContext,
1624            StructuralFieldScheduler,
1625        };
1626        use crate::format::pb;
1627        use lance_core::datatypes::Field;
1628
1629        const NUM_PAGES: u64 = 64;
1630        const ROWS_PER_PAGE: u64 = 100;
1631        // All-null constant pages need no initialization I/O, so this test
1632        // observes construction alone.
1633        let page_infos = (0..NUM_PAGES)
1634            .map(|page_index| PageInfo {
1635                num_rows: ROWS_PER_PAGE,
1636                priority: page_index * ROWS_PER_PAGE,
1637                encoding: PageEncoding::Structural(metadata::PageLayout {
1638                    layout: Some(metadata::page_layout::Layout::ConstantLayout(
1639                        metadata::ConstantLayout {
1640                            layers: vec![metadata::RepDefLayer::RepdefNullableItem as i32],
1641                            ..Default::default()
1642                        },
1643                    )),
1644                }),
1645                buffer_offsets_and_sizes: Arc::new([]),
1646            })
1647            .collect::<Vec<_>>();
1648        let column_info = Arc::new(ColumnInfo::new(
1649            3,
1650            Arc::from(page_infos),
1651            Vec::new(),
1652            pb::ColumnEncoding::default(),
1653        ));
1654        let field = Field::try_from(&ArrowField::new("x", DataType::Int32, true)).unwrap();
1655        #[allow(deprecated)]
1656        let eager = StructuralPrimitiveFieldScheduler::try_new(
1657            &column_info,
1658            &DefaultDecompressionStrategy {},
1659            false,
1660            &field,
1661        )
1662        .unwrap();
1663        assert_eq!(eager.page_schedulers.len(), NUM_PAGES as usize);
1664        assert!(eager.page_source.is_none());
1665
1666        let mut scheduler = StructuralPrimitiveFieldScheduler::try_new_lazy(
1667            column_info,
1668            Arc::new(DefaultDecompressionStrategy {}),
1669            false,
1670            &field,
1671        )
1672        .unwrap();
1673        assert!(
1674            scheduler.page_schedulers.is_empty(),
1675            "construction must not build page schedulers"
1676        );
1677        assert_eq!(scheduler.page_row_ends.len(), NUM_PAGES as usize);
1678
1679        let io: Arc<dyn EncodingsIo> = Arc::new(SimulatedScheduler::new(bytes::Bytes::new()));
1680        let cache = Arc::new(lance_core::cache::LanceCache::no_cache());
1681        let mut context = SchedulerContext::new(io, cache);
1682        let filter = FilterExpression::no_filter();
1683
1684        let requested_ranges = [150..151, 6_350..6_351];
1685        scheduler
1686            .initialize(Some(&requested_ranges), &filter, &context)
1687            .await
1688            .unwrap();
1689        let built_pages = |scheduler: &StructuralPrimitiveFieldScheduler| {
1690            scheduler
1691                .page_schedulers
1692                .iter()
1693                .map(|page| page.page_index)
1694                .collect::<Vec<_>>()
1695        };
1696        assert_eq!(built_pages(&scheduler), [1, 63]);
1697        assert_eq!(scheduler.page_schedulers[1].row_range, 6_300..6_400);
1698        assert!(scheduler.page_source.is_some());
1699
1700        {
1701            let mut job = scheduler
1702                .schedule_ranges(&requested_ranges, &filter)
1703                .unwrap();
1704            // Both pages fit one read batch, so one call schedules them together.
1705            assert_eq!(job.schedule_next(&mut context).unwrap().len(), 2);
1706            assert!(job.schedule_next(&mut context).unwrap().is_empty());
1707
1708            // Rows outside the initialized ranges must fail cleanly, not panic
1709            // on an uninitialized page.
1710            let mut job = scheduler.schedule_ranges(&[0..1], &filter).unwrap();
1711            let error = job.schedule_next(&mut context).unwrap_err();
1712            assert!(matches!(error, lance_core::Error::Internal { .. }));
1713            assert!(error.to_string().contains("not initialized"));
1714        }
1715
1716        // Widening the request builds only the new pages and keeps page order.
1717        scheduler
1718            .initialize(Some(&[0..250]), &filter, &context)
1719            .await
1720            .unwrap();
1721        assert_eq!(built_pages(&scheduler), [0, 1, 2, 63]);
1722
1723        // The eager path builds every page and releases the page source.
1724        scheduler.initialize(None, &filter, &context).await.unwrap();
1725        assert_eq!(scheduler.page_schedulers.len(), NUM_PAGES as usize);
1726        assert!(scheduler.page_source.is_none());
1727        let mut job = scheduler.schedule_ranges(&[0..1], &filter).unwrap();
1728        assert_eq!(job.schedule_next(&mut context).unwrap().len(), 1);
1729    }
1730
1731    /// Records every `submit_request` (ranges and priority) and answers each
1732    /// range with a zero buffer that starts with the range's offset, so a test
1733    /// can count requests and check that every reader got its own ranges back.
1734    #[derive(Debug, Default)]
1735    struct RecordingScheduler {
1736        requests: std::sync::Mutex<Vec<Vec<Range<u64>>>>,
1737        priorities: std::sync::Mutex<Vec<u64>>,
1738    }
1739
1740    impl crate::EncodingsIo for RecordingScheduler {
1741        fn submit_request(
1742            &self,
1743            ranges: Vec<Range<u64>>,
1744            priority: u64,
1745        ) -> futures::future::BoxFuture<'static, crate::Result<Vec<Bytes>>> {
1746            use futures::FutureExt;
1747
1748            let buffers = ranges
1749                .iter()
1750                .map(|range| {
1751                    let mut buffer = vec![0u8; (range.end - range.start) as usize];
1752                    let marker = range.start.to_le_bytes();
1753                    let marker_len = marker.len().min(buffer.len());
1754                    buffer[..marker_len].copy_from_slice(&marker[..marker_len]);
1755                    Bytes::from(buffer)
1756                })
1757                .collect();
1758            self.requests.lock().unwrap().push(ranges);
1759            self.priorities.lock().unwrap().push(priority);
1760            std::future::ready(Ok(buffers)).boxed()
1761        }
1762    }
1763
1764    #[tokio::test]
1765    async fn page_read_batch_submits_pending_reads_as_one_request() {
1766        let io = Arc::new(RecordingScheduler::default());
1767        let batch = RequestBatch::new(io.clone());
1768        let first = batch.submit_request(vec![100..108, 300..308], 7);
1769        let second = batch.submit_request(vec![200..208], 3);
1770        assert_eq!(batch.pending_bytes(), 24);
1771        assert!(
1772            io.requests.lock().unwrap().is_empty(),
1773            "nothing is read before the flush"
1774        );
1775
1776        batch.flush();
1777        {
1778            let requests = io.requests.lock().unwrap();
1779            assert_eq!(requests.len(), 1, "the batch is one request");
1780            assert_eq!(
1781                requests[0],
1782                vec![100..108, 200..208, 300..308],
1783                "the batch is sorted by file offset"
1784            );
1785        }
1786        assert_eq!(
1787            io.priorities.lock().unwrap()[0],
1788            3,
1789            "the batch takes the lowest priority"
1790        );
1791        let first = first.await.unwrap();
1792        assert_eq!(first.len(), 2);
1793        assert_eq!(first[0].as_ref(), &100_u64.to_le_bytes());
1794        assert_eq!(first[1].as_ref(), &300_u64.to_le_bytes());
1795        let second = second.await.unwrap();
1796        assert_eq!(second.len(), 1);
1797        assert_eq!(second[0].as_ref(), &200_u64.to_le_bytes());
1798
1799        // A read submitted after the flush is batched until it is awaited.
1800        let late = batch.submit_request(vec![400..408], 9).await.unwrap();
1801        assert_eq!(late[0].as_ref(), &400_u64.to_le_bytes());
1802        assert_eq!(io.requests.lock().unwrap().len(), 2);
1803
1804        // Queued reads that would exceed the budget are submitted first, so
1805        // no batch grows past it.
1806        let io = Arc::new(RecordingScheduler::default());
1807        let batch = RequestBatch::new(io.clone());
1808        let budget = crate::decoder::IO_REQUEST_BATCH_BYTES;
1809        let first = batch.submit_request(vec![0..budget], 1);
1810        assert!(io.requests.lock().unwrap().is_empty());
1811        let second = batch.submit_request(vec![budget..(budget + 8)], 2);
1812        assert_eq!(
1813            io.requests.lock().unwrap().as_slice(),
1814            &[vec![0..budget]],
1815            "the full batch is submitted before the read that would overflow it"
1816        );
1817        batch.flush();
1818        assert_eq!(io.requests.lock().unwrap().len(), 2);
1819        assert_eq!(first.await.unwrap()[0].len(), budget as usize);
1820        assert_eq!(second.await.unwrap()[0].as_ref(), &budget.to_le_bytes());
1821    }
1822
1823    #[tokio::test]
1824    async fn request_batch_flush_keeps_collecting_and_awaiting_flushes() {
1825        let io = Arc::new(RecordingScheduler::default());
1826        let batch = RequestBatch::new(io.clone());
1827        let first = batch.submit_request(vec![100..108], 1);
1828        let second = batch.submit_request(vec![300..308], 2);
1829        batch.flush();
1830        assert_eq!(
1831            io.requests.lock().unwrap().as_slice(),
1832            &[vec![100..108, 300..308]],
1833            "a flush submits the collected reads as one request"
1834        );
1835        // The batch keeps collecting after a flush.
1836        let third = batch.submit_request(vec![500..508], 3);
1837        let fourth = batch.submit_request(vec![700..708], 4);
1838        assert_eq!(batch.pending_bytes(), 16);
1839        assert_eq!(io.requests.lock().unwrap().len(), 1);
1840        // Awaiting a read submits whatever is queued, so a read queued outside
1841        // a scheduling step (indirect I/O) is never stuck behind a missing flush.
1842        assert_eq!(third.await.unwrap()[0].as_ref(), &500_u64.to_le_bytes());
1843        assert_eq!(
1844            io.requests.lock().unwrap().as_slice(),
1845            &[vec![100..108, 300..308], vec![500..508, 700..708]]
1846        );
1847        assert_eq!(fourth.await.unwrap()[0].as_ref(), &700_u64.to_le_bytes());
1848        assert_eq!(first.await.unwrap()[0].as_ref(), &100_u64.to_le_bytes());
1849        assert_eq!(second.await.unwrap()[0].as_ref(), &300_u64.to_le_bytes());
1850        assert_eq!(io.requests.lock().unwrap().len(), 2);
1851
1852        // Futures that submit when first polled and are polled in the same
1853        // pass (struct children initializing, list columns fetching items)
1854        // end up in one request: the flush waits for one yield.
1855        let (fifth, sixth) = futures::join!(
1856            async { batch.submit_request(vec![900..908], 5).await.unwrap() },
1857            async { batch.submit_request(vec![1100..1108], 6).await.unwrap() },
1858        );
1859        assert_eq!(fifth[0].as_ref(), &900_u64.to_le_bytes());
1860        assert_eq!(sixth[0].as_ref(), &1100_u64.to_le_bytes());
1861        assert_eq!(
1862            io.requests.lock().unwrap().last().unwrap(),
1863            &vec![900..908, 1100..1108],
1864            "reads submitted by sibling futures share one request"
1865        );
1866        assert_eq!(io.requests.lock().unwrap().len(), 3);
1867    }
1868
1869    #[tokio::test]
1870    async fn schedule_next_batches_page_reads_up_to_the_budget() {
1871        use super::{
1872            CachedPageData, IO_REQUEST_BATCH_BYTES, PageInfoAndScheduler, PageInitialization,
1873            StructuralPageScheduler, StructuralPrimitiveFieldScheduler,
1874        };
1875        use crate::decoder::{FilterExpression, SchedulerContext, StructuralFieldScheduler};
1876        use crate::encodings::logical::primitive::{PageInitializationBuffers, PageLoadTask};
1877
1878        /// A page that reads `data_ranges`, one shard (and one load task) per
1879        /// range. The bytes it gets back are covered by the batch test, so its
1880        /// load futures just fail and the test never has to build a decoder.
1881        #[derive(Debug)]
1882        struct FakeDataPage {
1883            data_ranges: Vec<Range<u64>>,
1884        }
1885
1886        impl StructuralPageScheduler for FakeDataPage {
1887            fn needs_initialization(&self) -> bool {
1888                false
1889            }
1890
1891            fn init_layout(&self) -> crate::Result<PageInitialization> {
1892                unreachable!("fake data pages need no initialization")
1893            }
1894
1895            fn init_from_buffers<'a>(
1896                &'a mut self,
1897                _buffers: PageInitializationBuffers,
1898                _io: &Arc<dyn EncodingsIo>,
1899            ) -> futures::future::BoxFuture<'a, crate::Result<Arc<dyn CachedPageData>>>
1900            {
1901                unreachable!("fake data pages need no initialization")
1902            }
1903
1904            fn try_load(&mut self, _data: &Arc<dyn CachedPageData>) -> crate::Result<()> {
1905                Ok(())
1906            }
1907
1908            fn schedule_ranges(
1909                &self,
1910                ranges: &[Range<u64>],
1911                io: &Arc<dyn EncodingsIo>,
1912            ) -> crate::Result<Vec<PageLoadTask>> {
1913                use futures::FutureExt;
1914
1915                let num_rows = ranges.iter().map(|r| r.end - r.start).sum();
1916                Ok(self
1917                    .data_ranges
1918                    .iter()
1919                    .enumerate()
1920                    .map(|(shard, range)| {
1921                        drop(io.submit_request(vec![range.clone()], range.start));
1922                        PageLoadTask {
1923                            decoder_fut: std::future::ready(Err(lance_core::Error::internal(
1924                                "fake page",
1925                            )))
1926                            .boxed(),
1927                            // The page's rows are reported once, on the first shard.
1928                            num_rows: if shard == 0 { num_rows } else { 0 },
1929                        }
1930                    })
1931                    .collect())
1932            }
1933        }
1934
1935        fn scheduler_with_pages(
1936            page_bytes: u64,
1937            num_pages: u64,
1938        ) -> StructuralPrimitiveFieldScheduler {
1939            // Leave a gap between pages so the test can tell batching from
1940            // the I/O layer's coalescing of adjacent ranges.
1941            let stride = page_bytes * 2;
1942            let pages = (0..num_pages)
1943                .map(|page| PageInfoAndScheduler {
1944                    page_index: page as usize,
1945                    row_range: (page * 10)..((page + 1) * 10),
1946                    scheduler: Box::new(FakeDataPage {
1947                        data_ranges: vec![(page * stride)..(page * stride + page_bytes)],
1948                    }),
1949                })
1950                .collect();
1951            StructuralPrimitiveFieldScheduler::from_page_schedulers(pages, 0, Arc::from("test"))
1952        }
1953
1954        let filter = FilterExpression::no_filter();
1955        let cache = Arc::new(lance_core::cache::LanceCache::no_cache());
1956
1957        // Small pages: every page of the request is read in one batch.
1958        let io = Arc::new(RecordingScheduler::default());
1959        let scheduler = scheduler_with_pages(64, 6);
1960        let mut context = SchedulerContext::new(io.clone(), cache.clone());
1961        let mut job = scheduler.schedule_ranges(&[5..45], &filter).unwrap();
1962        let scan_lines = job.schedule_next(&mut context).unwrap();
1963        context.flush_io();
1964        assert_eq!(scan_lines.len(), 5, "one scan line per touched page");
1965        assert_eq!(
1966            scan_lines
1967                .iter()
1968                .map(|line| line.rows_scheduled)
1969                .sum::<u64>(),
1970            40
1971        );
1972        assert!(job.schedule_next(&mut context).unwrap().is_empty());
1973        {
1974            let requests = io.requests.lock().unwrap();
1975            assert_eq!(requests.len(), 1, "five pages cost one request");
1976            assert_eq!(
1977                requests[0],
1978                vec![0..64, 128..192, 256..320, 384..448, 512..576],
1979                "the untouched last page is not read"
1980            );
1981        }
1982        assert_eq!(
1983            io.priorities.lock().unwrap()[0],
1984            0,
1985            "the batch carries the first page's priority"
1986        );
1987
1988        // Pages as large as the batch budget are read one request at a time.
1989        let io = Arc::new(RecordingScheduler::default());
1990        let scheduler = scheduler_with_pages(IO_REQUEST_BATCH_BYTES, 2);
1991        let mut context = SchedulerContext::new(io.clone(), cache);
1992        let mut job = scheduler.schedule_ranges(&[0..20], &filter).unwrap();
1993        let first = job.schedule_next(&mut context).unwrap();
1994        context.flush_io();
1995        assert_eq!(first.len(), 1);
1996        assert_eq!(io.requests.lock().unwrap().len(), 1);
1997        let second = job.schedule_next(&mut context).unwrap();
1998        context.flush_io();
1999        assert_eq!(second.len(), 1);
2000        assert_eq!(io.requests.lock().unwrap().len(), 2);
2001        assert!(job.schedule_next(&mut context).unwrap().is_empty());
2002
2003        // A page that shards its own reads (like a blob page) keeps every
2004        // shard in a bounded read instead of one request for the whole page.
2005        let io = Arc::new(RecordingScheduler::default());
2006        let shard = IO_REQUEST_BATCH_BYTES;
2007        let scheduler = StructuralPrimitiveFieldScheduler::from_page_schedulers(
2008            vec![PageInfoAndScheduler {
2009                page_index: 0,
2010                row_range: 0..10,
2011                scheduler: Box::new(FakeDataPage {
2012                    data_ranges: vec![0..shard, (2 * shard)..(3 * shard)],
2013                }),
2014            }],
2015            0,
2016            Arc::from("test"),
2017        );
2018        let mut context = SchedulerContext::new(
2019            io.clone(),
2020            Arc::new(lance_core::cache::LanceCache::no_cache()),
2021        );
2022        let mut job = scheduler.schedule_ranges(&[0..10], &filter).unwrap();
2023        let scan_lines = job.schedule_next(&mut context).unwrap();
2024        context.flush_io();
2025        assert_eq!(scan_lines.len(), 2, "one scan line per shard");
2026        assert_eq!(
2027            io.requests.lock().unwrap().as_slice(),
2028            &[vec![0..shard], vec![(2 * shard)..(3 * shard)]],
2029            "each shard stays its own bounded request"
2030        );
2031        assert!(job.schedule_next(&mut context).unwrap().is_empty());
2032    }
2033
2034    #[test]
2035    fn page_range_mapping_validates_and_splits_ranges() {
2036        // Exclusive end rows of three ten-row pages.
2037        let pages = [10_u64, 20, 30];
2038
2039        let mappings = map_ranges_to_pages(&pages, &[8..22]).unwrap();
2040        assert_eq!(mappings.len(), 3);
2041        assert_eq!(mappings[0].page_idx, 0);
2042        assert_eq!(mappings[0].ranges_in_page, [8..10]);
2043        assert_eq!(mappings[1].page_idx, 1);
2044        assert_eq!(mappings[1].ranges_in_page, [0..10]);
2045        assert_eq!(mappings[2].page_idx, 2);
2046        assert_eq!(mappings[2].ranges_in_page, [0..2]);
2047
2048        let sparse_mappings = map_ranges_to_pages(&pages, &[1..2, 3..4, 28..29]).unwrap();
2049        assert_eq!(sparse_mappings.len(), 2);
2050        assert_eq!(sparse_mappings[0].page_idx, 0);
2051        assert_eq!(sparse_mappings[0].ranges_in_page, [1..2, 3..4]);
2052        assert_eq!(sparse_mappings[1].page_idx, 2);
2053        assert_eq!(sparse_mappings[1].ranges_in_page, [8..9]);
2054        assert_eq!(
2055            pages_overlapping_ranges(&pages, &[1..2, 3..4, 28..29]).unwrap(),
2056            [0, 2]
2057        );
2058
2059        for (ranges, expected_message) in [
2060            (vec![Range { start: 2, end: 1 }], "is reversed"),
2061            (vec![15..16, 5..6], "not strictly ordered"),
2062            (vec![5..15, 10..20], "overlap"),
2063            (vec![5..5, 5..6], "not strictly ordered"),
2064        ] {
2065            let error = map_ranges_to_pages(&pages, &ranges).unwrap_err();
2066            assert!(matches!(error, lance_core::Error::InvalidInput { .. }));
2067            assert!(error.to_string().contains(expected_message));
2068        }
2069    }
2070}