Skip to main content

lance_encoding/
decoder.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Utilities and traits for scheduling & decoding data
5//!
6//! Reading data involves two steps: scheduling and decoding.  The
7//! scheduling step is responsible for figuring out what data is needed
8//! and issuing the appropriate I/O requests.  The decoding step is
9//! responsible for taking the loaded data and turning it into Arrow
10//! arrays.
11//!
12//! # Scheduling
13//!
14//! Scheduling is split into `FieldScheduler` and `PageScheduler`.
15//! There is one field scheduler for each output field, which may map to many
16//! columns of actual data.  A field scheduler is responsible for figuring out
17//! the order in which pages should be scheduled.  Field schedulers then delegate
18//! to page schedulers to figure out the I/O requests that need to be made for
19//! the page.
20//!
21//! Page schedulers also create the decoders that will be used to decode the
22//! scheduled data.
23//!
24//! # Decoding
25//!
26//! Decoders are split into `PhysicalPageDecoder` and
27//! [`LogicalPageDecoder`].  Note that both physical and logical decoding
28//! happens on a per-page basis.  There is no concept of a "field decoder" or
29//! "column decoder".
30//!
31//! The physical decoders handle lower level encodings.  They have a few advantages:
32//!
33//!  * They do not need to decode into an Arrow array and so they don't need
34//!    to be enveloped into the Arrow filesystem (e.g. Arrow doesn't have a
35//!    bit-packed type.  We can use variable-length binary but that is kind
36//!    of awkward)
37//!  * They can decode into an existing allocation.  This can allow for "page
38//!    bridging".  If we are trying to decode into a batch of 1024 rows and
39//!    the rows 0..1024 are spread across two pages then we can avoid a memory
40//!    copy by allocating once and decoding each page into the outer allocation.
41//!    (note: page bridging is not actually implemented yet)
42//!
43//! However, there are some limitations for physical decoders:
44//!
45//!  * They are constrained to a single column
46//!  * The API is more complex
47//!
48//! The logical decoders are designed to map one or more columns of Lance
49//! data into an Arrow array.
50//!
51//! Typically, a "logical encoding" will have both a logical decoder and a field scheduler.
52//! Meanwhile, a "physical encoding" will have a physical decoder but no corresponding field
53//! scheduler.
54//!
55//!
56//! # General notes
57//!
58//! Encodings are typically nested into each other to form a tree.  The top of the tree is
59//! the user requested schema.  Each field in that schema is assigned to one top-level logical
60//! encoding.  That encoding can then contain other logical encodings or physical encodings.
61//! Physical encodings can also contain other physical encodings.
62//!
63//! So, for example, a single field in the Arrow schema might have the type `List<UInt32>`
64//!
65//! The encoding tree could then be:
66//!
67//! root: List (logical encoding)
68//!  - indices: Primitive (logical encoding)
69//!    - column: Basic (physical encoding)
70//!      - validity: Bitmap (physical encoding)
71//!      - values: RLE (physical encoding)
72//!        - runs: Value (physical encoding)
73//!        - values: Value (physical encoding)
74//!  - items: Primitive (logical encoding)
75//!    - column: Basic (physical encoding)
76//!      - values: Value (physical encoding)
77//!
78//! Note that, in this example, root.items.column does not have a validity because there were
79//! no nulls in the page.
80//!
81//! ## Multiple buffers or multiple columns?
82//!
83//! Note that there are many different ways we can write encodings.  For example, we might
84//! store primitive fields in a single column with two buffers (one for validity and one for
85//! values)
86//!
87//! On the other hand, we could also store a primitive field as two different columns.  One
88//! that yields a non-nullable boolean array and one that yields a non-nullable array of items.
89//! Then we could combine these two arrays into a single array where the boolean array is the
90//! bitmap.  There are a few subtle differences between the approaches:
91//!
92//! * Storing things as multiple buffers within the same column is generally more efficient and
93//!   easier to schedule.  For example, in-batch coalescing is very easy but can only be done
94//!   on data that is in the same page.
95//! * When things are stored in multiple columns you have to worry about their pages not being
96//!   in sync.  In our previous validity / values example this means we might have to do some
97//!   memory copies to get the validity array and values arrays to be the same length as
98//!   decode.
99//! * When things are stored in a single column, projection is impossible.  For example, if we
100//!   tried to store all the struct fields in a single column with lots of buffers then we wouldn't
101//!   be able to read back individual fields of the struct.
102//!
103//! The fixed size list decoding is an interesting example because it is actually both a physical
104//! encoding and a logical encoding.  A fixed size list of a physical encoding is, itself, a physical
105//! encoding (e.g. a fixed size list of doubles).  However, a fixed size list of a logical encoding
106//! is a logical encoding (e.g. a fixed size list of structs).
107//!
108//! # The scheduling loop
109//!
110//! Reading a Lance file involves both scheduling and decoding.  Its generally expected that these
111//! will run as two separate threads.
112//!
113//! ```text
114//!
115//!                                    I/O PARALLELISM
116//!                       Issues
117//!                       Requests   ┌─────────────────┐
118//!                                  │                 │        Wait for
119//!                       ┌──────────►   I/O Service   ├─────►  Enough I/O ◄─┐
120//!                       │          │                 │        For batch    │
121//!                       │          └─────────────────┘             │3      │
122//!                       │                                          │       │
123//!                       │                                          │       │2
124//! ┌─────────────────────┴─┐                              ┌─────────▼───────┴┐
125//! │                       │                              │                  │Poll
126//! │       Batch Decode    │ Decode tasks sent via channel│   Batch Decode   │1
127//! │       Scheduler       ├─────────────────────────────►│   Stream         ◄─────
128//! │                       │                              │                  │
129//! └─────▲─────────────┬───┘                              └─────────┬────────┘
130//!       │             │                                            │4
131//!       │             │                                            │
132//!       └─────────────┘                                   ┌────────┴────────┐
133//!  Caller of schedule_range                Buffer polling │                 │
134//!  will be scheduler thread                to achieve CPU │ Decode Batch    ├────►
135//!  and schedule one decode                 parallelism    │ Task            │
136//!  task (and all needed I/O)               (thread per    │                 │
137//!  per logical page                         batch)        └─────────────────┘
138//! ```
139//!
140//! The scheduling thread will work through the file from the
141//! start to the end as quickly as possible.  Data is scheduled one page at a time in a row-major
142//! fashion.  For example, imagine we have a file with the following page structure:
143//!
144//! ```text
145//! Score (Float32)     | C0P0 |
146//! Id (16-byte UUID)   | C1P0 | C1P1 | C1P2 | C1P3 |
147//! Vector (4096 bytes) | C2P0 | C2P1 | C2P2 | C2P3 | .. | C2P1024 |
148//! ```
149//!
150//! This would be quite common as each of these pages has the same number of bytes.  Let's pretend
151//! each page is 1MiB and so there are 256Ki rows of data.  Each page of `Score` has 256Ki rows.
152//! Each page of `Id` has 64Ki rows.  Each page of `Vector` has 256 rows.  The scheduler would then
153//! schedule in the following order:
154//!
155//! C0 P0
156//! C1 P0
157//! C2 P0
158//! C2 P1
159//! ... (254 pages omitted)
160//! C2 P255
161//! C1 P1
162//! C2 P256
163//! ... (254 pages omitted)
164//! C2 P511
165//! C1 P2
166//! C2 P512
167//! ... (254 pages omitted)
168//! C2 P767
169//! C1 P3
170//! C2 P768
171//! ... (254 pages omitted)
172//! C2 P1024
173//!
174//! This is the ideal scheduling order because it means we can decode complete rows as quickly as possible.
175//! Note that the scheduler thread does not need to wait for I/O to happen at any point.  As soon as it starts
176//! it will start scheduling one page of I/O after another until it has scheduled the entire file's worth of
177//! I/O.  This is slightly different than other file readers which have "row group parallelism" and will
178//! typically only schedule X row groups worth of reads at a time.
179//!
180//! In the near future there will be a backpressure mechanism and so it may need to stop/pause if the compute
181//! falls behind.
182//!
183//! ## Indirect I/O
184//!
185//! Regrettably, there are times where we cannot know exactly what data we need until we have partially decoded
186//! the file.  This happens when we have variable sized list data.  In that case the scheduling task for that
187//! page will only schedule the first part of the read (loading the list offsets).  It will then immediately
188//! spawn a new tokio task to wait for that I/O and decode the list offsets.  That follow-up task is not part
189//! of the scheduling loop or the decode loop.  It is a free task.  Once the list offsets are decoded we submit
190//! a follow-up I/O task.  This task is scheduled at a high priority because the decoder is going to need it soon.
191//!
192//! # The decode loop
193//!
194//! As soon as the scheduler starts we can start decoding.  Each time we schedule a page we
195//! push a decoder for that page's data into a channel.  The decode loop
196//! ([`BatchDecodeStream`]) reads from that channel.  Each time it receives a decoder it
197//! waits until the decoder has all of its data.  Then it grabs the next decoder.  Once it has
198//! enough loaded decoders to complete a batch worth of rows it will spawn a "decode batch task".
199//!
200//! These batch decode tasks perform the actual CPU work of decoding the loaded data into Arrow
201//! arrays.  This may involve signifciant CPU processing like decompression or arithmetic in order
202//! to restore the data to its correct in-memory representation.
203//!
204//! ## Batch size
205//!
206//! The `BatchDecodeStream` is configured with a batch size.  This does not need to have any
207//! relation to the page size(s) used to write the data.  This keeps our compute work completely
208//! independent of our I/O work.  We suggest using small batch sizes:
209//!
210//!  * Batches should fit in CPU cache (at least L3)
211//!  * More batches means more opportunity for parallelism
212//!  * The "batch overhead" is very small in Lance compared to other formats because it has no
213//!    relation to the way the data is stored.
214
215use std::collections::VecDeque;
216use std::sync::atomic::{AtomicU64, Ordering};
217use std::sync::{LazyLock, Once, OnceLock};
218use std::{ops::Range, sync::Arc};
219
220use arrow_array::cast::AsArray;
221use arrow_array::{ArrayRef, RecordBatch, RecordBatchIterator, RecordBatchReader};
222use arrow_schema::{ArrowError, DataType, Field as ArrowField, Fields, Schema as ArrowSchema};
223use bytes::Bytes;
224use futures::future::{BoxFuture, MaybeDone, maybe_done};
225use futures::stream::{self, BoxStream};
226use futures::{FutureExt, StreamExt};
227use lance_arrow::DataTypeExt;
228use lance_core::cache::{Context, DeepSizeOf, LanceCache};
229use lance_core::datatypes::{
230    BLOB_DESC_LANCE_FIELD, Field, Schema, validate_fixed_size_list_dimensions,
231};
232use lance_core::utils::futures::{FinallyStreamExt, StreamOnDropExt};
233use lance_core::utils::parse::parse_env_as_bool;
234use log::{debug, trace, warn};
235use prost::Message;
236use tokio::sync::mpsc::error::SendError;
237use tokio::sync::mpsc::{self, unbounded_channel};
238
239use lance_core::error::LanceOptionExt;
240use lance_core::{ArrowResult, Error, Result};
241use tracing::instrument;
242
243use crate::compression::{DecompressionStrategy, DefaultDecompressionStrategy};
244use crate::data::DataBlock;
245use crate::encoder::EncodedBatch;
246use crate::encodings::logical::fixed_size_list::StructuralFixedSizeListScheduler;
247use crate::encodings::logical::list::StructuralListScheduler;
248use crate::encodings::logical::map::StructuralMapScheduler;
249use crate::encodings::logical::primitive::StructuralPrimitiveFieldScheduler;
250use crate::encodings::logical::r#struct::{StructuralStructDecoder, StructuralStructScheduler};
251use crate::format::pb::{self, column_encoding};
252use crate::format::pb21;
253use crate::previous::decoder::LogicalPageDecoder;
254use crate::previous::encodings::logical::list::OffsetPageInfo;
255use crate::previous::encodings::logical::r#struct::{SimpleStructDecoder, SimpleStructScheduler};
256use crate::previous::encodings::logical::{
257    binary::BinaryFieldScheduler, blob::BlobFieldScheduler, list::ListFieldScheduler,
258    primitive::PrimitiveFieldScheduler,
259};
260use crate::repdef::{CompositeRepDefUnraveler, RepDefUnraveler};
261use crate::version::LanceFileVersion;
262use crate::{BufferScheduler, EncodingsIo};
263
264// If users are getting batches over 10MiB large then it's time to reduce the batch size
265const BATCH_SIZE_BYTES_WARNING: u64 = 10 * 1024 * 1024;
266const ENV_LANCE_STRUCTURAL_BATCH_DECODE_SPAWN_MODE: &str =
267    "LANCE_STRUCTURAL_BATCH_DECODE_SPAWN_MODE";
268const ENV_LANCE_READ_CACHE_REPETITION_INDEX: &str = "LANCE_READ_CACHE_REPETITION_INDEX";
269const ENV_LANCE_INLINE_SCHEDULING_THRESHOLD: &str = "LANCE_INLINE_SCHEDULING_THRESHOLD";
270
271// If a request is for at most this many rows we skip the scheduler-task spawn
272// and run scheduling inline as part of the `schedule_and_decode` await.
273const DEFAULT_INLINE_SCHEDULING_THRESHOLD: u64 = 16 * 1024;
274
275fn default_cache_repetition_index() -> bool {
276    static DEFAULT_CACHE_REPETITION_INDEX: OnceLock<bool> = OnceLock::new();
277    *DEFAULT_CACHE_REPETITION_INDEX
278        .get_or_init(|| parse_env_as_bool(ENV_LANCE_READ_CACHE_REPETITION_INDEX, true))
279}
280
281fn inline_scheduling_threshold() -> u64 {
282    static THRESHOLD: OnceLock<u64> = OnceLock::new();
283    *THRESHOLD.get_or_init(|| {
284        std::env::var(ENV_LANCE_INLINE_SCHEDULING_THRESHOLD)
285            .ok()
286            .and_then(|v| v.trim().parse::<u64>().ok())
287            .unwrap_or(DEFAULT_INLINE_SCHEDULING_THRESHOLD)
288    })
289}
290
291/// Top-level encoding message for a page.  Wraps both the
292/// legacy pb::ArrayEncoding and the newer pb::PageLayout
293///
294/// A file should only use one or the other and never both.
295/// 2.0 decoders can always assume this is pb::ArrayEncoding
296/// and 2.1+ decoders can always assume this is pb::PageLayout
297#[derive(Debug)]
298pub enum PageEncoding {
299    Legacy(pb::ArrayEncoding),
300    Structural(pb21::PageLayout),
301}
302
303impl DeepSizeOf for PageEncoding {
304    fn deep_size_of_children(&self, _context: &mut Context) -> usize {
305        match self {
306            Self::Legacy(encoding) => encoding.encoded_len() * 4,
307            Self::Structural(encoding) => encoding.encoded_len() * 4,
308        }
309    }
310}
311
312impl PageEncoding {
313    pub fn as_legacy(&self) -> &pb::ArrayEncoding {
314        match self {
315            Self::Legacy(enc) => enc,
316            Self::Structural(_) => panic!("Expected a legacy encoding"),
317        }
318    }
319
320    pub fn as_structural(&self) -> &pb21::PageLayout {
321        match self {
322            Self::Structural(enc) => enc,
323            Self::Legacy(_) => panic!("Expected a structural encoding"),
324        }
325    }
326
327    pub fn is_structural(&self) -> bool {
328        matches!(self, Self::Structural(_))
329    }
330}
331
332/// Metadata describing a page in a file
333///
334/// This is typically created by reading the metadata section of a Lance file
335#[derive(Debug)]
336pub struct PageInfo {
337    /// The number of rows in the page
338    pub num_rows: u64,
339    /// The priority (top level row number) of the page
340    ///
341    /// This is only set in 2.1 files and will be 0 for 2.0 files
342    pub priority: u64,
343    /// The encoding that explains the buffers in the page
344    pub encoding: PageEncoding,
345    /// The offsets and sizes of the buffers in the file
346    pub buffer_offsets_and_sizes: Arc<[(u64, u64)]>,
347}
348
349impl DeepSizeOf for PageInfo {
350    fn deep_size_of_children(&self, context: &mut Context) -> usize {
351        self.encoding.deep_size_of_children(context)
352            + self.buffer_offsets_and_sizes.deep_size_of_children(context)
353    }
354}
355
356/// Metadata describing a column in a file
357///
358/// This is typically created by reading the metadata section of a Lance file
359#[derive(Debug, Clone)]
360pub struct ColumnInfo {
361    /// The index of the column in the file
362    pub index: u32,
363    /// The metadata for each page in the column
364    pub page_infos: Arc<[PageInfo]>,
365    /// File positions and their sizes of the column-level buffers
366    pub buffer_offsets_and_sizes: Arc<[(u64, u64)]>,
367    pub encoding: pb::ColumnEncoding,
368}
369
370impl DeepSizeOf for ColumnInfo {
371    fn deep_size_of_children(&self, context: &mut Context) -> usize {
372        self.page_infos.deep_size_of_children(context)
373            + self.buffer_offsets_and_sizes.deep_size_of_children(context)
374            + self.encoding.encoded_len() * 4
375    }
376}
377
378impl ColumnInfo {
379    /// Create a new instance
380    pub fn new(
381        index: u32,
382        page_infos: Arc<[PageInfo]>,
383        buffer_offsets_and_sizes: Vec<(u64, u64)>,
384        encoding: pb::ColumnEncoding,
385    ) -> Self {
386        Self {
387            index,
388            page_infos,
389            buffer_offsets_and_sizes: buffer_offsets_and_sizes.into_boxed_slice().into(),
390            encoding,
391        }
392    }
393
394    pub fn is_structural(&self) -> bool {
395        self.page_infos
396            // Can just look at the first since all should be the same
397            .first()
398            .map(|page| page.encoding.is_structural())
399            .unwrap_or(false)
400    }
401}
402
403enum RootScheduler {
404    Structural(Box<dyn StructuralFieldScheduler>),
405    Legacy(Arc<dyn crate::previous::decoder::FieldScheduler>),
406}
407
408impl RootScheduler {
409    fn as_legacy(&self) -> &Arc<dyn crate::previous::decoder::FieldScheduler> {
410        match self {
411            Self::Structural(_) => panic!("Expected a legacy scheduler"),
412            Self::Legacy(s) => s,
413        }
414    }
415
416    fn as_structural(&self) -> &dyn StructuralFieldScheduler {
417        match self {
418            Self::Structural(s) => s.as_ref(),
419            Self::Legacy(_) => panic!("Expected a structural scheduler"),
420        }
421    }
422}
423
424/// The scheduler for decoding batches
425///
426/// Lance decoding is done in two steps, scheduling, and decoding.  The
427/// scheduling tends to be lightweight and should quickly figure what data
428/// is needed from the disk issue the appropriate I/O requests.  A decode task is
429/// created to eventually decode the data (once it is loaded) and scheduling
430/// moves on to scheduling the next page.
431///
432/// Meanwhile, it's expected that a decode stream will be setup to run at the
433/// same time.  Decode tasks take the data that is loaded and turn it into
434/// Arrow arrays.
435///
436/// This approach allows us to keep our I/O parallelism and CPU parallelism
437/// completely separate since those are often two very different values.
438///
439/// Backpressure should be achieved via the I/O service.  Requests that are
440/// issued will pile up if the decode stream is not polling quickly enough.
441/// The [`crate::EncodingsIo::submit_request`] function should return a pending
442/// future once there are too many I/O requests in flight.
443///
444/// TODO: Implement backpressure
445pub struct DecodeBatchScheduler {
446    root_scheduler: RootScheduler,
447    pub root_fields: Fields,
448    cache: Arc<LanceCache>,
449}
450
451pub struct ColumnInfoIter<'a> {
452    column_infos: Vec<Arc<ColumnInfo>>,
453    column_indices: &'a [u32],
454    column_info_pos: usize,
455    column_indices_pos: usize,
456}
457
458impl<'a> ColumnInfoIter<'a> {
459    pub fn new(column_infos: Vec<Arc<ColumnInfo>>, column_indices: &'a [u32]) -> Self {
460        let initial_pos = column_indices.first().copied().unwrap_or(0) as usize;
461        Self {
462            column_infos,
463            column_indices,
464            column_info_pos: initial_pos,
465            column_indices_pos: 0,
466        }
467    }
468
469    pub fn peek(&self) -> &Arc<ColumnInfo> {
470        &self.column_infos[self.column_info_pos]
471    }
472
473    pub fn peek_transform(&mut self, transform: impl FnOnce(Arc<ColumnInfo>) -> Arc<ColumnInfo>) {
474        let column_info = self.column_infos[self.column_info_pos].clone();
475        let transformed = transform(column_info);
476        self.column_infos[self.column_info_pos] = transformed;
477    }
478
479    pub fn expect_next(&mut self) -> Result<&Arc<ColumnInfo>> {
480        self.next().ok_or_else(|| {
481            Error::invalid_input(
482                "there were more fields in the schema than provided column indices / infos",
483            )
484        })
485    }
486
487    fn next(&mut self) -> Option<&Arc<ColumnInfo>> {
488        if self.column_info_pos < self.column_infos.len() {
489            let info = &self.column_infos[self.column_info_pos];
490            self.column_info_pos += 1;
491            Some(info)
492        } else {
493            None
494        }
495    }
496
497    pub(crate) fn next_top_level(&mut self) {
498        self.column_indices_pos += 1;
499        if self.column_indices_pos < self.column_indices.len() {
500            self.column_info_pos = self.column_indices[self.column_indices_pos] as usize;
501        } else {
502            self.column_info_pos = self.column_infos.len();
503        }
504    }
505}
506
507/// These contain the file buffers shared across the entire file
508#[derive(Clone, Copy, Debug)]
509pub struct FileBuffers<'a> {
510    pub positions_and_sizes: &'a [(u64, u64)],
511}
512
513/// These contain the file buffers and also buffers specific to a column
514#[derive(Clone, Copy, Debug)]
515pub struct ColumnBuffers<'a, 'b> {
516    pub file_buffers: FileBuffers<'a>,
517    pub positions_and_sizes: &'b [(u64, u64)],
518}
519
520/// These contain the file & column buffers and also buffers specific to a page
521#[derive(Clone, Copy, Debug)]
522pub struct PageBuffers<'a, 'b, 'c> {
523    pub column_buffers: ColumnBuffers<'a, 'b>,
524    pub positions_and_sizes: &'c [(u64, u64)],
525}
526
527/// The core decoder strategy handles all the various Arrow types
528#[derive(Debug)]
529pub struct CoreFieldDecoderStrategy {
530    pub validate_data: bool,
531    pub decompressor_strategy: Arc<dyn DecompressionStrategy>,
532    pub cache_repetition_index: bool,
533}
534
535impl Default for CoreFieldDecoderStrategy {
536    fn default() -> Self {
537        Self {
538            validate_data: false,
539            decompressor_strategy: Arc::new(DefaultDecompressionStrategy {}),
540            cache_repetition_index: false,
541        }
542    }
543}
544
545impl CoreFieldDecoderStrategy {
546    /// Create a new strategy with cache_repetition_index enabled
547    pub fn with_cache_repetition_index(mut self, cache_repetition_index: bool) -> Self {
548        self.cache_repetition_index = cache_repetition_index;
549        self
550    }
551
552    /// Create a new strategy from decoder config
553    pub fn from_decoder_config(config: &DecoderConfig) -> Self {
554        Self {
555            validate_data: config.validate_on_decode,
556            decompressor_strategy: Arc::new(DefaultDecompressionStrategy {}),
557            cache_repetition_index: config.cache_repetition_index,
558        }
559    }
560
561    /// This is just a sanity check to ensure there is no "wrapped encodings"
562    /// that haven't been handled.
563    fn ensure_values_encoded(column_info: &ColumnInfo, field_name: &str) -> Result<()> {
564        let column_encoding = column_info
565            .encoding
566            .column_encoding
567            .as_ref()
568            .ok_or_else(|| {
569                Error::invalid_input(format!(
570                    "the column at index {} was missing a ColumnEncoding",
571                    column_info.index
572                ))
573            })?;
574        if matches!(
575            column_encoding,
576            pb::column_encoding::ColumnEncoding::Values(_)
577        ) {
578            Ok(())
579        } else {
580            Err(Error::invalid_input(format!(
581                "the column at index {} mapping to the input field {} has column encoding {:?} and no decoder is registered to handle it",
582                column_info.index, field_name, column_encoding
583            )))
584        }
585    }
586
587    fn is_structural_primitive(data_type: &DataType) -> bool {
588        if data_type.is_primitive() {
589            true
590        } else {
591            match data_type {
592                // DataType::is_primitive doesn't consider these primitive but we do
593                DataType::Dictionary(_, value_type) => Self::is_structural_primitive(value_type),
594                DataType::Boolean
595                | DataType::Null
596                | DataType::FixedSizeBinary(_)
597                | DataType::Binary
598                | DataType::LargeBinary
599                | DataType::Utf8
600                | DataType::LargeUtf8 => true,
601                DataType::FixedSizeList(inner, _) => {
602                    Self::is_structural_primitive(inner.data_type())
603                }
604                _ => false,
605            }
606        }
607    }
608
609    fn is_primitive_legacy(data_type: &DataType) -> bool {
610        if data_type.is_primitive() {
611            true
612        } else {
613            match data_type {
614                // DataType::is_primitive doesn't consider these primitive but we do
615                DataType::Boolean | DataType::Null | DataType::FixedSizeBinary(_) => true,
616                DataType::FixedSizeList(inner, _) => Self::is_primitive_legacy(inner.data_type()),
617                _ => false,
618            }
619        }
620    }
621
622    fn create_primitive_scheduler(
623        &self,
624        field: &Field,
625        column: &ColumnInfo,
626        buffers: FileBuffers,
627    ) -> Result<Box<dyn crate::previous::decoder::FieldScheduler>> {
628        Self::ensure_values_encoded(column, &field.name)?;
629        // Primitive fields map to a single column
630        let column_buffers = ColumnBuffers {
631            file_buffers: buffers,
632            positions_and_sizes: &column.buffer_offsets_and_sizes,
633        };
634        Ok(Box::new(PrimitiveFieldScheduler::new(
635            column.index,
636            field.data_type(),
637            column.page_infos.clone(),
638            column_buffers,
639            self.validate_data,
640        )))
641    }
642
643    /// Helper method to verify the page encoding of a struct header column
644    fn check_simple_struct(column_info: &ColumnInfo, field_name: &str) -> Result<()> {
645        Self::ensure_values_encoded(column_info, field_name)?;
646        if column_info.page_infos.len() != 1 {
647            return Err(Error::invalid_input_source(format!("Due to schema we expected a struct column but we received a column with {} pages and right now we only support struct columns with 1 page", column_info.page_infos.len()).into()));
648        }
649        let encoding = &column_info.page_infos[0].encoding;
650        match encoding.as_legacy().array_encoding.as_ref().unwrap() {
651            pb::array_encoding::ArrayEncoding::Struct(_) => Ok(()),
652            _ => Err(Error::invalid_input_source(format!("Expected a struct encoding because we have a struct field in the schema but got the encoding {:?}", encoding).into())),
653        }
654    }
655
656    fn check_packed_struct(column_info: &ColumnInfo) -> bool {
657        let encoding = &column_info.page_infos[0].encoding;
658        matches!(
659            encoding.as_legacy().array_encoding.as_ref().unwrap(),
660            pb::array_encoding::ArrayEncoding::PackedStruct(_)
661        )
662    }
663
664    fn create_list_scheduler(
665        &self,
666        list_field: &Field,
667        column_infos: &mut ColumnInfoIter,
668        buffers: FileBuffers,
669        offsets_column: &ColumnInfo,
670    ) -> Result<Box<dyn crate::previous::decoder::FieldScheduler>> {
671        Self::ensure_values_encoded(offsets_column, &list_field.name)?;
672        let offsets_column_buffers = ColumnBuffers {
673            file_buffers: buffers,
674            positions_and_sizes: &offsets_column.buffer_offsets_and_sizes,
675        };
676        let items_scheduler =
677            self.create_legacy_field_scheduler(&list_field.children[0], column_infos, buffers)?;
678
679        let (inner_infos, null_offset_adjustments): (Vec<_>, Vec<_>) = offsets_column
680            .page_infos
681            .iter()
682            .filter(|offsets_page| offsets_page.num_rows > 0)
683            .map(|offsets_page| {
684                if let Some(pb::array_encoding::ArrayEncoding::List(list_encoding)) =
685                    &offsets_page.encoding.as_legacy().array_encoding
686                {
687                    let inner = PageInfo {
688                        buffer_offsets_and_sizes: offsets_page.buffer_offsets_and_sizes.clone(),
689                        encoding: PageEncoding::Legacy(
690                            list_encoding.offsets.as_ref().unwrap().as_ref().clone(),
691                        ),
692                        num_rows: offsets_page.num_rows,
693                        priority: 0,
694                    };
695                    (
696                        inner,
697                        OffsetPageInfo {
698                            offsets_in_page: offsets_page.num_rows,
699                            null_offset_adjustment: list_encoding.null_offset_adjustment,
700                            num_items_referenced_by_page: list_encoding.num_items,
701                        },
702                    )
703                } else {
704                    // TODO: Should probably return Err here
705                    panic!("Expected a list column");
706                }
707            })
708            .unzip();
709        let inner = Arc::new(PrimitiveFieldScheduler::new(
710            offsets_column.index,
711            DataType::UInt64,
712            Arc::from(inner_infos.into_boxed_slice()),
713            offsets_column_buffers,
714            self.validate_data,
715        )) as Arc<dyn crate::previous::decoder::FieldScheduler>;
716        let items_field = match list_field.data_type() {
717            DataType::List(inner) => inner,
718            DataType::LargeList(inner) => inner,
719            _ => unreachable!(),
720        };
721        let offset_type = if matches!(list_field.data_type(), DataType::List(_)) {
722            DataType::Int32
723        } else {
724            DataType::Int64
725        };
726        Ok(Box::new(ListFieldScheduler::new(
727            inner,
728            items_scheduler.into(),
729            items_field,
730            offset_type,
731            null_offset_adjustments,
732        )))
733    }
734
735    fn unwrap_blob(column_info: &ColumnInfo) -> Option<ColumnInfo> {
736        if let column_encoding::ColumnEncoding::Blob(blob) =
737            column_info.encoding.column_encoding.as_ref().unwrap()
738        {
739            let mut column_info = column_info.clone();
740            column_info.encoding = blob.inner.as_ref().unwrap().as_ref().clone();
741            Some(column_info)
742        } else {
743            None
744        }
745    }
746
747    fn create_structural_field_scheduler(
748        &self,
749        field: &Field,
750        column_infos: &mut ColumnInfoIter,
751    ) -> Result<Box<dyn StructuralFieldScheduler>> {
752        let data_type = field.data_type();
753        validate_fixed_size_list_dimensions(&field.name, &data_type)?;
754        if Self::is_structural_primitive(&data_type) {
755            let column_info = column_infos.expect_next()?;
756            let scheduler = Box::new(StructuralPrimitiveFieldScheduler::try_new(
757                column_info.as_ref(),
758                self.decompressor_strategy.as_ref(),
759                self.cache_repetition_index,
760                field,
761            )?);
762
763            // advance to the next top level column
764            column_infos.next_top_level();
765
766            return Ok(scheduler);
767        }
768        match &data_type {
769            DataType::Struct(fields) => {
770                if field.is_packed_struct() {
771                    // Packed struct
772                    let column_info = column_infos.expect_next()?;
773                    let scheduler = Box::new(StructuralPrimitiveFieldScheduler::try_new(
774                        column_info.as_ref(),
775                        self.decompressor_strategy.as_ref(),
776                        self.cache_repetition_index,
777                        field,
778                    )?);
779
780                    // advance to the next top level column
781                    column_infos.next_top_level();
782
783                    return Ok(scheduler);
784                }
785                // Maybe a blob descriptions struct?
786                if field.is_blob() {
787                    let column_info = column_infos.peek();
788                    if column_info.page_infos.iter().any(|page| {
789                        matches!(
790                            page.encoding,
791                            PageEncoding::Structural(pb21::PageLayout {
792                                layout: Some(pb21::page_layout::Layout::BlobLayout(_))
793                            })
794                        )
795                    }) {
796                        let column_info = column_infos.expect_next()?;
797                        let scheduler = Box::new(StructuralPrimitiveFieldScheduler::try_new(
798                            column_info.as_ref(),
799                            self.decompressor_strategy.as_ref(),
800                            self.cache_repetition_index,
801                            field,
802                        )?);
803                        column_infos.next_top_level();
804                        return Ok(scheduler);
805                    }
806                }
807
808                let mut child_schedulers = Vec::with_capacity(field.children.len());
809                for field in field.children.iter() {
810                    let field_scheduler =
811                        self.create_structural_field_scheduler(field, column_infos)?;
812                    child_schedulers.push(field_scheduler);
813                }
814
815                let fields = fields.clone();
816                Ok(
817                    Box::new(StructuralStructScheduler::new(child_schedulers, fields))
818                        as Box<dyn StructuralFieldScheduler>,
819                )
820            }
821            DataType::List(_) | DataType::LargeList(_) => {
822                let child = field.children.first().expect_ok()?;
823                let child_scheduler =
824                    self.create_structural_field_scheduler(child, column_infos)?;
825                Ok(Box::new(StructuralListScheduler::new(child_scheduler))
826                    as Box<dyn StructuralFieldScheduler>)
827            }
828            DataType::FixedSizeList(inner, dimension)
829                if matches!(inner.data_type(), DataType::Struct(_)) =>
830            {
831                let child = field.children.first().expect_ok()?;
832                let child_scheduler =
833                    self.create_structural_field_scheduler(child, column_infos)?;
834                Ok(Box::new(StructuralFixedSizeListScheduler::new(
835                    child_scheduler,
836                    *dimension,
837                )) as Box<dyn StructuralFieldScheduler>)
838            }
839            DataType::Map(_, keys_sorted) => {
840                // TODO: We only support keys_sorted=false for now,
841                //  because converting a rust arrow map field to the python arrow field will
842                //  lose the keys_sorted property.
843                if *keys_sorted {
844                    return Err(Error::not_supported_source(format!("Map data type is not supported with keys_sorted=true now, current value is {}", *keys_sorted).into()));
845                }
846                let entries_child = field.children.first().expect_ok()?;
847                let child_scheduler =
848                    self.create_structural_field_scheduler(entries_child, column_infos)?;
849                Ok(Box::new(StructuralMapScheduler::new(child_scheduler))
850                    as Box<dyn StructuralFieldScheduler>)
851            }
852            _ => todo!("create_structural_field_scheduler for {}", data_type),
853        }
854    }
855
856    fn create_legacy_field_scheduler(
857        &self,
858        field: &Field,
859        column_infos: &mut ColumnInfoIter,
860        buffers: FileBuffers,
861    ) -> Result<Box<dyn crate::previous::decoder::FieldScheduler>> {
862        let data_type = field.data_type();
863        validate_fixed_size_list_dimensions(&field.name, &data_type)?;
864        if Self::is_primitive_legacy(&data_type) {
865            let column_info = column_infos.expect_next()?;
866            let scheduler = self.create_primitive_scheduler(field, column_info, buffers)?;
867            return Ok(scheduler);
868        } else if data_type.is_binary_like() {
869            let column_info = column_infos.expect_next()?.clone();
870            // Column is blob and user is asking for binary data
871            if let Some(blob_col) = Self::unwrap_blob(column_info.as_ref()) {
872                let desc_scheduler =
873                    self.create_primitive_scheduler(&BLOB_DESC_LANCE_FIELD, &blob_col, buffers)?;
874                let blob_scheduler = Box::new(BlobFieldScheduler::new(desc_scheduler.into()));
875                return Ok(blob_scheduler);
876            }
877            if let Some(page_info) = column_info.page_infos.first() {
878                if matches!(
879                    page_info.encoding.as_legacy(),
880                    pb::ArrayEncoding {
881                        array_encoding: Some(pb::array_encoding::ArrayEncoding::List(..))
882                    }
883                ) {
884                    let list_type = if matches!(data_type, DataType::Utf8 | DataType::Binary) {
885                        DataType::List(Arc::new(ArrowField::new("item", DataType::UInt8, false)))
886                    } else {
887                        DataType::LargeList(Arc::new(ArrowField::new(
888                            "item",
889                            DataType::UInt8,
890                            false,
891                        )))
892                    };
893                    let list_field = Field::try_from(ArrowField::new(
894                        field.name.clone(),
895                        list_type,
896                        field.nullable,
897                    ))
898                    .unwrap();
899                    let list_scheduler = self.create_list_scheduler(
900                        &list_field,
901                        column_infos,
902                        buffers,
903                        &column_info,
904                    )?;
905                    let binary_scheduler = Box::new(BinaryFieldScheduler::new(
906                        list_scheduler.into(),
907                        field.data_type(),
908                    ));
909                    return Ok(binary_scheduler);
910                } else {
911                    let scheduler =
912                        self.create_primitive_scheduler(field, &column_info, buffers)?;
913                    return Ok(scheduler);
914                }
915            } else {
916                return self.create_primitive_scheduler(field, &column_info, buffers);
917            }
918        }
919        match &data_type {
920            DataType::FixedSizeList(inner, _dimension) => {
921                // A fixed size list column could either be a physical or a logical decoder
922                // depending on the child data type.
923                if Self::is_primitive_legacy(inner.data_type()) {
924                    let primitive_col = column_infos.expect_next()?;
925                    let scheduler =
926                        self.create_primitive_scheduler(field, primitive_col, buffers)?;
927                    Ok(scheduler)
928                } else {
929                    todo!()
930                }
931            }
932            DataType::Dictionary(_key_type, value_type) => {
933                if Self::is_primitive_legacy(value_type) || value_type.is_binary_like() {
934                    let primitive_col = column_infos.expect_next()?;
935                    let scheduler =
936                        self.create_primitive_scheduler(field, primitive_col, buffers)?;
937                    Ok(scheduler)
938                } else {
939                    Err(Error::not_supported_source(
940                        format!(
941                            "No way to decode into a dictionary field of type {}",
942                            value_type
943                        )
944                        .into(),
945                    ))
946                }
947            }
948            DataType::List(_) | DataType::LargeList(_) => {
949                let offsets_column = column_infos.expect_next()?.clone();
950                column_infos.next_top_level();
951                self.create_list_scheduler(field, column_infos, buffers, &offsets_column)
952            }
953            DataType::Struct(fields) => {
954                let column_info = column_infos.expect_next()?;
955
956                // Column is blob and user is asking for descriptions
957                if let Some(blob_col) = Self::unwrap_blob(column_info.as_ref()) {
958                    // Can use primitive scheduler here since descriptions are always packed struct
959                    return self.create_primitive_scheduler(field, &blob_col, buffers);
960                }
961
962                if Self::check_packed_struct(column_info) {
963                    // use packed struct encoding
964                    self.create_primitive_scheduler(field, column_info, buffers)
965                } else {
966                    // use default struct encoding
967                    Self::check_simple_struct(column_info, &field.name).unwrap();
968                    let num_rows = column_info
969                        .page_infos
970                        .iter()
971                        .map(|page| page.num_rows)
972                        .sum();
973                    let mut child_schedulers = Vec::with_capacity(field.children.len());
974                    for field in &field.children {
975                        column_infos.next_top_level();
976                        let field_scheduler =
977                            self.create_legacy_field_scheduler(field, column_infos, buffers)?;
978                        child_schedulers.push(Arc::from(field_scheduler));
979                    }
980
981                    let fields = fields.clone();
982                    Ok(Box::new(SimpleStructScheduler::new(
983                        child_schedulers,
984                        fields,
985                        num_rows,
986                    )))
987                }
988            }
989            // TODO: Still need support for RLE
990            _ => todo!(),
991        }
992    }
993}
994
995/// Create's a dummy ColumnInfo for the root column
996fn root_column(num_rows: u64) -> ColumnInfo {
997    let num_root_pages = num_rows.div_ceil(u32::MAX as u64);
998    let final_page_num_rows = num_rows % (u32::MAX as u64);
999    let root_pages = (0..num_root_pages)
1000        .map(|i| PageInfo {
1001            num_rows: if i == num_root_pages - 1 {
1002                final_page_num_rows
1003            } else {
1004                u64::MAX
1005            },
1006            encoding: PageEncoding::Legacy(pb::ArrayEncoding {
1007                array_encoding: Some(pb::array_encoding::ArrayEncoding::Struct(
1008                    pb::SimpleStruct {},
1009                )),
1010            }),
1011            priority: 0, // not used in legacy scheduler
1012            buffer_offsets_and_sizes: Arc::new([]),
1013        })
1014        .collect::<Vec<_>>();
1015    ColumnInfo {
1016        buffer_offsets_and_sizes: Arc::new([]),
1017        encoding: pb::ColumnEncoding {
1018            column_encoding: Some(pb::column_encoding::ColumnEncoding::Values(())),
1019        },
1020        index: u32::MAX,
1021        page_infos: Arc::from(root_pages),
1022    }
1023}
1024
1025pub enum RootDecoder {
1026    Structural(StructuralStructDecoder),
1027    Legacy(SimpleStructDecoder),
1028}
1029
1030impl RootDecoder {
1031    pub fn into_structural(self) -> StructuralStructDecoder {
1032        match self {
1033            Self::Structural(decoder) => decoder,
1034            Self::Legacy(_) => panic!("Expected a structural decoder"),
1035        }
1036    }
1037
1038    pub fn into_legacy(self) -> SimpleStructDecoder {
1039        match self {
1040            Self::Legacy(decoder) => decoder,
1041            Self::Structural(_) => panic!("Expected a legacy decoder"),
1042        }
1043    }
1044}
1045
1046impl DecodeBatchScheduler {
1047    /// Creates a new decode scheduler with the expected schema and the column
1048    /// metadata of the file.
1049    #[allow(clippy::too_many_arguments)]
1050    pub async fn try_new<'a>(
1051        schema: &'a Schema,
1052        column_indices: &[u32],
1053        column_infos: &[Arc<ColumnInfo>],
1054        file_buffer_positions_and_sizes: &'a Vec<(u64, u64)>,
1055        num_rows: u64,
1056        _decoder_plugins: Arc<DecoderPlugins>,
1057        io: Arc<dyn EncodingsIo>,
1058        cache: Arc<LanceCache>,
1059        filter: &FilterExpression,
1060        decoder_config: &DecoderConfig,
1061    ) -> Result<Self> {
1062        assert!(num_rows > 0);
1063        let buffers = FileBuffers {
1064            positions_and_sizes: file_buffer_positions_and_sizes,
1065        };
1066        let arrow_schema = ArrowSchema::from(schema);
1067        let root_fields = arrow_schema.fields().clone();
1068        let root_type = DataType::Struct(root_fields.clone());
1069        let mut root_field = Field::try_from(&ArrowField::new("root", root_type, false))?;
1070        // root_field.children and schema.fields should be identical at this point but the latter
1071        // has field ids and the former does not.  This line restores that.
1072        // TODO:  Is there another way to create the root field without forcing a trip through arrow?
1073        root_field.children.clone_from(&schema.fields);
1074        root_field
1075            .metadata
1076            .insert("__lance_decoder_root".to_string(), "true".to_string());
1077
1078        if column_infos.is_empty() || column_infos[0].is_structural() {
1079            let mut column_iter = ColumnInfoIter::new(column_infos.to_vec(), column_indices);
1080
1081            let strategy = CoreFieldDecoderStrategy::from_decoder_config(decoder_config);
1082            let mut root_scheduler =
1083                strategy.create_structural_field_scheduler(&root_field, &mut column_iter)?;
1084
1085            let context = SchedulerContext::new(io, cache.clone());
1086            root_scheduler.initialize(filter, &context).await?;
1087
1088            Ok(Self {
1089                root_scheduler: RootScheduler::Structural(root_scheduler),
1090                root_fields,
1091                cache,
1092            })
1093        } else {
1094            // The old encoding style expected a header column for structs and so we
1095            // need a header column for the top-level struct
1096            let mut columns = Vec::with_capacity(column_infos.len() + 1);
1097            columns.push(Arc::new(root_column(num_rows)));
1098            columns.extend(column_infos.iter().cloned());
1099
1100            let adjusted_column_indices = [0_u32]
1101                .into_iter()
1102                .chain(column_indices.iter().map(|i| i.saturating_add(1)))
1103                .collect::<Vec<_>>();
1104            let mut column_iter = ColumnInfoIter::new(columns, &adjusted_column_indices);
1105            let strategy = CoreFieldDecoderStrategy::from_decoder_config(decoder_config);
1106            let root_scheduler =
1107                strategy.create_legacy_field_scheduler(&root_field, &mut column_iter, buffers)?;
1108
1109            let context = SchedulerContext::new(io, cache.clone());
1110            root_scheduler.initialize(filter, &context).await?;
1111
1112            Ok(Self {
1113                root_scheduler: RootScheduler::Legacy(root_scheduler.into()),
1114                root_fields,
1115                cache,
1116            })
1117        }
1118    }
1119
1120    #[deprecated(since = "0.29.1", note = "This is for legacy 2.0 paths")]
1121    pub fn from_scheduler(
1122        root_scheduler: Arc<dyn crate::previous::decoder::FieldScheduler>,
1123        root_fields: Fields,
1124        cache: Arc<LanceCache>,
1125    ) -> Self {
1126        Self {
1127            root_scheduler: RootScheduler::Legacy(root_scheduler),
1128            root_fields,
1129            cache,
1130        }
1131    }
1132
1133    fn do_schedule_ranges_structural(
1134        &mut self,
1135        ranges: &[Range<u64>],
1136        filter: &FilterExpression,
1137        io: Arc<dyn EncodingsIo>,
1138        mut schedule_action: impl FnMut(Result<DecoderMessage>) -> bool,
1139    ) {
1140        let root_scheduler = self.root_scheduler.as_structural();
1141        let mut context = SchedulerContext::new(io, self.cache.clone());
1142        let maybe_root_job = root_scheduler.schedule_ranges(ranges, filter);
1143        if let Err(schedule_ranges_err) = maybe_root_job {
1144            schedule_action(Err(schedule_ranges_err));
1145            return;
1146        }
1147        let mut root_job = maybe_root_job.unwrap();
1148        let mut num_rows_scheduled = 0;
1149        loop {
1150            let maybe_next_scan_lines = root_job.schedule_next(&mut context);
1151            if let Err(err) = maybe_next_scan_lines {
1152                schedule_action(Err(err));
1153                return;
1154            }
1155            let next_scan_lines = maybe_next_scan_lines.unwrap();
1156            if next_scan_lines.is_empty() {
1157                return;
1158            }
1159            for next_scan_line in next_scan_lines {
1160                trace!(
1161                    "Scheduled scan line of {} rows and {} decoders",
1162                    next_scan_line.rows_scheduled,
1163                    next_scan_line.decoders.len()
1164                );
1165                num_rows_scheduled += next_scan_line.rows_scheduled;
1166                if !schedule_action(Ok(DecoderMessage {
1167                    scheduled_so_far: num_rows_scheduled,
1168                    decoders: next_scan_line.decoders,
1169                })) {
1170                    // Decoder has disconnected
1171                    return;
1172                }
1173            }
1174        }
1175    }
1176
1177    fn do_schedule_ranges_legacy(
1178        &mut self,
1179        ranges: &[Range<u64>],
1180        filter: &FilterExpression,
1181        io: Arc<dyn EncodingsIo>,
1182        mut schedule_action: impl FnMut(Result<DecoderMessage>) -> bool,
1183        // If specified, this will be used as the top_level_row for all scheduling
1184        // tasks.  This is used by list scheduling to ensure all items scheduling
1185        // tasks are scheduled at the same top level row.
1186        priority: Option<Box<dyn PriorityRange>>,
1187    ) {
1188        let root_scheduler = self.root_scheduler.as_legacy();
1189        let rows_requested = ranges.iter().map(|r| r.end - r.start).sum::<u64>();
1190        trace!(
1191            "Scheduling {} ranges across {}..{} ({} rows){}",
1192            ranges.len(),
1193            ranges.first().unwrap().start,
1194            ranges.last().unwrap().end,
1195            rows_requested,
1196            priority
1197                .as_ref()
1198                .map(|p| format!(" (priority={:?})", p))
1199                .unwrap_or_default()
1200        );
1201
1202        let mut context = SchedulerContext::new(io, self.cache.clone());
1203        let maybe_root_job = root_scheduler.schedule_ranges(ranges, filter);
1204        if let Err(schedule_ranges_err) = maybe_root_job {
1205            schedule_action(Err(schedule_ranges_err));
1206            return;
1207        }
1208        let mut root_job = maybe_root_job.unwrap();
1209        let mut num_rows_scheduled = 0;
1210        let mut rows_to_schedule = root_job.num_rows();
1211        let mut priority = priority.unwrap_or(Box::new(SimplePriorityRange::new(0)));
1212        trace!("Scheduled ranges refined to {} rows", rows_to_schedule);
1213        while rows_to_schedule > 0 {
1214            let maybe_next_scan_line = root_job.schedule_next(&mut context, priority.as_ref());
1215            if let Err(schedule_next_err) = maybe_next_scan_line {
1216                schedule_action(Err(schedule_next_err));
1217                return;
1218            }
1219            let next_scan_line = maybe_next_scan_line.unwrap();
1220            priority.advance(next_scan_line.rows_scheduled);
1221            num_rows_scheduled += next_scan_line.rows_scheduled;
1222            rows_to_schedule -= next_scan_line.rows_scheduled;
1223            trace!(
1224                "Scheduled scan line of {} rows and {} decoders",
1225                next_scan_line.rows_scheduled,
1226                next_scan_line.decoders.len()
1227            );
1228            if !schedule_action(Ok(DecoderMessage {
1229                scheduled_so_far: num_rows_scheduled,
1230                decoders: next_scan_line.decoders,
1231            })) {
1232                // Decoder has disconnected
1233                return;
1234            }
1235
1236            trace!("Finished scheduling {} ranges", ranges.len());
1237        }
1238    }
1239
1240    fn do_schedule_ranges(
1241        &mut self,
1242        ranges: &[Range<u64>],
1243        filter: &FilterExpression,
1244        io: Arc<dyn EncodingsIo>,
1245        schedule_action: impl FnMut(Result<DecoderMessage>) -> bool,
1246        // If specified, this will be used as the top_level_row for all scheduling
1247        // tasks.  This is used by list scheduling to ensure all items scheduling
1248        // tasks are scheduled at the same top level row.
1249        priority: Option<Box<dyn PriorityRange>>,
1250    ) {
1251        match &self.root_scheduler {
1252            RootScheduler::Legacy(_) => {
1253                self.do_schedule_ranges_legacy(ranges, filter, io, schedule_action, priority)
1254            }
1255            RootScheduler::Structural(_) => {
1256                self.do_schedule_ranges_structural(ranges, filter, io, schedule_action)
1257            }
1258        }
1259    }
1260
1261    // This method is similar to schedule_ranges but instead of
1262    // sending the decoders to a channel it collects them all into a vector
1263    pub fn schedule_ranges_to_vec(
1264        &mut self,
1265        ranges: &[Range<u64>],
1266        filter: &FilterExpression,
1267        io: Arc<dyn EncodingsIo>,
1268        priority: Option<Box<dyn PriorityRange>>,
1269    ) -> Result<Vec<DecoderMessage>> {
1270        let mut decode_messages = Vec::new();
1271        self.do_schedule_ranges(
1272            ranges,
1273            filter,
1274            io,
1275            |msg| {
1276                decode_messages.push(msg);
1277                true
1278            },
1279            priority,
1280        );
1281        decode_messages.into_iter().collect::<Result<Vec<_>>>()
1282    }
1283
1284    /// Schedules the load of multiple ranges of rows
1285    ///
1286    /// Ranges must be non-overlapping and in sorted order
1287    ///
1288    /// # Arguments
1289    ///
1290    /// * `ranges` - The ranges of rows to load
1291    /// * `sink` - A channel to send the decode tasks
1292    /// * `scheduler` An I/O scheduler to issue I/O requests
1293    #[instrument(level = "debug", skip_all)]
1294    pub fn schedule_ranges(
1295        &mut self,
1296        ranges: &[Range<u64>],
1297        filter: &FilterExpression,
1298        sink: mpsc::UnboundedSender<Result<DecoderMessage>>,
1299        scheduler: Arc<dyn EncodingsIo>,
1300    ) {
1301        self.do_schedule_ranges(
1302            ranges,
1303            filter,
1304            scheduler,
1305            |msg| {
1306                match sink.send(msg) {
1307                    Ok(_) => true,
1308                    Err(SendError { .. }) => {
1309                        // The receiver has gone away.  We can't do anything about it
1310                        // so just ignore the error.
1311                        debug!(
1312                        "schedule_ranges aborting early since decoder appears to have been dropped"
1313                    );
1314                        false
1315                    }
1316                }
1317            },
1318            None,
1319        )
1320    }
1321
1322    /// Schedules the load of a range of rows
1323    ///
1324    /// # Arguments
1325    ///
1326    /// * `range` - The range of rows to load
1327    /// * `sink` - A channel to send the decode tasks
1328    /// * `scheduler` An I/O scheduler to issue I/O requests
1329    #[instrument(level = "debug", skip_all)]
1330    pub fn schedule_range(
1331        &mut self,
1332        range: Range<u64>,
1333        filter: &FilterExpression,
1334        sink: mpsc::UnboundedSender<Result<DecoderMessage>>,
1335        scheduler: Arc<dyn EncodingsIo>,
1336    ) {
1337        self.schedule_ranges(&[range], filter, sink, scheduler)
1338    }
1339
1340    /// Schedules the load of selected rows
1341    ///
1342    /// # Arguments
1343    ///
1344    /// * `indices` - The row indices to load (these must be in ascending order!)
1345    /// * `sink` - A channel to send the decode tasks
1346    /// * `scheduler` An I/O scheduler to issue I/O requests
1347    pub fn schedule_take(
1348        &mut self,
1349        indices: &[u64],
1350        filter: &FilterExpression,
1351        sink: mpsc::UnboundedSender<Result<DecoderMessage>>,
1352        scheduler: Arc<dyn EncodingsIo>,
1353    ) {
1354        debug_assert!(indices.windows(2).all(|w| w[0] < w[1]));
1355        if indices.is_empty() {
1356            return;
1357        }
1358        trace!("Scheduling take of {} rows", indices.len());
1359        let ranges = Self::indices_to_ranges(indices);
1360        self.schedule_ranges(&ranges, filter, sink, scheduler)
1361    }
1362
1363    // coalesce continuous indices if possible (the input indices must be sorted and non-empty)
1364    fn indices_to_ranges(indices: &[u64]) -> Vec<Range<u64>> {
1365        let mut ranges = Vec::new();
1366        let mut start = indices[0];
1367
1368        for window in indices.windows(2) {
1369            if window[1] != window[0] + 1 {
1370                ranges.push(start..window[0] + 1);
1371                start = window[1];
1372            }
1373        }
1374
1375        ranges.push(start..*indices.last().unwrap() + 1);
1376        ranges
1377    }
1378}
1379
1380pub struct ReadBatchTask {
1381    pub task: BoxFuture<'static, Result<RecordBatch>>,
1382    pub num_rows: u32,
1383}
1384
1385/// A stream that takes scheduled jobs and generates decode tasks from them.
1386pub struct BatchDecodeStream {
1387    context: DecoderContext,
1388    root_decoder: SimpleStructDecoder,
1389    rows_remaining: u64,
1390    rows_per_batch: u32,
1391    rows_scheduled: u64,
1392    rows_drained: u64,
1393    scheduler_exhausted: bool,
1394    emitted_batch_size_warning: Arc<Once>,
1395}
1396
1397impl BatchDecodeStream {
1398    /// Create a new instance of a batch decode stream
1399    ///
1400    /// # Arguments
1401    ///
1402    /// * `scheduled` - an incoming stream of decode tasks from a `DecodeBatchScheduler`
1403    /// * `schema` - the schema of the data to create
1404    /// * `rows_per_batch` the number of rows to create before making a batch
1405    /// * `num_rows` the total number of rows scheduled
1406    /// * `num_columns` the total number of columns in the file
1407    pub fn new(
1408        scheduled: mpsc::UnboundedReceiver<Result<DecoderMessage>>,
1409        rows_per_batch: u32,
1410        num_rows: u64,
1411        root_decoder: SimpleStructDecoder,
1412    ) -> Self {
1413        Self {
1414            context: DecoderContext::new(scheduled),
1415            root_decoder,
1416            rows_remaining: num_rows,
1417            rows_per_batch,
1418            rows_scheduled: 0,
1419            rows_drained: 0,
1420            scheduler_exhausted: false,
1421            emitted_batch_size_warning: Arc::new(Once::new()),
1422        }
1423    }
1424
1425    fn accept_decoder(&mut self, decoder: crate::previous::decoder::DecoderReady) -> Result<()> {
1426        if decoder.path.is_empty() {
1427            // The root decoder we can ignore
1428            Ok(())
1429        } else {
1430            self.root_decoder.accept_child(decoder)
1431        }
1432    }
1433
1434    #[instrument(level = "debug", skip_all)]
1435    async fn wait_for_scheduled(&mut self, scheduled_need: u64) -> Result<u64> {
1436        if self.scheduler_exhausted {
1437            return Ok(self.rows_scheduled);
1438        }
1439        while self.rows_scheduled < scheduled_need {
1440            let next_message = self.context.source.recv().await;
1441            match next_message {
1442                Some(scan_line) => {
1443                    let scan_line = scan_line?;
1444                    self.rows_scheduled = scan_line.scheduled_so_far;
1445                    for message in scan_line.decoders {
1446                        self.accept_decoder(message.into_legacy())?;
1447                    }
1448                }
1449                None => {
1450                    // Schedule ended before we got all the data we expected.  This probably
1451                    // means some kind of pushdown filter was applied and we didn't load as
1452                    // much data as we thought we would.
1453                    self.scheduler_exhausted = true;
1454                    return Ok(self.rows_scheduled);
1455                }
1456            }
1457        }
1458        Ok(scheduled_need)
1459    }
1460
1461    #[instrument(level = "debug", skip_all)]
1462    async fn next_batch_task(&mut self) -> Result<Option<NextDecodeTask>> {
1463        trace!(
1464            "Draining batch task (rows_remaining={} rows_drained={} rows_scheduled={})",
1465            self.rows_remaining, self.rows_drained, self.rows_scheduled,
1466        );
1467        if self.rows_remaining == 0 {
1468            return Ok(None);
1469        }
1470
1471        let mut to_take = self.rows_remaining.min(self.rows_per_batch as u64);
1472        self.rows_remaining -= to_take;
1473
1474        let scheduled_need = (self.rows_drained + to_take).saturating_sub(self.rows_scheduled);
1475        trace!(
1476            "scheduled_need = {} because rows_drained = {} and to_take = {} and rows_scheduled = {}",
1477            scheduled_need, self.rows_drained, to_take, self.rows_scheduled
1478        );
1479        if scheduled_need > 0 {
1480            let desired_scheduled = scheduled_need + self.rows_scheduled;
1481            trace!(
1482                "Draining from scheduler (desire at least {} scheduled rows)",
1483                desired_scheduled
1484            );
1485            let actually_scheduled = self.wait_for_scheduled(desired_scheduled).await?;
1486            if actually_scheduled < desired_scheduled {
1487                let under_scheduled = desired_scheduled - actually_scheduled;
1488                to_take -= under_scheduled;
1489            }
1490        }
1491
1492        if to_take == 0 {
1493            return Ok(None);
1494        }
1495
1496        // wait_for_loaded waits for *>* loaded_need (not >=) so we do a -1 here
1497        let loaded_need = self.rows_drained + to_take - 1;
1498        trace!(
1499            "Waiting for I/O (desire at least {} fully loaded rows)",
1500            loaded_need
1501        );
1502        self.root_decoder.wait_for_loaded(loaded_need).await?;
1503
1504        let next_task = self.root_decoder.drain(to_take)?;
1505        self.rows_drained += to_take;
1506        Ok(Some(next_task))
1507    }
1508
1509    pub fn into_stream(self) -> BoxStream<'static, ReadBatchTask> {
1510        let stream = futures::stream::unfold(self, |mut slf| async move {
1511            let next_task = match slf.next_batch_task().await {
1512                Ok(Some(next_task)) => next_task,
1513                Ok(None) => return None,
1514                Err(err) => {
1515                    slf.rows_remaining = 0;
1516                    return Some((
1517                        ReadBatchTask {
1518                            task: async move { Err(err) }.boxed(),
1519                            num_rows: 0,
1520                        },
1521                        slf,
1522                    ));
1523                }
1524            };
1525            let num_rows = next_task.num_rows;
1526            let emitted_batch_size_warning = slf.emitted_batch_size_warning.clone();
1527            let task = async move {
1528                // Real decode work happens inside into_batch, which can block the current
1529                // thread for a long time. By spawning it as a new task, we allow Tokio's
1530                // worker threads to keep making progress.
1531                let (batch, _data_size) =
1532                    tokio::spawn(async move { next_task.into_batch(emitted_batch_size_warning) })
1533                        .await
1534                        .map_err(|err| Error::wrapped(err.into()))??;
1535                Ok(batch)
1536            };
1537            // This should be true since batch size is u32
1538            debug_assert!(num_rows <= u32::MAX as u64);
1539            Some((
1540                ReadBatchTask {
1541                    task: task.boxed(),
1542                    num_rows: num_rows as u32,
1543                },
1544                slf,
1545            ))
1546        });
1547        stream.boxed()
1548    }
1549}
1550
1551// Utility types to smooth out the differences between the 2.0 and 2.1 decoders so that
1552// we can have a single implementation of the batch decode iterator
1553enum RootDecoderMessage {
1554    LoadedPage(LoadedPageShard),
1555    LegacyPage(crate::previous::decoder::DecoderReady),
1556}
1557trait RootDecoderType {
1558    fn accept_message(&mut self, message: RootDecoderMessage) -> Result<()>;
1559    fn drain_batch(&mut self, num_rows: u64) -> Result<NextDecodeTask>;
1560    fn wait(&mut self, loaded_need: u64, runtime: &tokio::runtime::Runtime) -> Result<()>;
1561}
1562impl RootDecoderType for StructuralStructDecoder {
1563    fn accept_message(&mut self, message: RootDecoderMessage) -> Result<()> {
1564        let RootDecoderMessage::LoadedPage(loaded_page) = message else {
1565            unreachable!()
1566        };
1567        self.accept_page(loaded_page)
1568    }
1569    fn drain_batch(&mut self, num_rows: u64) -> Result<NextDecodeTask> {
1570        self.drain_batch_task(num_rows)
1571    }
1572    fn wait(&mut self, _: u64, _: &tokio::runtime::Runtime) -> Result<()> {
1573        // Waiting happens elsewhere (not as part of the decoder)
1574        Ok(())
1575    }
1576}
1577impl RootDecoderType for SimpleStructDecoder {
1578    fn accept_message(&mut self, message: RootDecoderMessage) -> Result<()> {
1579        let RootDecoderMessage::LegacyPage(legacy_page) = message else {
1580            unreachable!()
1581        };
1582        self.accept_child(legacy_page)
1583    }
1584    fn drain_batch(&mut self, num_rows: u64) -> Result<NextDecodeTask> {
1585        self.drain(num_rows)
1586    }
1587    fn wait(&mut self, loaded_need: u64, runtime: &tokio::runtime::Runtime) -> Result<()> {
1588        runtime.block_on(self.wait_for_loaded(loaded_need))
1589    }
1590}
1591
1592/// A blocking batch decoder that performs synchronous decoding
1593struct BatchDecodeIterator<T: RootDecoderType> {
1594    messages: VecDeque<Result<DecoderMessage>>,
1595    root_decoder: T,
1596    rows_remaining: u64,
1597    rows_per_batch: u32,
1598    rows_scheduled: u64,
1599    rows_drained: u64,
1600    emitted_batch_size_warning: Arc<Once>,
1601    // Note: this is not the runtime on which I/O happens.
1602    // That's always in the scheduler.  This is just a runtime we use to
1603    // sleep the current thread if I/O is unready
1604    wait_for_io_runtime: tokio::runtime::Runtime,
1605    schema: Arc<ArrowSchema>,
1606}
1607
1608impl<T: RootDecoderType> BatchDecodeIterator<T> {
1609    /// Create a new instance of a batch decode iterator
1610    pub fn new(
1611        messages: VecDeque<Result<DecoderMessage>>,
1612        rows_per_batch: u32,
1613        num_rows: u64,
1614        root_decoder: T,
1615        schema: Arc<ArrowSchema>,
1616    ) -> Self {
1617        Self {
1618            messages,
1619            root_decoder,
1620            rows_remaining: num_rows,
1621            rows_per_batch,
1622            rows_scheduled: 0,
1623            rows_drained: 0,
1624            wait_for_io_runtime: tokio::runtime::Builder::new_current_thread()
1625                .build()
1626                .unwrap(),
1627            emitted_batch_size_warning: Arc::new(Once::new()),
1628            schema,
1629        }
1630    }
1631
1632    /// Wait for a single page of data to finish loading
1633    ///
1634    /// If the data is not available this will perform a *blocking* wait (put
1635    /// the current thread to sleep)
1636    fn wait_for_page(&self, unloaded_page: UnloadedPageShard) -> Result<LoadedPageShard> {
1637        match maybe_done(unloaded_page.0) {
1638            // Fast path, avoid all runtime shenanigans if the data is ready
1639            MaybeDone::Done(loaded_page) => loaded_page,
1640            // Slow path, we need to wait on I/O, enter the runtime
1641            MaybeDone::Future(fut) => self.wait_for_io_runtime.block_on(fut),
1642            MaybeDone::Gone => unreachable!(),
1643        }
1644    }
1645
1646    /// Waits for I/O until `scheduled_need` rows have been loaded
1647    ///
1648    /// Note that `scheduled_need` is cumulative.  E.g. this method
1649    /// should be called with 5, 10, 15 and not 5, 5, 5
1650    #[instrument(level = "debug", skip_all)]
1651    fn wait_for_io(&mut self, scheduled_need: u64, to_take: u64) -> Result<u64> {
1652        while self.rows_scheduled < scheduled_need && !self.messages.is_empty() {
1653            let message = self.messages.pop_front().unwrap()?;
1654            self.rows_scheduled = message.scheduled_so_far;
1655            for decoder_message in message.decoders {
1656                match decoder_message {
1657                    MessageType::UnloadedPage(unloaded_page) => {
1658                        let loaded_page = self.wait_for_page(unloaded_page)?;
1659                        self.root_decoder
1660                            .accept_message(RootDecoderMessage::LoadedPage(loaded_page))?;
1661                    }
1662                    MessageType::DecoderReady(decoder_ready) => {
1663                        // The root decoder we can ignore
1664                        if !decoder_ready.path.is_empty() {
1665                            self.root_decoder
1666                                .accept_message(RootDecoderMessage::LegacyPage(decoder_ready))?;
1667                        }
1668                    }
1669                }
1670            }
1671        }
1672
1673        let loaded_need = self.rows_drained + to_take.min(self.rows_per_batch as u64) - 1;
1674
1675        self.root_decoder
1676            .wait(loaded_need, &self.wait_for_io_runtime)?;
1677        Ok(self.rows_scheduled)
1678    }
1679
1680    #[instrument(level = "debug", skip_all)]
1681    fn next_batch_task(&mut self) -> Result<Option<RecordBatch>> {
1682        trace!(
1683            "Draining batch task (rows_remaining={} rows_drained={} rows_scheduled={})",
1684            self.rows_remaining, self.rows_drained, self.rows_scheduled,
1685        );
1686        if self.rows_remaining == 0 {
1687            return Ok(None);
1688        }
1689
1690        let mut to_take = self.rows_remaining.min(self.rows_per_batch as u64);
1691        self.rows_remaining -= to_take;
1692
1693        let scheduled_need = (self.rows_drained + to_take).saturating_sub(self.rows_scheduled);
1694        trace!(
1695            "scheduled_need = {} because rows_drained = {} and to_take = {} and rows_scheduled = {}",
1696            scheduled_need, self.rows_drained, to_take, self.rows_scheduled
1697        );
1698        if scheduled_need > 0 {
1699            let desired_scheduled = scheduled_need + self.rows_scheduled;
1700            trace!(
1701                "Draining from scheduler (desire at least {} scheduled rows)",
1702                desired_scheduled
1703            );
1704            let actually_scheduled = self.wait_for_io(desired_scheduled, to_take)?;
1705            if actually_scheduled < desired_scheduled {
1706                let under_scheduled = desired_scheduled - actually_scheduled;
1707                to_take -= under_scheduled;
1708            }
1709        }
1710
1711        if to_take == 0 {
1712            return Ok(None);
1713        }
1714
1715        let next_task = self.root_decoder.drain_batch(to_take)?;
1716
1717        self.rows_drained += to_take;
1718
1719        let (batch, _data_size) = next_task.into_batch(self.emitted_batch_size_warning.clone())?;
1720
1721        Ok(Some(batch))
1722    }
1723}
1724
1725impl<T: RootDecoderType> Iterator for BatchDecodeIterator<T> {
1726    type Item = ArrowResult<RecordBatch>;
1727
1728    fn next(&mut self) -> Option<Self::Item> {
1729        self.next_batch_task()
1730            .transpose()
1731            .map(|r| r.map_err(ArrowError::from))
1732    }
1733}
1734
1735impl<T: RootDecoderType> RecordBatchReader for BatchDecodeIterator<T> {
1736    fn schema(&self) -> Arc<ArrowSchema> {
1737        self.schema.clone()
1738    }
1739}
1740
1741/// Estimate the number of bytes per row for a given Arrow data type.
1742///
1743/// For fixed-width types this is exact. For variable-width types (strings,
1744/// binary, lists) a rough default is used. The estimate is used as a
1745/// starting point when `batch_size_bytes` is set; a post-decode feedback
1746/// loop corrects it after the first batch.
1747///
1748/// This estimate ignores validity bitmaps at the moment.  We can't infer
1749/// their presence simply from the data_type and their impact is probably
1750/// fairly negligible.
1751fn estimate_bytes_per_row(data_type: &DataType) -> f64 {
1752    if let Some(w) = data_type.byte_width_opt() {
1753        return w as f64;
1754    }
1755    match data_type {
1756        DataType::Boolean => 1.0 / 8.0,
1757        DataType::Utf8 | DataType::Binary | DataType::LargeUtf8 | DataType::LargeBinary => 64.0,
1758        DataType::Struct(fields) => fields
1759            .iter()
1760            .map(|f| estimate_bytes_per_row(f.data_type()))
1761            .sum(),
1762        DataType::List(child) | DataType::LargeList(child) => {
1763            5.0 * estimate_bytes_per_row(child.data_type())
1764        }
1765        DataType::FixedSizeList(child, dim) => {
1766            *dim as f64 * estimate_bytes_per_row(child.data_type())
1767        }
1768        DataType::Dictionary(_, value_type) => estimate_bytes_per_row(value_type),
1769        DataType::Map(entries, _) => 5.0 * estimate_bytes_per_row(entries.data_type()),
1770        _ => 64.0,
1771    }
1772}
1773
1774/// A stream that takes scheduled jobs and generates decode tasks from them.
1775pub struct StructuralBatchDecodeStream {
1776    context: DecoderContext,
1777    root_decoder: StructuralStructDecoder,
1778    rows_remaining: u64,
1779    rows_per_batch: u32,
1780    rows_scheduled: u64,
1781    rows_drained: u64,
1782    scheduler_exhausted: bool,
1783    emitted_batch_size_warning: Arc<Once>,
1784    // Decode scheduling policy selected at planning time.
1785    //
1786    // Performance tradeoff:
1787    // - true: spawn `into_batch` onto Tokio, which improves scan throughput by allowing
1788    //   more decode parallelism.
1789    // - false: run `into_batch` inline, which avoids Tokio scheduling overhead and is
1790    //   typically better for point lookups / small takes.
1791    spawn_batch_decode_tasks: bool,
1792    /// If set, target this many bytes per batch instead of `rows_per_batch` rows.
1793    batch_size_bytes: Option<u64>,
1794    /// Schema-based estimate of bytes per row, computed once at construction.
1795    /// Only meaningful when `batch_size_bytes` is `Some`.
1796    schema_bytes_per_row: f64,
1797    /// Post-decode feedback: actual bytes-per-row measured from the most
1798    /// recently decoded batch.  Zero means no feedback yet (use schema estimate).
1799    bytes_per_row_feedback: Arc<AtomicU64>,
1800}
1801
1802impl StructuralBatchDecodeStream {
1803    /// Create a new instance of a batch decode stream
1804    ///
1805    /// # Arguments
1806    ///
1807    /// * `scheduled` - an incoming stream of decode tasks from a `DecodeBatchScheduler`
1808    /// * `schema` - the schema of the data to create
1809    /// * `rows_per_batch` the number of rows to create before making a batch
1810    /// * `num_rows` the total number of rows scheduled
1811    /// * `num_columns` the total number of columns in the file
1812    pub fn new(
1813        scheduled: mpsc::UnboundedReceiver<Result<DecoderMessage>>,
1814        rows_per_batch: u32,
1815        num_rows: u64,
1816        root_decoder: StructuralStructDecoder,
1817        spawn_batch_decode_tasks: bool,
1818        batch_size_bytes: Option<u64>,
1819    ) -> Self {
1820        let schema_bytes_per_row = if batch_size_bytes.is_some() {
1821            estimate_bytes_per_row(root_decoder.data_type()).max(1.0)
1822        } else {
1823            0.0
1824        };
1825        Self {
1826            context: DecoderContext::new(scheduled),
1827            root_decoder,
1828            rows_remaining: num_rows,
1829            rows_per_batch,
1830            rows_scheduled: 0,
1831            rows_drained: 0,
1832            scheduler_exhausted: false,
1833            emitted_batch_size_warning: Arc::new(Once::new()),
1834            spawn_batch_decode_tasks,
1835            batch_size_bytes,
1836            schema_bytes_per_row,
1837            bytes_per_row_feedback: Arc::new(AtomicU64::new(0)),
1838        }
1839    }
1840
1841    #[instrument(level = "debug", skip_all)]
1842    async fn wait_for_scheduled(&mut self, scheduled_need: u64) -> Result<u64> {
1843        if self.scheduler_exhausted {
1844            return Ok(self.rows_scheduled);
1845        }
1846        while self.rows_scheduled < scheduled_need {
1847            let next_message = self.context.source.recv().await;
1848            match next_message {
1849                Some(scan_line) => {
1850                    let scan_line = scan_line?;
1851                    self.rows_scheduled = scan_line.scheduled_so_far;
1852                    for message in scan_line.decoders {
1853                        let unloaded_page = message.into_structural();
1854                        let loaded_page = unloaded_page.0.await?;
1855                        self.root_decoder.accept_page(loaded_page)?;
1856                    }
1857                }
1858                None => {
1859                    // Schedule ended before we got all the data we expected.  This probably
1860                    // means some kind of pushdown filter was applied and we didn't load as
1861                    // much data as we thought we would.
1862                    self.scheduler_exhausted = true;
1863                    return Ok(self.rows_scheduled);
1864                }
1865            }
1866        }
1867        Ok(scheduled_need)
1868    }
1869
1870    #[instrument(level = "debug", skip_all)]
1871    async fn next_batch_task(&mut self) -> Result<Option<NextDecodeTask>> {
1872        trace!(
1873            "Draining batch task (rows_remaining={} rows_drained={} rows_scheduled={})",
1874            self.rows_remaining, self.rows_drained, self.rows_scheduled,
1875        );
1876        if self.rows_remaining == 0 {
1877            return Ok(None);
1878        }
1879
1880        let mut to_take = if let Some(batch_size_bytes) = self.batch_size_bytes {
1881            let feedback = self.bytes_per_row_feedback.load(Ordering::Relaxed);
1882            let bpr = if feedback > 0 {
1883                feedback as f64
1884            } else {
1885                self.schema_bytes_per_row
1886            };
1887            let rows = (batch_size_bytes as f64 / bpr) as u64;
1888            self.rows_remaining.min(rows.max(1))
1889        } else {
1890            self.rows_remaining.min(self.rows_per_batch as u64)
1891        };
1892        self.rows_remaining -= to_take;
1893
1894        let scheduled_need = (self.rows_drained + to_take).saturating_sub(self.rows_scheduled);
1895        trace!(
1896            "scheduled_need = {} because rows_drained = {} and to_take = {} and rows_scheduled = {}",
1897            scheduled_need, self.rows_drained, to_take, self.rows_scheduled
1898        );
1899        if scheduled_need > 0 {
1900            let desired_scheduled = scheduled_need + self.rows_scheduled;
1901            trace!(
1902                "Draining from scheduler (desire at least {} scheduled rows)",
1903                desired_scheduled
1904            );
1905            let actually_scheduled = self.wait_for_scheduled(desired_scheduled).await?;
1906            if actually_scheduled < desired_scheduled {
1907                let under_scheduled = desired_scheduled - actually_scheduled;
1908                to_take -= under_scheduled;
1909            }
1910        }
1911
1912        if to_take == 0 {
1913            return Ok(None);
1914        }
1915
1916        let next_task = self.root_decoder.drain_batch_task(to_take)?;
1917        self.rows_drained += to_take;
1918        Ok(Some(next_task))
1919    }
1920
1921    pub fn into_stream(self) -> BoxStream<'static, ReadBatchTask> {
1922        let stream = futures::stream::unfold(self, |mut slf| async move {
1923            let next_task = match slf.next_batch_task().await {
1924                Ok(Some(next_task)) => next_task,
1925                Ok(None) => return None,
1926                Err(err) => {
1927                    slf.rows_remaining = 0;
1928                    return Some((
1929                        ReadBatchTask {
1930                            task: async move { Err(err) }.boxed(),
1931                            num_rows: 0,
1932                        },
1933                        slf,
1934                    ));
1935                }
1936            };
1937            let num_rows = next_task.num_rows;
1938            let emitted_batch_size_warning = slf.emitted_batch_size_warning.clone();
1939            let bytes_per_row_feedback = slf.bytes_per_row_feedback.clone();
1940            // Capture the per-stream policy once so every emitted batch task follows the
1941            // same throughput-vs-overhead choice made by the scheduler.
1942            let spawn_batch_decode_tasks = slf.spawn_batch_decode_tasks;
1943            let task = async move {
1944                let (batch, data_size) = if spawn_batch_decode_tasks {
1945                    tokio::spawn(async move { next_task.into_batch(emitted_batch_size_warning) })
1946                        .await
1947                        .map_err(|err| Error::wrapped(err.into()))??
1948                } else {
1949                    next_task.into_batch(emitted_batch_size_warning)?
1950                };
1951                let num_rows = batch.num_rows() as u64;
1952                if let Some(bpr) = data_size.checked_div(num_rows) {
1953                    let prev = bytes_per_row_feedback.load(Ordering::Relaxed);
1954                    let next = if prev == 0 || bpr >= prev {
1955                        // First batch or actual size is larger than estimate:
1956                        // adopt immediately to avoid OOM.
1957                        bpr
1958                    } else {
1959                        // Actual size is smaller: degrade gradually toward
1960                        // the true value to avoid over-correcting on a
1961                        // single anomalous batch.
1962                        (prev + bpr) / 2
1963                    };
1964                    bytes_per_row_feedback.store(next.max(1), Ordering::Relaxed);
1965                }
1966                Ok(batch)
1967            };
1968            // This should be true since batch size is u32
1969            debug_assert!(num_rows <= u32::MAX as u64);
1970            Some((
1971                ReadBatchTask {
1972                    task: task.boxed(),
1973                    num_rows: num_rows as u32,
1974                },
1975                slf,
1976            ))
1977        });
1978        stream.boxed()
1979    }
1980}
1981
1982#[derive(Debug)]
1983pub enum RequestedRows {
1984    Ranges(Vec<Range<u64>>),
1985    Indices(Vec<u64>),
1986}
1987
1988impl RequestedRows {
1989    pub fn num_rows(&self) -> u64 {
1990        match self {
1991            Self::Ranges(ranges) => ranges.iter().map(|r| r.end - r.start).sum(),
1992            Self::Indices(indices) => indices.len() as u64,
1993        }
1994    }
1995
1996    pub fn trim_empty_ranges(mut self) -> Self {
1997        if let Self::Ranges(ranges) = &mut self {
1998            ranges.retain(|r| !r.is_empty());
1999        }
2000        self
2001    }
2002}
2003
2004/// Configuration for decoder behavior
2005#[derive(Debug, Clone)]
2006pub struct DecoderConfig {
2007    /// Whether to cache repetition indices for better performance.
2008    ///
2009    /// This defaults to the `LANCE_READ_CACHE_REPETITION_INDEX` environment variable
2010    /// when present and is enabled by default. Set the env var to a non-truthy
2011    /// value (for example `0` or `false`) to disable it. The env var is read
2012    /// once per process.
2013    pub cache_repetition_index: bool,
2014    /// Whether to validate decoded data
2015    pub validate_on_decode: bool,
2016    /// Override the strategy used to dispatch the scheduling work in
2017    /// [`schedule_and_decode`].
2018    ///
2019    /// `schedule_and_decode` always awaits the scheduler's `initialize` (which
2020    /// performs metadata I/O) before returning.  This flag controls what
2021    /// happens with the subsequent (synchronous) work of pushing decoder
2022    /// messages into the channel that feeds the decode stream.
2023    ///
2024    /// * `None` - default behavior: the scheduling work runs inline (as part
2025    ///   of the `schedule_and_decode` await) when the request is small
2026    ///   (controlled by the `LANCE_INLINE_SCHEDULING_THRESHOLD` env var) and
2027    ///   is dispatched onto a spawned task otherwise.
2028    /// * `Some(true)` - always run scheduling inline.  The await of
2029    ///   `schedule_and_decode` does not return until every decoder message
2030    ///   has been queued.
2031    /// * `Some(false)` - always spawn a task for scheduling so that it can
2032    ///   overlap with consumption of the decode stream.
2033    pub inline_scheduling: Option<bool>,
2034}
2035
2036impl Default for DecoderConfig {
2037    fn default() -> Self {
2038        Self {
2039            cache_repetition_index: default_cache_repetition_index(),
2040            validate_on_decode: false,
2041            inline_scheduling: None,
2042        }
2043    }
2044}
2045
2046#[derive(Debug, Clone)]
2047pub struct SchedulerDecoderConfig {
2048    pub decoder_plugins: Arc<DecoderPlugins>,
2049    pub batch_size: u32,
2050    pub io: Arc<dyn EncodingsIo>,
2051    pub cache: Arc<LanceCache>,
2052    /// Decoder configuration
2053    pub decoder_config: DecoderConfig,
2054    /// If set, target this many bytes per batch instead of using `batch_size` rows.
2055    ///
2056    /// Only supported for v2.1+ (structural) files. For v2.0 files this
2057    /// option is ignored and a warning is logged.
2058    pub batch_size_bytes: Option<u64>,
2059}
2060
2061fn check_scheduler_on_drop(
2062    stream: BoxStream<'static, ReadBatchTask>,
2063    scheduler_handle: tokio::task::JoinHandle<()>,
2064) -> BoxStream<'static, ReadBatchTask> {
2065    // This is a bit weird but we create an "empty stream" that unwraps the scheduler handle (which
2066    // will panic if the scheduler panicked).  This let's us check if the scheduler panicked
2067    // when the stream finishes.
2068    let abort_handle = scheduler_handle.abort_handle();
2069    let mut scheduler_handle = Some(scheduler_handle);
2070    let check_scheduler = stream::unfold((), move |_| {
2071        let handle = scheduler_handle.take();
2072        async move {
2073            if let Some(handle) = handle {
2074                handle.await.unwrap();
2075            }
2076            None
2077        }
2078    });
2079    stream
2080        .chain(check_scheduler)
2081        .on_drop(move || {
2082            // Abort the scheduler task on early drop. The scheduler task holds
2083            // a reference to the I/O scheduler (via config.io) which keeps the
2084            // ScanScheduler alive. If the scheduler task is stuck waiting for
2085            // initialization I/O (which is blocked on backpressure that will
2086            // never drain because no one is consuming the stream), we need to
2087            // abort it so it releases its I/O reference and allows the
2088            // ScanScheduler to drop and cancel pending I/O.
2089            abort_handle.abort();
2090        })
2091        .boxed()
2092}
2093
2094#[allow(clippy::too_many_arguments)]
2095pub fn create_decode_stream(
2096    schema: &Schema,
2097    num_rows: u64,
2098    batch_size: u32,
2099    is_structural: bool,
2100    should_validate: bool,
2101    spawn_structural_batch_decode_tasks: bool,
2102    rx: mpsc::UnboundedReceiver<Result<DecoderMessage>>,
2103    batch_size_bytes: Option<u64>,
2104) -> Result<BoxStream<'static, ReadBatchTask>> {
2105    if is_structural {
2106        let arrow_schema = ArrowSchema::from(schema);
2107        let structural_decoder = StructuralStructDecoder::new(
2108            arrow_schema.fields,
2109            should_validate,
2110            /*is_root=*/ true,
2111        )?;
2112        Ok(StructuralBatchDecodeStream::new(
2113            rx,
2114            batch_size,
2115            num_rows,
2116            structural_decoder,
2117            spawn_structural_batch_decode_tasks,
2118            batch_size_bytes,
2119        )
2120        .into_stream())
2121    } else {
2122        if batch_size_bytes.is_some() {
2123            warn!("batch_size_bytes is not supported for v2.0 files and will be ignored");
2124        }
2125        let arrow_schema = ArrowSchema::from(schema);
2126        let root_fields = arrow_schema.fields;
2127
2128        let simple_struct_decoder = SimpleStructDecoder::new(root_fields, num_rows);
2129        Ok(BatchDecodeStream::new(rx, batch_size, num_rows, simple_struct_decoder).into_stream())
2130    }
2131}
2132
2133/// Creates a iterator that decodes a set of messages in a blocking fashion
2134///
2135/// See [`schedule_and_decode_blocking`] for more information.
2136pub fn create_decode_iterator(
2137    schema: &Schema,
2138    num_rows: u64,
2139    batch_size: u32,
2140    should_validate: bool,
2141    is_structural: bool,
2142    messages: VecDeque<Result<DecoderMessage>>,
2143) -> Result<Box<dyn RecordBatchReader + Send + 'static>> {
2144    let arrow_schema = Arc::new(ArrowSchema::from(schema));
2145    let root_fields = arrow_schema.fields.clone();
2146    if is_structural {
2147        let simple_struct_decoder =
2148            StructuralStructDecoder::new(root_fields, should_validate, /*is_root=*/ true)?;
2149        Ok(Box::new(BatchDecodeIterator::new(
2150            messages,
2151            batch_size,
2152            num_rows,
2153            simple_struct_decoder,
2154            arrow_schema,
2155        )))
2156    } else {
2157        let root_decoder = SimpleStructDecoder::new(root_fields, num_rows);
2158        Ok(Box::new(BatchDecodeIterator::new(
2159            messages,
2160            batch_size,
2161            num_rows,
2162            root_decoder,
2163            arrow_schema,
2164        )))
2165    }
2166}
2167
2168async fn create_scheduler_decoder(
2169    column_infos: Vec<Arc<ColumnInfo>>,
2170    requested_rows: RequestedRows,
2171    filter: FilterExpression,
2172    column_indices: Vec<u32>,
2173    target_schema: Arc<Schema>,
2174    config: SchedulerDecoderConfig,
2175) -> Result<BoxStream<'static, ReadBatchTask>> {
2176    let num_rows = requested_rows.num_rows();
2177
2178    let is_structural = column_infos[0].is_structural();
2179    let mode = std::env::var(ENV_LANCE_STRUCTURAL_BATCH_DECODE_SPAWN_MODE);
2180    let spawn_structural_batch_decode_tasks = match mode.ok().as_deref() {
2181        Some("always") => true,
2182        Some("never") => false,
2183        _ => matches!(requested_rows, RequestedRows::Ranges(_)),
2184    };
2185
2186    let (tx, rx) = mpsc::unbounded_channel();
2187
2188    let decode_stream = create_decode_stream(
2189        &target_schema,
2190        num_rows,
2191        config.batch_size,
2192        is_structural,
2193        config.decoder_config.validate_on_decode,
2194        spawn_structural_batch_decode_tasks,
2195        rx,
2196        config.batch_size_bytes,
2197    )?;
2198
2199    // The scheduler's `initialize` may perform I/O to load column metadata
2200    // unless that metadata is already in the cache.  This metadata loading
2201    // happens as part of this call and should be parallelized if reading
2202    // multiple files.
2203    let mut decode_scheduler = DecodeBatchScheduler::try_new(
2204        target_schema.as_ref(),
2205        &column_indices,
2206        &column_infos,
2207        &vec![],
2208        num_rows,
2209        config.decoder_plugins,
2210        config.io.clone(),
2211        config.cache,
2212        &filter,
2213        &config.decoder_config,
2214    )
2215    .await?;
2216
2217    // For small requests the scheduling cost is dwarfed by the overhead of
2218    // spawning a task, so we run scheduling inline (still as part of this
2219    // await) before returning.  The threshold is configurable via
2220    // `LANCE_INLINE_SCHEDULING_THRESHOLD`, and callers can force either
2221    // strategy via `DecoderConfig::inline_scheduling`.
2222    let inline_scheduling = config
2223        .decoder_config
2224        .inline_scheduling
2225        .unwrap_or_else(|| num_rows <= inline_scheduling_threshold());
2226
2227    if inline_scheduling {
2228        match requested_rows {
2229            RequestedRows::Ranges(ranges) => {
2230                decode_scheduler.schedule_ranges(&ranges, &filter, tx, config.io)
2231            }
2232            RequestedRows::Indices(indices) => {
2233                decode_scheduler.schedule_take(&indices, &filter, tx, config.io)
2234            }
2235        }
2236        Ok(decode_stream)
2237    } else {
2238        // Spawn the (still synchronous) scheduling work so that decoder
2239        // messages can stream into the channel while the consumer is
2240        // already pulling from the decode stream.
2241        let scheduling = async move {
2242            match requested_rows {
2243                RequestedRows::Ranges(ranges) => {
2244                    decode_scheduler.schedule_ranges(&ranges, &filter, tx, config.io)
2245                }
2246                RequestedRows::Indices(indices) => {
2247                    decode_scheduler.schedule_take(&indices, &filter, tx, config.io)
2248                }
2249            }
2250        };
2251        let scheduler_handle = tokio::task::spawn(scheduling);
2252        Ok(check_scheduler_on_drop(decode_stream, scheduler_handle))
2253    }
2254}
2255
2256/// Initializes the scheduler, schedules the requested rows, and returns a
2257/// stream of decode tasks for the resulting batches.
2258///
2259/// This is a convenience function that creates both the scheduler and the
2260/// decoder, which can be a little tricky to get right.
2261///
2262/// # Why is this async?
2263///
2264/// Constructing the scheduler runs `initialize` which will perform I/O
2265/// unless the data required is already in the file metadata cache.
2266///
2267/// When `DecoderConfig::inline_scheduling` resolves to `true`, the
2268/// subsequent (synchronous) scheduling work also runs before this function
2269/// returns, leaving a fully primed decode stream.
2270pub async fn schedule_and_decode(
2271    column_infos: Vec<Arc<ColumnInfo>>,
2272    requested_rows: RequestedRows,
2273    filter: FilterExpression,
2274    column_indices: Vec<u32>,
2275    target_schema: Arc<Schema>,
2276    config: SchedulerDecoderConfig,
2277) -> Result<BoxStream<'static, ReadBatchTask>> {
2278    if requested_rows.num_rows() == 0 {
2279        return Ok(stream::empty().boxed());
2280    }
2281
2282    // If the user requested any ranges that are empty, ignore them.  They are pointless and
2283    // trying to read them has caused bugs in the past.
2284    let requested_rows = requested_rows.trim_empty_ranges();
2285
2286    let io = config.io.clone();
2287
2288    let stream = create_scheduler_decoder(
2289        column_infos,
2290        requested_rows,
2291        filter,
2292        column_indices,
2293        target_schema,
2294        config,
2295    )
2296    .await?;
2297
2298    // Keep the io alive until the stream is dropped or finishes.  Otherwise the
2299    // I/O drops as soon as the scheduling is finished and the I/O loop terminates.
2300    Ok(stream.finally(move || drop(io)).boxed())
2301}
2302
2303pub static WAITER_RT: LazyLock<tokio::runtime::Runtime> = LazyLock::new(|| {
2304    tokio::runtime::Builder::new_current_thread()
2305        .build()
2306        .unwrap()
2307});
2308
2309/// Schedules and decodes the requested data in a blocking fashion
2310///
2311/// This function is a blocking version of [`schedule_and_decode`]. It schedules the requested data
2312/// and decodes it in the current thread.
2313///
2314/// This can be useful when the disk is fast (or the data is in memory) and the amount
2315/// of data is relatively small.  For example, when doing a take against NVMe or in-memory data.
2316///
2317/// This should NOT be used for full scans.  Even if the data is in memory this function will
2318/// not parallelize the decode and will be slower than the async version.  Full scans typically
2319/// make relatively few IOPs and so the asynchronous overhead is much smaller.
2320///
2321/// This method will first completely run the scheduling process.  Then it will run the
2322/// decode process.
2323pub fn schedule_and_decode_blocking(
2324    column_infos: Vec<Arc<ColumnInfo>>,
2325    requested_rows: RequestedRows,
2326    filter: FilterExpression,
2327    column_indices: Vec<u32>,
2328    target_schema: Arc<Schema>,
2329    config: SchedulerDecoderConfig,
2330) -> Result<Box<dyn RecordBatchReader + Send + 'static>> {
2331    if requested_rows.num_rows() == 0 {
2332        let arrow_schema = Arc::new(ArrowSchema::from(target_schema.as_ref()));
2333        return Ok(Box::new(RecordBatchIterator::new(vec![], arrow_schema)));
2334    }
2335
2336    let num_rows = requested_rows.num_rows();
2337    let is_structural = column_infos[0].is_structural();
2338
2339    let (tx, mut rx) = mpsc::unbounded_channel();
2340
2341    // Initialize the scheduler.  This is still "asynchronous" but we run it with a current-thread
2342    // runtime.
2343    let mut decode_scheduler = WAITER_RT.block_on(DecodeBatchScheduler::try_new(
2344        target_schema.as_ref(),
2345        &column_indices,
2346        &column_infos,
2347        &vec![],
2348        num_rows,
2349        config.decoder_plugins,
2350        config.io.clone(),
2351        config.cache,
2352        &filter,
2353        &config.decoder_config,
2354    ))?;
2355
2356    // Schedule the requested rows
2357    match requested_rows {
2358        RequestedRows::Ranges(ranges) => {
2359            decode_scheduler.schedule_ranges(&ranges, &filter, tx, config.io)
2360        }
2361        RequestedRows::Indices(indices) => {
2362            decode_scheduler.schedule_take(&indices, &filter, tx, config.io)
2363        }
2364    }
2365
2366    // Drain the scheduler queue into a vec of decode messages
2367    let mut messages = Vec::new();
2368    while rx
2369        .recv_many(&mut messages, usize::MAX)
2370        .now_or_never()
2371        .unwrap()
2372        != 0
2373    {}
2374
2375    // Create a decoder to decode the messages
2376    let decode_iterator = create_decode_iterator(
2377        &target_schema,
2378        num_rows,
2379        config.batch_size,
2380        config.decoder_config.validate_on_decode,
2381        is_structural,
2382        messages.into(),
2383    )?;
2384
2385    Ok(decode_iterator)
2386}
2387
2388/// A decoder for single-column encodings of primitive data (this includes fixed size
2389/// lists of primitive data)
2390///
2391/// Physical decoders are able to decode into existing buffers for zero-copy operation.
2392///
2393/// Instances should be stateless and `Send` / `Sync`.  This is because multiple decode
2394/// tasks could reference the same page.  For example, imagine a page covers rows 0-2000
2395/// and the decoder stream has a batch size of 1024.  The decoder will be needed by both
2396/// the decode task for batch 0 and the decode task for batch 1.
2397///
2398/// See [`crate::decoder`] for more information
2399pub trait PrimitivePageDecoder: Send + Sync {
2400    /// Decode data into buffers
2401    ///
2402    /// This may be a simple zero-copy from a disk buffer or could involve complex decoding
2403    /// such as decompressing from some compressed representation.
2404    ///
2405    /// Capacity is stored as a tuple of (num_bytes: u64, is_needed: bool).  The `is_needed`
2406    /// portion only needs to be updated if the encoding has some concept of an "optional"
2407    /// buffer.
2408    ///
2409    /// Encodings can have any number of input or output buffers.  For example, a dictionary
2410    /// decoding will convert two buffers (indices + dictionary) into a single buffer
2411    ///
2412    /// Binary decodings have two output buffers (one for values, one for offsets)
2413    ///
2414    /// Other decodings could even expand the # of output buffers.  For example, we could decode
2415    /// fixed size strings into variable length strings going from one input buffer to multiple output
2416    /// buffers.
2417    ///
2418    /// Each Arrow data type typically has a fixed structure of buffers and the encoding chain will
2419    /// generally end at one of these structures.  However, intermediate structures may exist which
2420    /// do not correspond to any Arrow type at all.  For example, a bitpacking encoding will deal
2421    /// with buffers that have bits-per-value that is not a multiple of 8.
2422    ///
2423    /// The `primitive_array_from_buffers` method has an expected buffer layout for each arrow
2424    /// type (order matters) and encodings that aim to decode into arrow types should respect
2425    /// this layout.
2426    /// # Arguments
2427    ///
2428    /// * `rows_to_skip` - how many rows to skip (within the page) before decoding
2429    /// * `num_rows` - how many rows to decode
2430    /// * `all_null` - A mutable bool, set to true if a decoder determines all values are null
2431    fn decode(&self, rows_to_skip: u64, num_rows: u64) -> Result<DataBlock>;
2432}
2433
2434/// A scheduler for single-column encodings of primitive data
2435///
2436/// The scheduler is responsible for calculating what I/O is needed for the requested rows
2437///
2438/// Instances should be stateless and `Send` and `Sync`.  This is because instances can
2439/// be shared in follow-up I/O tasks.
2440///
2441/// See [`crate::decoder`] for more information
2442pub trait PageScheduler: Send + Sync + std::fmt::Debug {
2443    /// Schedules a batch of I/O to load the data needed for the requested ranges
2444    ///
2445    /// Returns a future that will yield a decoder once the data has been loaded
2446    ///
2447    /// # Arguments
2448    ///
2449    /// * `range` - the range of row offsets (relative to start of page) requested
2450    ///   these must be ordered and must not overlap
2451    /// * `scheduler` - a scheduler to submit the I/O request to
2452    /// * `top_level_row` - the row offset of the top level field currently being
2453    ///   scheduled.  This can be used to assign priority to I/O requests
2454    fn schedule_ranges(
2455        &self,
2456        ranges: &[Range<u64>],
2457        scheduler: &Arc<dyn EncodingsIo>,
2458        top_level_row: u64,
2459    ) -> BoxFuture<'static, Result<Box<dyn PrimitivePageDecoder>>>;
2460}
2461
2462/// A trait to control the priority of I/O
2463pub trait PriorityRange: std::fmt::Debug + Send + Sync {
2464    fn advance(&mut self, num_rows: u64);
2465    fn current_priority(&self) -> u64;
2466    fn box_clone(&self) -> Box<dyn PriorityRange>;
2467}
2468
2469/// A simple priority scheme for top-level fields with no parent
2470/// repetition
2471#[derive(Debug)]
2472pub struct SimplePriorityRange {
2473    priority: u64,
2474}
2475
2476impl SimplePriorityRange {
2477    fn new(priority: u64) -> Self {
2478        Self { priority }
2479    }
2480}
2481
2482impl PriorityRange for SimplePriorityRange {
2483    fn advance(&mut self, num_rows: u64) {
2484        self.priority += num_rows;
2485    }
2486
2487    fn current_priority(&self) -> u64 {
2488        self.priority
2489    }
2490
2491    fn box_clone(&self) -> Box<dyn PriorityRange> {
2492        Box::new(Self {
2493            priority: self.priority,
2494        })
2495    }
2496}
2497
2498/// Determining the priority of a list request is tricky.  We want
2499/// the priority to be the top-level row.  So if we have a
2500/// `list<list<int>>` and each outer list has 10 rows and each inner
2501/// list has 5 rows then the priority of the 100th item is 1 because
2502/// it is the 5th item in the 10th item of the *second* row.
2503///
2504/// This structure allows us to keep track of this complicated priority
2505/// relationship.
2506///
2507/// There's a fair amount of bookkeeping involved here.
2508///
2509/// A better approach (using repetition levels) is coming in the future.
2510pub struct ListPriorityRange {
2511    base: Box<dyn PriorityRange>,
2512    offsets: Arc<[u64]>,
2513    cur_index_into_offsets: usize,
2514    cur_position: u64,
2515}
2516
2517impl ListPriorityRange {
2518    pub(crate) fn new(base: Box<dyn PriorityRange>, offsets: Arc<[u64]>) -> Self {
2519        Self {
2520            base,
2521            offsets,
2522            cur_index_into_offsets: 0,
2523            cur_position: 0,
2524        }
2525    }
2526}
2527
2528impl std::fmt::Debug for ListPriorityRange {
2529    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2530        f.debug_struct("ListPriorityRange")
2531            .field("base", &self.base)
2532            .field("offsets.len()", &self.offsets.len())
2533            .field("cur_index_into_offsets", &self.cur_index_into_offsets)
2534            .field("cur_position", &self.cur_position)
2535            .finish()
2536    }
2537}
2538
2539impl PriorityRange for ListPriorityRange {
2540    fn advance(&mut self, num_rows: u64) {
2541        // We've scheduled X items.  Now walk through the offsets to
2542        // determine how many rows we've scheduled.
2543        self.cur_position += num_rows;
2544        let mut idx_into_offsets = self.cur_index_into_offsets;
2545        while idx_into_offsets + 1 < self.offsets.len()
2546            && self.offsets[idx_into_offsets + 1] <= self.cur_position
2547        {
2548            idx_into_offsets += 1;
2549        }
2550        let base_rows_advanced = idx_into_offsets - self.cur_index_into_offsets;
2551        self.cur_index_into_offsets = idx_into_offsets;
2552        self.base.advance(base_rows_advanced as u64);
2553    }
2554
2555    fn current_priority(&self) -> u64 {
2556        self.base.current_priority()
2557    }
2558
2559    fn box_clone(&self) -> Box<dyn PriorityRange> {
2560        Box::new(Self {
2561            base: self.base.box_clone(),
2562            offsets: self.offsets.clone(),
2563            cur_index_into_offsets: self.cur_index_into_offsets,
2564            cur_position: self.cur_position,
2565        })
2566    }
2567}
2568
2569/// Contains the context for a scheduler
2570pub struct SchedulerContext {
2571    recv: Option<mpsc::UnboundedReceiver<DecoderMessage>>,
2572    io: Arc<dyn EncodingsIo>,
2573    cache: Arc<LanceCache>,
2574    name: String,
2575    path: Vec<u32>,
2576    path_names: Vec<String>,
2577}
2578
2579pub struct ScopedSchedulerContext<'a> {
2580    pub context: &'a mut SchedulerContext,
2581}
2582
2583impl<'a> ScopedSchedulerContext<'a> {
2584    pub fn pop(self) -> &'a mut SchedulerContext {
2585        self.context.pop();
2586        self.context
2587    }
2588}
2589
2590impl SchedulerContext {
2591    pub fn new(io: Arc<dyn EncodingsIo>, cache: Arc<LanceCache>) -> Self {
2592        Self {
2593            io,
2594            cache,
2595            recv: None,
2596            name: "".to_string(),
2597            path: Vec::new(),
2598            path_names: Vec::new(),
2599        }
2600    }
2601
2602    pub fn io(&self) -> &Arc<dyn EncodingsIo> {
2603        &self.io
2604    }
2605
2606    pub fn cache(&self) -> &Arc<LanceCache> {
2607        &self.cache
2608    }
2609
2610    pub fn push(&'_ mut self, name: &str, index: u32) -> ScopedSchedulerContext<'_> {
2611        self.path.push(index);
2612        self.path_names.push(name.to_string());
2613        ScopedSchedulerContext { context: self }
2614    }
2615
2616    pub fn pop(&mut self) {
2617        self.path.pop();
2618        self.path_names.pop();
2619    }
2620
2621    pub fn path_name(&self) -> String {
2622        let path = self.path_names.join("/");
2623        if self.recv.is_some() {
2624            format!("TEMP({}){}", self.name, path)
2625        } else {
2626            format!("ROOT{}", path)
2627        }
2628    }
2629
2630    pub fn current_path(&self) -> VecDeque<u32> {
2631        VecDeque::from_iter(self.path.iter().copied())
2632    }
2633
2634    #[deprecated(since = "0.29.1", note = "This is for legacy 2.0 paths")]
2635    pub fn locate_decoder(
2636        &mut self,
2637        decoder: Box<dyn crate::previous::decoder::LogicalPageDecoder>,
2638    ) -> crate::previous::decoder::DecoderReady {
2639        trace!(
2640            "Scheduling decoder of type {:?} for {:?}",
2641            decoder.data_type(),
2642            self.path,
2643        );
2644        crate::previous::decoder::DecoderReady {
2645            decoder,
2646            path: self.current_path(),
2647        }
2648    }
2649}
2650
2651pub struct UnloadedPageShard(pub BoxFuture<'static, Result<LoadedPageShard>>);
2652
2653impl std::fmt::Debug for UnloadedPageShard {
2654    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2655        f.debug_struct("UnloadedPage").finish()
2656    }
2657}
2658
2659#[derive(Debug)]
2660pub struct ScheduledScanLine {
2661    pub rows_scheduled: u64,
2662    pub decoders: Vec<MessageType>,
2663}
2664
2665pub trait StructuralSchedulingJob: std::fmt::Debug {
2666    /// Schedule the next batch of data
2667    ///
2668    /// Normally this equates to scheduling the next page of data into one task.  Very large pages
2669    /// might be split into multiple scan lines.  Each scan line has one or more rows.
2670    ///
2671    /// If a scheduler ends early it may return an empty vector.
2672    fn schedule_next(&mut self, context: &mut SchedulerContext) -> Result<Vec<ScheduledScanLine>>;
2673}
2674
2675/// A filter expression to apply to the data
2676///
2677/// The core decoders do not currently take advantage of filtering in
2678/// any way.  In order to maintain the abstraction we represent filters
2679/// as an arbitrary byte sequence.
2680///
2681/// We recommend that encodings use Substrait for filters.
2682pub struct FilterExpression(pub Bytes);
2683
2684impl FilterExpression {
2685    /// Create a filter expression that does not filter any data
2686    ///
2687    /// This is currently represented by an empty byte array.  Encoders
2688    /// that are "filter aware" should make sure they handle this case.
2689    pub fn no_filter() -> Self {
2690        Self(Bytes::new())
2691    }
2692
2693    /// Returns true if the filter is the same as the [`Self::no_filter`] filter
2694    pub fn is_noop(&self) -> bool {
2695        self.0.is_empty()
2696    }
2697}
2698
2699pub trait StructuralFieldScheduler: Send + std::fmt::Debug {
2700    fn initialize<'a>(
2701        &'a mut self,
2702        filter: &'a FilterExpression,
2703        context: &'a SchedulerContext,
2704    ) -> BoxFuture<'a, Result<()>>;
2705    fn schedule_ranges<'a>(
2706        &'a self,
2707        ranges: &[Range<u64>],
2708        filter: &FilterExpression,
2709    ) -> Result<Box<dyn StructuralSchedulingJob + 'a>>;
2710}
2711
2712/// A trait for tasks that decode data into an Arrow array
2713pub trait DecodeArrayTask: Send {
2714    /// Decodes the data into an Arrow array and its data size in bytes
2715    fn decode(self: Box<Self>) -> Result<(ArrayRef, u64)>;
2716}
2717
2718impl DecodeArrayTask for Box<dyn StructuralDecodeArrayTask> {
2719    fn decode(self: Box<Self>) -> Result<(ArrayRef, u64)> {
2720        StructuralDecodeArrayTask::decode(*self)
2721            .map(|decoded_array| (decoded_array.array, decoded_array.data_size))
2722    }
2723}
2724
2725/// A task to decode data into an Arrow record batch
2726///
2727/// It has a child `task` which decodes a struct array with no nulls.
2728/// This is then converted into a record batch.
2729pub struct NextDecodeTask {
2730    /// The decode task itself
2731    pub task: Box<dyn DecodeArrayTask>,
2732    /// The number of rows that will be created
2733    pub num_rows: u64,
2734}
2735
2736impl NextDecodeTask {
2737    // Run the task and produce a record batch
2738    //
2739    // If the batch is very large this function will log a warning message
2740    // suggesting the user try a smaller batch size.
2741    #[instrument(name = "task_to_batch", level = "debug", skip_all)]
2742    fn into_batch(self, emitted_batch_size_warning: Arc<Once>) -> Result<(RecordBatch, u64)> {
2743        let (struct_arr, data_size) = self
2744            .task
2745            .decode()
2746            .map_err(|e| Error::internal(format!("Error decoding batch: {}", e)))?;
2747        let batch = RecordBatch::from(struct_arr.as_struct());
2748        if data_size > BATCH_SIZE_BYTES_WARNING {
2749            emitted_batch_size_warning.call_once(|| {
2750                let size_mb = data_size / 1024 / 1024;
2751                debug!("Lance read in a single batch that contained more than {}MiB of data.  You may want to consider reducing the batch size.", size_mb);
2752            });
2753        }
2754        Ok((batch, data_size))
2755    }
2756}
2757
2758// An envelope to wrap both 2.0 style messages and 2.1 style messages so we can
2759// share some code paths between the two.  Decoders can safely unwrap into whatever
2760// style they expect since a file will be either all-2.0 or all-2.1
2761#[derive(Debug)]
2762pub enum MessageType {
2763    // The older v2.0 scheduler/decoder used a scheme where the message was the
2764    // decoder itself.  The messages were not sent in priority order and the decoder
2765    // had to wait for I/O, figuring out the correct priority.  This was a lot of
2766    // complexity.
2767    DecoderReady(crate::previous::decoder::DecoderReady),
2768    // Starting in 2.1 we use a simpler scheme where the scheduling happens in priority
2769    // order and the message is an unloaded decoder.  These can be awaited, in order, and
2770    // the decoder does not have to worry about waiting for I/O.
2771    UnloadedPage(UnloadedPageShard),
2772}
2773
2774impl MessageType {
2775    pub fn into_legacy(self) -> crate::previous::decoder::DecoderReady {
2776        match self {
2777            Self::DecoderReady(decoder) => decoder,
2778            Self::UnloadedPage(_) => {
2779                panic!("Expected DecoderReady but got UnloadedPage")
2780            }
2781        }
2782    }
2783
2784    pub fn into_structural(self) -> UnloadedPageShard {
2785        match self {
2786            Self::UnloadedPage(unloaded) => unloaded,
2787            Self::DecoderReady(_) => {
2788                panic!("Expected UnloadedPage but got DecoderReady")
2789            }
2790        }
2791    }
2792}
2793
2794pub struct DecoderMessage {
2795    pub scheduled_so_far: u64,
2796    pub decoders: Vec<MessageType>,
2797}
2798
2799pub struct DecoderContext {
2800    source: mpsc::UnboundedReceiver<Result<DecoderMessage>>,
2801}
2802
2803impl DecoderContext {
2804    pub fn new(source: mpsc::UnboundedReceiver<Result<DecoderMessage>>) -> Self {
2805        Self { source }
2806    }
2807}
2808
2809pub struct DecodedPage {
2810    pub data: DataBlock,
2811    pub repdef: RepDefUnraveler,
2812}
2813
2814pub trait DecodePageTask: Send + std::fmt::Debug {
2815    /// Decodes the data into an Arrow array
2816    fn decode(self: Box<Self>) -> Result<DecodedPage>;
2817}
2818
2819pub trait StructuralPageDecoder: std::fmt::Debug + Send {
2820    fn drain(&mut self, num_rows: u64) -> Result<Box<dyn DecodePageTask>>;
2821    fn num_rows(&self) -> u64;
2822}
2823
2824#[derive(Debug)]
2825pub struct LoadedPageShard {
2826    // The decoder that is ready to be decoded
2827    pub decoder: Box<dyn StructuralPageDecoder>,
2828    // The path to the decoder, the first value is the column index
2829    // following values, if present, are nested child indices
2830    //
2831    // For example, a path of [1, 1, 0] would mean to grab the second
2832    // column, then the second child, and then the first child.
2833    //
2834    // It could represent x in the following schema:
2835    //
2836    // score: float64
2837    // points: struct
2838    //   color: string
2839    //   location: struct
2840    //     x: float64
2841    //
2842    // Currently, only struct decoders have "children" although other
2843    // decoders may at some point as well.  List children are only
2844    // handled through indirect I/O at the moment and so they don't
2845    // need to be represented (yet)
2846    pub path: VecDeque<u32>,
2847}
2848
2849pub struct DecodedArray {
2850    pub array: ArrayRef,
2851    pub repdef: CompositeRepDefUnraveler,
2852    /// The number of bytes of data in this array (excluding Arrow overhead).
2853    pub data_size: u64,
2854}
2855
2856pub trait StructuralDecodeArrayTask: std::fmt::Debug + Send {
2857    fn decode(self: Box<Self>) -> Result<DecodedArray>;
2858}
2859
2860pub trait StructuralFieldDecoder: std::fmt::Debug + Send {
2861    /// Add a newly scheduled child decoder
2862    ///
2863    /// The default implementation does not expect children and returns
2864    /// an error.
2865    fn accept_page(&mut self, _child: LoadedPageShard) -> Result<()>;
2866    /// Creates a task to decode `num_rows` of data into an array
2867    fn drain(&mut self, num_rows: u64) -> Result<Box<dyn StructuralDecodeArrayTask>>;
2868    /// The data type of the decoded data
2869    fn data_type(&self) -> &DataType;
2870}
2871
2872#[derive(Debug, Default)]
2873pub struct DecoderPlugins {}
2874
2875/// Decodes a batch of data from an in-memory structure created by [`crate::encoder::encode_batch`]
2876pub async fn decode_batch(
2877    batch: &EncodedBatch,
2878    filter: &FilterExpression,
2879    decoder_plugins: Arc<DecoderPlugins>,
2880    should_validate: bool,
2881    version: LanceFileVersion,
2882    cache: Option<Arc<LanceCache>>,
2883) -> Result<RecordBatch> {
2884    // The io is synchronous so it shouldn't be possible for any async stuff to still be in progress
2885    // Still, if we just use now_or_never we hit misfires because some futures (channels) need to be
2886    // polled twice.
2887
2888    let io_scheduler = Arc::new(BufferScheduler::new(batch.data.clone())) as Arc<dyn EncodingsIo>;
2889    let cache = if let Some(cache) = cache {
2890        cache
2891    } else {
2892        Arc::new(lance_core::cache::LanceCache::with_capacity(
2893            128 * 1024 * 1024,
2894        ))
2895    };
2896    let mut decode_scheduler = DecodeBatchScheduler::try_new(
2897        batch.schema.as_ref(),
2898        &batch.top_level_columns,
2899        &batch.page_table,
2900        &vec![],
2901        batch.num_rows,
2902        decoder_plugins,
2903        io_scheduler.clone(),
2904        cache,
2905        filter,
2906        &DecoderConfig::default(),
2907    )
2908    .await?;
2909    let (tx, rx) = unbounded_channel();
2910    decode_scheduler.schedule_range(0..batch.num_rows, filter, tx, io_scheduler);
2911    let is_structural = version >= LanceFileVersion::V2_1;
2912    let mode = std::env::var(ENV_LANCE_STRUCTURAL_BATCH_DECODE_SPAWN_MODE);
2913    let spawn_structural_batch_decode_tasks = !matches!(mode.ok().as_deref(), Some("never"));
2914    let mut decode_stream = create_decode_stream(
2915        &batch.schema,
2916        batch.num_rows,
2917        batch.num_rows as u32,
2918        is_structural,
2919        should_validate,
2920        spawn_structural_batch_decode_tasks,
2921        rx,
2922        None,
2923    )?;
2924    decode_stream.next().await.unwrap().task.await
2925}
2926
2927#[cfg(test)]
2928// test coalesce indices to ranges
2929mod tests {
2930    use super::*;
2931    use crate::previous::decoder::{DecoderReady, LogicalPageDecoder};
2932    use std::collections::VecDeque;
2933
2934    #[derive(Debug)]
2935    struct FailingPageDecoder {
2936        page_data_type: DataType,
2937        total_rows: u64,
2938        load_error_message: &'static str,
2939    }
2940
2941    impl FailingPageDecoder {
2942        fn new(
2943            page_data_type: DataType,
2944            total_rows: u64,
2945            load_error_message: &'static str,
2946        ) -> Self {
2947            Self {
2948                page_data_type,
2949                total_rows,
2950                load_error_message,
2951            }
2952        }
2953    }
2954
2955    impl LogicalPageDecoder for FailingPageDecoder {
2956        fn wait_for_loaded(&'_ mut self, _rows_needed: u64) -> BoxFuture<'_, Result<()>> {
2957            let load_error_message = self.load_error_message;
2958            async move { Err(Error::io(load_error_message)) }.boxed()
2959        }
2960
2961        fn rows_loaded(&self) -> u64 {
2962            0
2963        }
2964
2965        fn num_rows(&self) -> u64 {
2966            self.total_rows
2967        }
2968
2969        fn rows_drained(&self) -> u64 {
2970            0
2971        }
2972
2973        fn drain(&mut self, requested_rows: u64) -> Result<NextDecodeTask> {
2974            Err(Error::internal(format!(
2975                "failing page decoder should not be drained after load error \
2976                 (requested_rows={})",
2977                requested_rows
2978            )))
2979        }
2980
2981        fn data_type(&self) -> &DataType {
2982            &self.page_data_type
2983        }
2984    }
2985
2986    #[test]
2987    fn test_read_zero_dimension_fsl_errors_instead_of_panicking() {
2988        // Simulates reading a column whose stored schema declares a
2989        // zero-dimension FixedSizeList, as old writers (before #5102) could
2990        // persist. The read plan is built by the field-scheduler factories,
2991        // which run the dimension guard before touching any column data, so
2992        // an empty column iterator is sufficient to reach the guard. The read
2993        // must surface a clean error rather than a divide-by-zero panic.
2994        use arrow_schema::Field as ArrowField;
2995
2996        let zero_dim = DataType::FixedSizeList(
2997            Arc::new(ArrowField::new("item", DataType::Float32, true)),
2998            0,
2999        );
3000        let field = Field::try_from(&ArrowField::new("vec", zero_dim, true)).unwrap();
3001        let strategy = CoreFieldDecoderStrategy::default();
3002
3003        let mut structural_columns = ColumnInfoIter::new(vec![], &[]);
3004        let err = strategy
3005            .create_structural_field_scheduler(&field, &mut structural_columns)
3006            .unwrap_err();
3007        assert!(
3008            err.to_string()
3009                .contains("dimension must be a positive integer"),
3010            "unexpected error: {}",
3011            err
3012        );
3013
3014        let mut legacy_columns = ColumnInfoIter::new(vec![], &[]);
3015        let err = strategy
3016            .create_legacy_field_scheduler(
3017                &field,
3018                &mut legacy_columns,
3019                FileBuffers {
3020                    positions_and_sizes: &[],
3021                },
3022            )
3023            .unwrap_err();
3024        assert!(
3025            err.to_string()
3026                .contains("dimension must be a positive integer"),
3027            "unexpected error: {}",
3028            err
3029        );
3030    }
3031
3032    #[tokio::test]
3033    async fn test_legacy_stream_stops_on_load_error() {
3034        use arrow_schema::Field as ArrowField;
3035
3036        let rows_per_batch = 1;
3037        let total_rows = 2;
3038        let scheduled_rows = 1;
3039        let page_rows = 1;
3040        let batch_readahead = 2;
3041        let load_error_message = "simulated page load failure";
3042        let fields = Fields::from(vec![ArrowField::new("vector", DataType::Float32, true)]);
3043        let root_decoder = SimpleStructDecoder::new(fields, total_rows);
3044        let (tx, rx) = unbounded_channel();
3045
3046        tx.send(Ok(DecoderMessage {
3047            scheduled_so_far: scheduled_rows,
3048            decoders: vec![MessageType::DecoderReady(DecoderReady {
3049                decoder: Box::new(FailingPageDecoder::new(
3050                    DataType::Float32,
3051                    page_rows,
3052                    load_error_message,
3053                )),
3054                path: VecDeque::from([0]),
3055            })],
3056        }))
3057        .unwrap();
3058        drop(tx);
3059
3060        let stream =
3061            BatchDecodeStream::new(rx, rows_per_batch, total_rows, root_decoder).into_stream();
3062        let mut batches = stream.map(|task| task.task).buffered(batch_readahead);
3063
3064        let err = batches
3065            .next()
3066            .await
3067            .expect("stream should emit the legacy page-load error")
3068            .unwrap_err();
3069        assert!(
3070            err.to_string().contains(load_error_message),
3071            "unexpected error: {}",
3072            err
3073        );
3074        assert!(
3075            batches.next().await.is_none(),
3076            "stream should stop after the legacy page-load error"
3077        );
3078    }
3079
3080    #[tokio::test]
3081    async fn test_structural_stream_stops_on_load_error() {
3082        let rows_per_batch = 1;
3083        let total_rows = 2;
3084        let scheduled_rows = 1;
3085        let batch_readahead = 2;
3086        let load_error_message = "simulated page load failure";
3087        let fields = Fields::from(vec![ArrowField::new("vector", DataType::Float32, true)]);
3088        let root_decoder = StructuralStructDecoder::new(fields, false, /*is_root=*/ true).unwrap();
3089        let (tx, rx) = unbounded_channel();
3090        let failed_page = async move { Err(Error::io(load_error_message)) }.boxed();
3091
3092        tx.send(Ok(DecoderMessage {
3093            scheduled_so_far: scheduled_rows,
3094            decoders: vec![MessageType::UnloadedPage(UnloadedPageShard(failed_page))],
3095        }))
3096        .unwrap();
3097        drop(tx);
3098
3099        let stream = StructuralBatchDecodeStream::new(
3100            rx,
3101            rows_per_batch,
3102            total_rows,
3103            root_decoder,
3104            /*spawn_batch_decode_tasks=*/ true,
3105            None,
3106        )
3107        .into_stream();
3108        let mut batches = stream.map(|task| task.task).buffered(batch_readahead);
3109
3110        let err = batches
3111            .next()
3112            .await
3113            .expect("stream should emit the page-load error")
3114            .unwrap_err();
3115        assert!(
3116            err.to_string().contains(load_error_message),
3117            "unexpected error: {}",
3118            err
3119        );
3120        assert!(
3121            batches.next().await.is_none(),
3122            "stream should stop after the page-load error"
3123        );
3124    }
3125
3126    #[test]
3127    fn test_coalesce_indices_to_ranges_with_single_index() {
3128        let indices = vec![1];
3129        let ranges = DecodeBatchScheduler::indices_to_ranges(&indices);
3130        assert_eq!(ranges, vec![1..2]);
3131    }
3132
3133    #[test]
3134    fn test_coalesce_indices_to_ranges() {
3135        let indices = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];
3136        let ranges = DecodeBatchScheduler::indices_to_ranges(&indices);
3137        assert_eq!(ranges, vec![1..10]);
3138    }
3139
3140    #[test]
3141    fn test_coalesce_indices_to_ranges_with_gaps() {
3142        let indices = vec![1, 2, 3, 5, 6, 7, 9];
3143        let ranges = DecodeBatchScheduler::indices_to_ranges(&indices);
3144        assert_eq!(ranges, vec![1..4, 5..8, 9..10]);
3145    }
3146
3147    #[test]
3148    fn test_estimate_bytes_per_row() {
3149        assert_eq!(estimate_bytes_per_row(&DataType::Int32), 4.0);
3150        assert_eq!(estimate_bytes_per_row(&DataType::Int64), 8.0);
3151        assert_eq!(estimate_bytes_per_row(&DataType::Float32), 4.0);
3152        assert_eq!(estimate_bytes_per_row(&DataType::Boolean), 1.0 / 8.0);
3153        assert_eq!(estimate_bytes_per_row(&DataType::Utf8), 64.0);
3154        assert_eq!(estimate_bytes_per_row(&DataType::Binary), 64.0);
3155        // Struct of 4 x Int32 = 16 bytes
3156        let struct_type = DataType::Struct(Fields::from(vec![
3157            ArrowField::new("a", DataType::Int32, false),
3158            ArrowField::new("b", DataType::Int32, false),
3159            ArrowField::new("c", DataType::Int32, false),
3160            ArrowField::new("d", DataType::Int32, false),
3161        ]));
3162        assert_eq!(estimate_bytes_per_row(&struct_type), 16.0);
3163    }
3164
3165    /// Helper: encode a batch, then decode it as a stream with optional
3166    /// `batch_size_bytes`, collecting all output batches.
3167    async fn decode_batches_with_byte_limit(
3168        batch: &RecordBatch,
3169        batch_size: u32,
3170        batch_size_bytes: Option<u64>,
3171    ) -> Vec<RecordBatch> {
3172        use crate::encoder::{EncodingOptions, default_encoding_strategy, encode_batch};
3173        use crate::version::LanceFileVersion;
3174
3175        let version = LanceFileVersion::V2_1;
3176        let options = EncodingOptions {
3177            version,
3178            ..Default::default()
3179        };
3180        let strategy = default_encoding_strategy(version);
3181        let schema = Schema::try_from(batch.schema().as_ref()).unwrap();
3182        let encoded = encode_batch(batch, Arc::new(schema.clone()), strategy.as_ref(), &options)
3183            .await
3184            .unwrap();
3185
3186        let io_scheduler =
3187            Arc::new(BufferScheduler::new(encoded.data.clone())) as Arc<dyn EncodingsIo>;
3188        let cache = Arc::new(lance_core::cache::LanceCache::with_capacity(
3189            128 * 1024 * 1024,
3190        ));
3191        let decoder_plugins = Arc::new(DecoderPlugins::default());
3192
3193        let mut decode_scheduler = DecodeBatchScheduler::try_new(
3194            encoded.schema.as_ref(),
3195            &encoded.top_level_columns,
3196            &encoded.page_table,
3197            &vec![],
3198            encoded.num_rows,
3199            decoder_plugins,
3200            io_scheduler.clone(),
3201            cache,
3202            &FilterExpression::no_filter(),
3203            &DecoderConfig::default(),
3204        )
3205        .await
3206        .unwrap();
3207
3208        let (tx, rx) = unbounded_channel();
3209        decode_scheduler.schedule_range(
3210            0..encoded.num_rows,
3211            &FilterExpression::no_filter(),
3212            tx,
3213            io_scheduler,
3214        );
3215
3216        let mut decode_stream = create_decode_stream(
3217            &encoded.schema,
3218            encoded.num_rows,
3219            batch_size,
3220            /*is_structural=*/ true,
3221            /*should_validate=*/ true,
3222            /*spawn_structural_batch_decode_tasks=*/ true,
3223            rx,
3224            batch_size_bytes,
3225        )
3226        .unwrap();
3227
3228        let mut batches = Vec::new();
3229        while let Some(task) = decode_stream.next().await {
3230            batches.push(task.task.await.unwrap());
3231        }
3232        batches
3233    }
3234
3235    #[tokio::test]
3236    async fn test_byte_sized_batches_fixed_width() {
3237        use arrow_array::Int32Array;
3238
3239        // 1000 rows x 4 Int32 columns = 16 bytes/row
3240        let num_rows: i32 = 1000;
3241        let arrays: Vec<Arc<dyn arrow_array::Array>> = (0..4)
3242            .map(|col| {
3243                Arc::new(Int32Array::from_iter_values(
3244                    (0..num_rows).map(move |row| row * 10 + col),
3245                )) as _
3246            })
3247            .collect();
3248
3249        let schema = Arc::new(ArrowSchema::new(vec![
3250            ArrowField::new("a", DataType::Int32, false),
3251            ArrowField::new("b", DataType::Int32, false),
3252            ArrowField::new("c", DataType::Int32, false),
3253            ArrowField::new("d", DataType::Int32, false),
3254        ]));
3255        let input_batch = RecordBatch::try_new(schema, arrays).unwrap();
3256
3257        // 16 bytes/row, batch_size_bytes=1600 => 100 rows/batch
3258        let batches =
3259            decode_batches_with_byte_limit(&input_batch, /*batch_size=*/ 1024, Some(1600)).await;
3260
3261        // Should produce 10 batches of 100 rows each
3262        assert_eq!(batches.len(), 10);
3263        for (i, batch) in batches.iter().enumerate() {
3264            assert_eq!(
3265                batch.num_rows(),
3266                100,
3267                "batch {i} should have 100 rows, got {}",
3268                batch.num_rows()
3269            );
3270        }
3271
3272        // Verify roundtrip: concatenate and compare
3273        let all_batches: Vec<&RecordBatch> = batches.iter().collect();
3274        let concatenated =
3275            arrow_select::concat::concat_batches(&batches[0].schema(), all_batches.iter().copied())
3276                .unwrap();
3277        assert_eq!(concatenated.num_rows(), num_rows as usize);
3278        for col in 0..4 {
3279            assert_eq!(
3280                concatenated.column(col).as_ref(),
3281                input_batch.column(col).as_ref(),
3282                "column {col} roundtrip mismatch"
3283            );
3284        }
3285    }
3286
3287    #[tokio::test]
3288    async fn test_byte_sized_batches_none_unchanged() {
3289        use arrow_array::Int32Array;
3290
3291        // Without batch_size_bytes, rows_per_batch controls batching
3292        let num_rows: i32 = 1000;
3293        let arrays: Vec<Arc<dyn arrow_array::Array>> = (0..2)
3294            .map(|col| {
3295                Arc::new(Int32Array::from_iter_values(
3296                    (0..num_rows).map(move |row| row * 10 + col),
3297                )) as _
3298            })
3299            .collect();
3300
3301        let schema = Arc::new(ArrowSchema::new(vec![
3302            ArrowField::new("x", DataType::Int32, false),
3303            ArrowField::new("y", DataType::Int32, false),
3304        ]));
3305        let input_batch = RecordBatch::try_new(schema, arrays).unwrap();
3306
3307        // batch_size=250, batch_size_bytes=None => 4 batches of 250 rows
3308        let batches = decode_batches_with_byte_limit(&input_batch, /*batch_size=*/ 250, None).await;
3309        assert_eq!(batches.len(), 4);
3310        for (i, batch) in batches.iter().enumerate() {
3311            assert_eq!(
3312                batch.num_rows(),
3313                250,
3314                "batch {i} should have 250 rows, got {}",
3315                batch.num_rows()
3316            );
3317        }
3318    }
3319
3320    #[tokio::test]
3321    async fn test_byte_sized_batches_feedback_convergence() {
3322        use arrow_array::StringArray;
3323
3324        // Each row has a 100-byte string. Schema estimate = 64 bytes (default
3325        // for Utf8), so the first batch will overshoot. The feedback loop
3326        // should correct subsequent batches toward the target.
3327        let num_rows = 500;
3328        let value: String = "x".repeat(100);
3329        let arrays: Vec<Arc<dyn arrow_array::Array>> = vec![Arc::new(StringArray::from(
3330            (0..num_rows).map(|_| value.as_str()).collect::<Vec<_>>(),
3331        ))];
3332        let schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
3333            "s",
3334            DataType::Utf8,
3335            false,
3336        )]));
3337        let input_batch = RecordBatch::try_new(schema, arrays).unwrap();
3338
3339        // Target 5000 bytes/batch. At 100 bytes/row the ideal is 50 rows/batch.
3340        // Schema estimate is 64 bytes/row → first batch ~78 rows (overshoot).
3341        // After feedback kicks in, batches should converge to ~50 rows.
3342        let target_bytes: u64 = 5000;
3343        let batches = decode_batches_with_byte_limit(
3344            &input_batch,
3345            /*batch_size=*/ 1024,
3346            Some(target_bytes),
3347        )
3348        .await;
3349
3350        // Verify all data round-trips correctly
3351        let all_batches: Vec<&RecordBatch> = batches.iter().collect();
3352        let concatenated =
3353            arrow_select::concat::concat_batches(&batches[0].schema(), all_batches.iter().copied())
3354                .unwrap();
3355        assert_eq!(concatenated.num_rows(), num_rows as usize);
3356        assert_eq!(
3357            concatenated.column(0).as_ref(),
3358            input_batch.column(0).as_ref()
3359        );
3360
3361        // After the first batch, subsequent batches should be closer to the
3362        // target. The ideal is 50 rows/batch.
3363        assert!(
3364            batches.len() >= 2,
3365            "need at least 2 batches to test convergence"
3366        );
3367        // The first batch uses the schema estimate (64 bytes/row) →
3368        // ~78 rows. After feedback the rows should settle near 50.
3369        if batches.len() >= 3 {
3370            let second_batch_rows = batches[1].num_rows();
3371            let third_batch_rows = batches[2].num_rows();
3372            // Both should be within 20% of the ideal (50 rows)
3373            assert!(
3374                (40..=60).contains(&second_batch_rows),
3375                "second batch should be near 50 rows, got {second_batch_rows}"
3376            );
3377            assert!(
3378                (40..=60).contains(&third_batch_rows),
3379                "third batch should be near 50 rows, got {third_batch_rows}"
3380            );
3381        }
3382    }
3383}