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