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