Skip to main content

delta_arrow_reader/
reader.rs

1//! Public Delta-to-Arrow reader and optional DataFusion adapter.
2
3pub(crate) mod backend;
4#[cfg(feature = "datafusion")]
5pub mod datafusion;
6pub(crate) mod deletion_vector;
7pub(crate) mod metrics;
8mod options;
9pub(crate) mod partition_target;
10pub(crate) mod planning;
11pub(crate) mod predicate;
12#[allow(dead_code)]
13pub(crate) mod scheduling;
14pub(crate) mod transform;
15
16#[doc(hidden)]
17pub use metrics::ParquetRangePlanningDiagnosticSnapshot;
18pub use metrics::{DeltaScanMetrics, DeltaScanMetricsSnapshot};
19#[doc(hidden)]
20pub use options::ParquetRangeReadPolicy;
21pub use options::{
22    DeltaScanExecutionOptions, DeltaSnapshotSelection, DeltaStorageOptions, ParquetReaderBackend,
23};
24pub use predicate::{DeltaComparison, DeltaPredicate, DeltaScalar};
25
26use std::{
27    fmt,
28    pin::Pin,
29    sync::Arc,
30    task::{Context, Poll},
31};
32
33use arrow::{datatypes::SchemaRef, record_batch::RecordBatch};
34use futures_util::Stream;
35use snafu::ResultExt;
36
37use self::{
38    planning::{
39        DeltaScanPartitionTargetOptions, DeltaScanPlan, build_physical_row_predicate, plan_scan,
40    },
41    predicate::{evaluate_predicate, referenced_columns, validate_predicate},
42    scheduling::{
43        DeltaScanScheduler, FileAdmissionDecision, FileAdmissionPolicy, FileBatchStream,
44        FileExecutor, OrderedPartitionStream,
45    },
46};
47
48use crate::{
49    DeltaProtocol, DeltaReaderError,
50    delta::{
51        kernel::{DeltaKernelPredicate, kernel_pruning_predicate, kernel_row_predicate},
52        protocol::validate_protocol,
53        snapshot::{
54            ArrowTableSnapshot, KernelTableSnapshot, load_delta_table_snapshot,
55            load_kernel_table_snapshot,
56        },
57    },
58    error::{DataFileReadSnafu, InvalidConfigurationSnafu, ScanPlanningSnafu, SnapshotLoadSnafu},
59};
60
61const TRACING_TARGET: &str = "delta_arrow_reader";
62const DELTA_LOG_SCAN_METADATA_SOURCE: &str = "delta_log";
63const QUERY_PLANNING_CACHE_SCAN_METADATA_SOURCE: &str = "query_planning_cache";
64
65/// Selects how much work table loading performs before returning.
66///
67/// Each warmup level includes the work performed by the levels before it. The default performs no
68/// warmup and leaves reusable metadata work to individual queries.
69#[non_exhaustive]
70#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
71pub enum WarmupMode {
72    /// Performs no warmup.
73    #[default]
74    None,
75    /// Loads and retains the active-file metadata used during query planning.
76    QueryPlanning,
77}
78
79/// Configures and loads one immutable Delta table snapshot.
80///
81/// The asynchronous path uses the caller's Tokio runtime. Scans return a
82/// pull-driven stream and do not materialize the whole table.
83///
84/// # Example
85///
86/// ```no_run
87/// use delta_arrow_reader::{DeltaComparison, DeltaPredicate, DeltaScalar, DeltaTableBuilder};
88/// use futures_util::TryStreamExt;
89///
90/// # async fn read_table() -> Result<(), Box<dyn std::error::Error>> {
91/// let table = DeltaTableBuilder::new("/tmp/example-delta-table")
92///     .load_table()
93///     .await?;
94/// let scan = table
95///     .scan()
96///     .with_projection(["id", "name"])
97///     .with_predicate(DeltaPredicate::Compare {
98///         column: "id".into(),
99///         op: DeltaComparison::GtEq,
100///         value: DeltaScalar::Int64(10),
101///     })
102///     .with_limit(100)
103///     .build()
104///     .await?;
105/// let mut batches = scan.into_stream();
106///
107/// while let Some(batch) = batches.try_next().await? {
108///     println!("rows={}", batch.num_rows());
109/// }
110/// # Ok(())
111/// # }
112/// ```
113#[must_use = "table builder settings do nothing unless the builder is loaded"]
114pub struct DeltaTableBuilder {
115    table_location: String,
116    storage_options: DeltaStorageOptions,
117    snapshot_selection: DeltaSnapshotSelection,
118    execution_options: DeltaScanExecutionOptions,
119    warmup: WarmupMode,
120}
121
122impl DeltaTableBuilder {
123    /// Creates a builder for the latest snapshot with default execution settings.
124    pub fn new(table_location: impl Into<String>) -> Self {
125        Self {
126            table_location: table_location.into(),
127            storage_options: DeltaStorageOptions::new(),
128            snapshot_selection: DeltaSnapshotSelection::Latest,
129            execution_options: DeltaScanExecutionOptions::new(),
130            warmup: WarmupMode::None,
131        }
132    }
133
134    /// Replaces the storage options forwarded during table loading.
135    pub fn with_storage_options(mut self, storage_options: DeltaStorageOptions) -> Self {
136        self.storage_options = storage_options;
137        self
138    }
139
140    /// Selects the Delta snapshot to load.
141    pub const fn with_snapshot_selection(
142        mut self,
143        snapshot_selection: DeltaSnapshotSelection,
144    ) -> Self {
145        self.snapshot_selection = snapshot_selection;
146        self
147    }
148
149    /// Replaces the default execution settings used by scans of this table.
150    pub const fn with_execution_options(
151        mut self,
152        execution_options: DeltaScanExecutionOptions,
153    ) -> Self {
154        self.execution_options = execution_options;
155        self
156    }
157
158    /// Selects work to finish during [`Self::load_table`] for reuse by later queries.
159    ///
160    /// [`Self::load_snapshot`] does not build a table and rejects any setting other than
161    /// [`WarmupMode::None`].
162    pub const fn with_warmup(mut self, warmup: WarmupMode) -> Self {
163        self.warmup = warmup;
164        self
165    }
166
167    /// Loads the table through the caller-owned Tokio runtime.
168    ///
169    /// With [`WarmupMode::None`], this method loads the snapshot and converts its schema. Protocol
170    /// validation and scan metadata loading remain deferred until a scan is built. This allows an
171    /// application to inspect the version, schema, and protocol of a table that the reader cannot
172    /// scan.
173    ///
174    /// A warmup mode validates the protocol while loading because it builds reusable query-planning
175    /// state. Query-planning warmup retains active-file metadata.
176    ///
177    /// # Errors
178    ///
179    /// Returns an error if the table location, storage, snapshot, or schema cannot be loaded. A
180    /// warmup can also fail if the table protocol is unsupported or its reusable metadata cannot be
181    /// built.
182    pub async fn load_table(self) -> Result<DeltaTable, DeltaReaderError> {
183        let snapshot = load_delta_table_snapshot(
184            self.table_location,
185            self.storage_options,
186            self.snapshot_selection,
187        )
188        .await?;
189        let snapshot = if self.warmup == WarmupMode::None {
190            snapshot
191        } else {
192            validate_protocol(snapshot.protocol())?;
193            materialize_eager_scan_metadata(snapshot).await?
194        };
195        Ok(DeltaTable::new(snapshot, self.execution_options))
196    }
197
198    /// Loads a Delta Kernel snapshot without converting its logical Arrow schema.
199    ///
200    /// Warmup settings apply to [`DeltaTable`] and are not supported by this method. Calling this
201    /// method after selecting any mode other than [`WarmupMode::None`] returns a configuration
202    /// error.
203    ///
204    /// # Errors
205    ///
206    /// Returns an error if warmup was requested or if the table location, storage, or snapshot
207    /// cannot be loaded.
208    pub async fn load_snapshot(self) -> Result<DeltaTableSnapshot, DeltaReaderError> {
209        if self.warmup != WarmupMode::None {
210            return InvalidConfigurationSnafu {
211                reason: "snapshot_load_does_not_support_table_warmup",
212            }
213            .fail();
214        }
215        let snapshot = load_kernel_table_snapshot(
216            self.table_location,
217            self.storage_options,
218            self.snapshot_selection,
219        )
220        .await?;
221        Ok(DeltaTableSnapshot::new(snapshot, self.execution_options))
222    }
223}
224
225async fn materialize_eager_scan_metadata(
226    snapshot: ArrowTableSnapshot,
227) -> Result<ArrowTableSnapshot, DeltaReaderError> {
228    tokio::task::spawn_blocking(move || snapshot.materialize_eager_scan_metadata())
229        .await
230        .boxed()
231        .context(ScanPlanningSnafu {
232            reason: "query_planning_warmup_task_failed",
233        })?
234}
235
236impl fmt::Debug for DeltaTableBuilder {
237    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
238        formatter
239            .debug_struct("DeltaTableBuilder")
240            .field("table_location", &"<redacted>")
241            .field("storage_options", &"<redacted>")
242            .field("snapshot_selection", &self.snapshot_selection)
243            .field("execution_options", &self.execution_options)
244            .field("warmup", &self.warmup)
245            .finish()
246    }
247}
248
249/// Loaded Delta snapshot metadata awaiting logical Arrow schema conversion.
250pub struct DeltaTableSnapshot {
251    snapshot: KernelTableSnapshot,
252    execution_options: DeltaScanExecutionOptions,
253}
254
255impl DeltaTableSnapshot {
256    fn new(snapshot: KernelTableSnapshot, execution_options: DeltaScanExecutionOptions) -> Self {
257        Self {
258            snapshot,
259            execution_options,
260        }
261    }
262
263    /// Returns the loaded Delta snapshot version.
264    pub fn version(&self) -> u64 {
265        self.snapshot.version()
266    }
267
268    /// Returns the loaded Delta protocol metadata.
269    pub fn protocol(&self) -> &DeltaProtocol {
270        self.snapshot.protocol()
271    }
272
273    /// Returns the normalized table URL.
274    ///
275    /// This value may contain sensitive caller input. Do not log or expose it.
276    pub fn table_url(&self) -> &str {
277        self.snapshot.table_url()
278    }
279
280    /// Validates the loaded snapshot against the supported reader protocol.
281    pub fn validate_protocol(&self) -> Result<(), DeltaReaderError> {
282        validate_protocol(self.protocol())
283    }
284
285    /// Converts the logical Arrow schema and finishes constructing the table.
286    pub fn into_table(self) -> Result<DeltaTable, DeltaReaderError> {
287        Ok(DeltaTable::new(
288            self.snapshot.into_arrow_snapshot()?,
289            self.execution_options,
290        ))
291    }
292}
293
294impl fmt::Debug for DeltaTableSnapshot {
295    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
296        formatter
297            .debug_struct("DeltaTableSnapshot")
298            .field("version", &self.version())
299            .finish_non_exhaustive()
300    }
301}
302
303/// One immutable loaded Delta table snapshot.
304#[derive(Clone)]
305pub struct DeltaTable {
306    snapshot: Arc<ArrowTableSnapshot>,
307    execution_options: DeltaScanExecutionOptions,
308}
309
310impl DeltaTable {
311    fn new(snapshot: ArrowTableSnapshot, execution_options: DeltaScanExecutionOptions) -> Self {
312        Self {
313            snapshot: Arc::new(snapshot),
314            execution_options,
315        }
316    }
317
318    /// Returns the loaded Delta snapshot version.
319    pub fn version(&self) -> u64 {
320        self.snapshot.version()
321    }
322
323    /// Returns a shared handle to the logical Arrow schema.
324    pub fn schema(&self) -> SchemaRef {
325        self.snapshot.schema()
326    }
327
328    /// Returns the loaded Delta protocol metadata.
329    pub fn protocol(&self) -> &DeltaProtocol {
330        self.snapshot.protocol()
331    }
332
333    /// Returns the normalized table URL.
334    ///
335    /// This value may contain sensitive caller input. Do not log or expose it.
336    pub fn table_url(&self) -> &str {
337        self.snapshot.table_url()
338    }
339
340    #[allow(dead_code)]
341    pub(crate) fn partition_columns(&self) -> &[String] {
342        self.snapshot.partition_columns()
343    }
344
345    #[allow(dead_code)]
346    pub(crate) fn snapshot(&self) -> &ArrowTableSnapshot {
347        self.snapshot.as_ref()
348    }
349
350    /// Validates the loaded snapshot against the supported reader protocol.
351    pub fn validate_protocol(&self) -> Result<(), DeltaReaderError> {
352        validate_protocol(self.protocol())
353    }
354
355    /// Loads the latest version from this table and returns it as a new immutable table.
356    ///
357    /// The current table and any scans built from it remain fixed at their original version. A
358    /// query-planning metadata cache is refreshed for the new version and reused directly when the
359    /// version has not changed.
360    ///
361    /// # Errors
362    ///
363    /// Returns an error if the newer snapshot, schema, or retained query-planning metadata cannot
364    /// be loaded. No partially refreshed table is returned.
365    pub async fn refresh(&self) -> Result<Self, DeltaReaderError> {
366        let snapshot = Arc::clone(&self.snapshot);
367        let snapshot = tokio::task::spawn_blocking(move || snapshot.refresh())
368            .await
369            .boxed()
370            .context(SnapshotLoadSnafu {
371                reason: "snapshot_refresh_task_failed",
372            })
373            .and_then(|result| result)?;
374        Ok(Self::new(snapshot, self.execution_options))
375    }
376
377    /// Starts configuring a new single-use scan.
378    pub fn scan(&self) -> DeltaScanBuilder<'_> {
379        DeltaScanBuilder {
380            table: self,
381            projection: None,
382            predicate: None,
383            limit: None,
384            target_partitions: None,
385            execution_options: self.execution_options,
386        }
387    }
388}
389
390impl fmt::Debug for DeltaTable {
391    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
392        formatter
393            .debug_struct("DeltaTable")
394            .field("version", &self.version())
395            .finish_non_exhaustive()
396    }
397}
398
399/// Configures one single-use streaming Delta scan.
400#[must_use = "scan builder settings do nothing unless the scan is built"]
401pub struct DeltaScanBuilder<'table> {
402    table: &'table DeltaTable,
403    projection: Option<Vec<String>>,
404    predicate: Option<DeltaPredicate>,
405    limit: Option<usize>,
406    target_partitions: Option<usize>,
407    execution_options: DeltaScanExecutionOptions,
408}
409
410impl<'table> DeltaScanBuilder<'table> {
411    /// Selects visible logical columns in caller order.
412    pub fn with_projection(
413        mut self,
414        logical_columns: impl IntoIterator<Item = impl Into<String>>,
415    ) -> Self {
416        self.projection = Some(logical_columns.into_iter().map(Into::into).collect());
417        self
418    }
419
420    /// Replaces the exact logical row predicate.
421    pub fn with_predicate(mut self, predicate: DeltaPredicate) -> Self {
422        self.predicate = Some(predicate);
423        self
424    }
425
426    /// Sets the maximum number of output rows.
427    pub const fn with_limit(mut self, limit: usize) -> Self {
428        self.limit = Some(limit);
429        self
430    }
431
432    /// Overrides the number of planned scan partitions.
433    pub fn with_target_partitions(
434        mut self,
435        target_partitions: usize,
436    ) -> Result<Self, DeltaReaderError> {
437        if target_partitions == 0 {
438            return InvalidConfigurationSnafu {
439                reason: "scan_partition_target_must_be_positive",
440            }
441            .fail();
442        }
443        self.target_partitions = Some(target_partitions);
444        Ok(self)
445    }
446
447    /// Replaces the execution settings for this scan.
448    pub const fn with_execution_options(
449        mut self,
450        execution_options: DeltaScanExecutionOptions,
451    ) -> Self {
452        self.execution_options = execution_options;
453        self
454    }
455
456    /// Builds one immutable single-use scan plan without reading data files.
457    pub async fn build(self) -> Result<DeltaScan, DeltaReaderError> {
458        self.table.validate_protocol()?;
459        if let Some(predicate) = self.predicate.as_ref() {
460            validate_predicate(predicate, self.table.schema().as_ref())?;
461        }
462
463        let snapshot_version = self.table.version();
464        let backend = self.execution_options.parquet_backend();
465        let scan_metadata_source = if self.table.snapshot.eager_scan_metadata().is_some() {
466            QUERY_PLANNING_CACHE_SCAN_METADATA_SOURCE
467        } else {
468            DELTA_LOG_SCAN_METADATA_SOURCE
469        };
470        trace_planning_started(snapshot_version, backend, scan_metadata_source);
471        let snapshot = Arc::clone(&self.table.snapshot);
472        let projection = self.projection;
473        let predicate = self.predicate;
474        let hidden_columns = predicate
475            .as_ref()
476            .map(referenced_columns)
477            .unwrap_or_default();
478        let row_predicate = predicate
479            .as_ref()
480            .filter(|_| backend == ParquetReaderBackend::Direct)
481            .and_then(|predicate| kernel_row_predicate(predicate, snapshot.partition_columns()));
482        let kernel_predicate = predicate.as_ref().and_then(kernel_pruning_predicate);
483        let include_stats = kernel_predicate.is_some();
484        let execution_options = self.execution_options;
485        let target_partitions = self.target_partitions;
486        let result = tokio::task::spawn_blocking(move || {
487            // Reuse the already-mapped predicate for ordinary data-only filters.
488            let same_predicate = row_predicate.is_some() && row_predicate == kernel_predicate;
489            let plan = plan_scan(
490                snapshot.as_ref(),
491                projection.as_deref(),
492                &hidden_columns,
493                kernel_predicate,
494                include_stats,
495                execution_options,
496                DeltaScanPartitionTargetOptions {
497                    explicit_target_partitions: target_partitions,
498                    datafusion_target_partitions: None,
499                },
500            )?;
501            let physical_row_predicate = if plan.partitions.is_empty() {
502                None
503            } else if same_predicate {
504                plan.physical_predicate.clone()
505            } else {
506                build_physical_row_predicate(
507                    snapshot.as_ref(),
508                    projection.as_deref(),
509                    &hidden_columns,
510                    row_predicate,
511                )?
512            };
513            Ok((plan, physical_row_predicate))
514        })
515        .await
516        .boxed()
517        .context(ScanPlanningSnafu {
518            reason: "scan_planning_task_failed",
519        })
520        .and_then(|result| result);
521
522        match result {
523            Ok((plan, physical_row_predicate)) => {
524                trace_planning_completed(
525                    snapshot_version,
526                    backend,
527                    plan.partitions.len(),
528                    scan_metadata_source,
529                );
530                Ok(DeltaScan {
531                    plan: Arc::new(plan),
532                    predicate,
533                    limit: self.limit,
534                    physical_row_predicate,
535                })
536            }
537            Err(error) => {
538                trace_planning_failed(snapshot_version, backend, scan_metadata_source, &error);
539                Err(error)
540            }
541        }
542    }
543}
544
545/// One immutable, single-use streaming Delta scan plan.
546///
547/// A scan cannot be cloned or converted into a stream twice.
548///
549/// ```compile_fail
550/// use delta_arrow_reader::DeltaScan;
551///
552/// fn stream_twice(scan: DeltaScan) {
553///     let _ = scan.into_stream();
554///     let _ = scan.into_stream();
555/// }
556/// ```
557///
558/// ```compile_fail
559/// use delta_arrow_reader::DeltaScan;
560///
561/// fn clone_scan(scan: DeltaScan) {
562///     let _ = scan.clone();
563/// }
564/// ```
565#[must_use = "scans do nothing unless converted into a stream"]
566pub struct DeltaScan {
567    plan: Arc<DeltaScanPlan>,
568    predicate: Option<DeltaPredicate>,
569    limit: Option<usize>,
570    physical_row_predicate: Option<DeltaKernelPredicate>,
571}
572
573impl DeltaScan {
574    /// Returns a shared handle to the visible logical output schema.
575    pub fn schema(&self) -> SchemaRef {
576        Arc::clone(&self.plan.projected_schema)
577    }
578
579    /// Returns the number of planned execution partitions.
580    pub fn partition_count(&self) -> usize {
581        self.plan.partitions.len()
582    }
583
584    /// Converts the scan into a pull-driven Arrow batch stream.
585    ///
586    /// Data-file reads begin only when the stream is polled.
587    pub fn into_stream(self) -> DeltaBatchStream {
588        let metrics = self.plan.metrics.clone();
589        let schema = Arc::clone(&self.plan.projected_schema);
590        let partition_count = self.plan.partitions.len();
591        let snapshot_version = self.plan.snapshot_version;
592        let backend = self.plan.execution_options.parquet_backend();
593        let projection = (self.plan.logical_schema.as_ref() != schema.as_ref())
594            .then(|| (0..schema.fields().len()).collect::<Vec<_>>());
595        let partitions = if self.limit == Some(0) {
596            OrderedPartitionStream::default()
597        } else {
598            let scheduler = DeltaScanScheduler::new(Arc::clone(&self.plan));
599            let admission: FileAdmissionPolicy<_> = Arc::new(|_| Ok(FileAdmissionDecision::Admit));
600            let executor = match backend {
601                ParquetReaderBackend::Direct => {
602                    direct_parquet_executor(&self.plan, None, self.physical_row_predicate)
603                }
604                ParquetReaderBackend::DeltaKernel => delta_kernel_executor(&self.plan),
605            };
606            scheduler.partition_streams(admission, executor)
607        };
608
609        DeltaBatchStream {
610            schema,
611            metrics,
612            partitions,
613            predicate: self.predicate,
614            projection,
615            remaining: self.limit,
616            snapshot_version,
617            backend,
618            partition_count,
619            started: false,
620            done: false,
621        }
622    }
623}
624
625/// Pull-driven stream of finalized logical Arrow batches from one Delta scan.
626///
627/// The stream has no inherent whole-result collection method. Callers that
628/// intentionally materialize a result must opt into a stream extension trait.
629///
630/// ```compile_fail
631/// use delta_arrow_reader::DeltaBatchStream;
632///
633/// fn collect_without_opt_in(stream: DeltaBatchStream) {
634///     let _ = stream.collect();
635/// }
636/// ```
637#[must_use = "streams do nothing unless polled"]
638pub struct DeltaBatchStream {
639    schema: SchemaRef,
640    metrics: DeltaScanMetrics,
641    partitions: OrderedPartitionStream,
642    predicate: Option<DeltaPredicate>,
643    projection: Option<Vec<usize>>,
644    remaining: Option<usize>,
645    snapshot_version: u64,
646    backend: ParquetReaderBackend,
647    partition_count: usize,
648    started: bool,
649    done: bool,
650}
651
652impl DeltaBatchStream {
653    /// Returns a shared handle to the visible logical output schema.
654    pub fn schema(&self) -> SchemaRef {
655        Arc::clone(&self.schema)
656    }
657
658    /// Returns a lightweight shared handle to live scan metrics.
659    pub fn metrics(&self) -> DeltaScanMetrics {
660        self.metrics.clone()
661    }
662
663    fn start(&mut self) {
664        if self.started {
665            return;
666        }
667        self.started = true;
668        trace_execution_started(self.snapshot_version, self.backend, self.partition_count);
669    }
670
671    fn complete(&mut self) {
672        if self.done {
673            return;
674        }
675        self.partitions.clear();
676        self.done = true;
677        trace_execution_completed(self.snapshot_version, self.backend, self.partition_count);
678    }
679
680    fn fail(&mut self, error: &DeltaReaderError) {
681        self.partitions.clear();
682        self.done = true;
683        trace_execution_failed(
684            self.snapshot_version,
685            self.backend,
686            self.partition_count,
687            error,
688        );
689    }
690
691    fn finalize_batch(&self, mut batch: RecordBatch) -> Result<RecordBatch, DeltaReaderError> {
692        if let Some(predicate) = self.predicate.as_ref() {
693            batch = evaluate_predicate(&batch, predicate)?;
694        }
695        if let Some(projection) = self.projection.as_ref() {
696            batch = batch
697                .project(projection)
698                .boxed()
699                .context(DataFileReadSnafu {
700                    reason: "direct_projection_failed",
701                })?;
702        }
703        Ok(batch)
704    }
705}
706
707impl Stream for DeltaBatchStream {
708    type Item = Result<RecordBatch, DeltaReaderError>;
709
710    fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
711        let this = self.get_mut();
712        if this.done {
713            return Poll::Ready(None);
714        }
715        this.start();
716
717        match Pin::new(&mut this.partitions).poll_next(context) {
718            Poll::Ready(Some(Ok(batch))) => {
719                let mut batch = match this.finalize_batch(batch) {
720                    Ok(batch) => batch,
721                    Err(error) => {
722                        this.fail(&error);
723                        return Poll::Ready(Some(Err(error)));
724                    }
725                };
726                if let Some(remaining) = this.remaining.as_mut() {
727                    if batch.num_rows() >= *remaining {
728                        batch = batch.slice(0, *remaining);
729                        *remaining = 0;
730                        this.complete();
731                    } else {
732                        *remaining -= batch.num_rows();
733                    }
734                }
735                Poll::Ready(Some(Ok(batch)))
736            }
737            Poll::Ready(Some(Err(error))) => {
738                this.fail(&error);
739                Poll::Ready(Some(Err(error)))
740            }
741            Poll::Ready(None) => {
742                this.complete();
743                Poll::Ready(None)
744            }
745            Poll::Pending => Poll::Pending,
746        }
747    }
748}
749
750impl Drop for DeltaBatchStream {
751    fn drop(&mut self) {
752        if self.done {
753            return;
754        }
755        self.partitions.clear();
756        self.done = true;
757        trace_execution_dropped(self.snapshot_version, self.backend, self.partition_count);
758    }
759}
760
761pub(crate) fn direct_parquet_executor(
762    plan: &Arc<DeltaScanPlan>,
763    output_batch_size_rows: Option<usize>,
764    row_predicate: Option<crate::delta::kernel::DeltaKernelPredicate>,
765) -> FileExecutor<planning::DeltaScanFileTask, FileBatchStream> {
766    backend::direct_parquet::direct_parquet_file_executor(
767        plan,
768        output_batch_size_rows,
769        row_predicate,
770        Arc::default(),
771        None,
772    )
773}
774
775pub(crate) fn delta_kernel_executor(
776    plan: &Arc<DeltaScanPlan>,
777) -> FileExecutor<planning::DeltaScanFileTask, FileBatchStream> {
778    backend::kernel_reader::delta_kernel_file_executor(plan)
779}
780
781fn trace_planning_started(
782    snapshot_version: u64,
783    backend: ParquetReaderBackend,
784    scan_metadata_source: &'static str,
785) {
786    tracing::debug!(
787        target: TRACING_TARGET,
788        event = "scan_planning.started",
789        snapshot_version,
790        backend = ?backend,
791        partition_count = tracing::field::Empty,
792        scan_metadata_source,
793        outcome = "started"
794    );
795}
796
797fn trace_planning_completed(
798    snapshot_version: u64,
799    backend: ParquetReaderBackend,
800    partition_count: usize,
801    scan_metadata_source: &'static str,
802) {
803    tracing::debug!(
804        target: TRACING_TARGET,
805        event = "scan_planning.completed",
806        snapshot_version,
807        backend = ?backend,
808        partition_count,
809        scan_metadata_source,
810        outcome = "completed"
811    );
812}
813
814fn trace_planning_failed(
815    snapshot_version: u64,
816    backend: ParquetReaderBackend,
817    scan_metadata_source: &'static str,
818    error: &DeltaReaderError,
819) {
820    tracing::debug!(
821        target: TRACING_TARGET,
822        event = "scan_planning.failed",
823        snapshot_version,
824        backend = ?backend,
825        partition_count = tracing::field::Empty,
826        scan_metadata_source,
827        outcome = "failed",
828        error_code = error.code(),
829        error_phase = error.phase().as_str()
830    );
831}
832
833fn trace_execution_started(
834    snapshot_version: u64,
835    backend: ParquetReaderBackend,
836    partition_count: usize,
837) {
838    tracing::debug!(
839        target: TRACING_TARGET,
840        event = "scan_execution.started",
841        snapshot_version,
842        backend = ?backend,
843        partition_count,
844        outcome = "started"
845    );
846}
847
848fn trace_execution_completed(
849    snapshot_version: u64,
850    backend: ParquetReaderBackend,
851    partition_count: usize,
852) {
853    tracing::debug!(
854        target: TRACING_TARGET,
855        event = "scan_execution.completed",
856        snapshot_version,
857        backend = ?backend,
858        partition_count,
859        outcome = "completed"
860    );
861}
862
863fn trace_execution_failed(
864    snapshot_version: u64,
865    backend: ParquetReaderBackend,
866    partition_count: usize,
867    error: &DeltaReaderError,
868) {
869    tracing::debug!(
870        target: TRACING_TARGET,
871        event = "scan_execution.failed",
872        snapshot_version,
873        backend = ?backend,
874        partition_count,
875        outcome = "failed",
876        error_code = error.code(),
877        error_phase = error.phase().as_str()
878    );
879}
880
881fn trace_execution_dropped(
882    snapshot_version: u64,
883    backend: ParquetReaderBackend,
884    partition_count: usize,
885) {
886    tracing::debug!(
887        target: TRACING_TARGET,
888        event = "scan_execution.dropped",
889        snapshot_version,
890        backend = ?backend,
891        partition_count,
892        outcome = "dropped"
893    );
894}
895
896#[cfg(test)]
897mod tests {
898    use std::{
899        collections::{BTreeMap, VecDeque},
900        fmt, fs,
901        future::pending,
902        path::{Path, PathBuf},
903        sync::{Arc, Mutex, Once},
904        time::{Duration, SystemTime, UNIX_EPOCH},
905    };
906
907    use arrow::{
908        array::Int32Array,
909        datatypes::{DataType, Field, Schema, SchemaRef},
910        record_batch::RecordBatch,
911    };
912    use futures_util::{FutureExt, StreamExt, stream};
913    use tokio::{sync::Notify, time::timeout};
914    use tracing::{
915        Event, Level, Metadata, Subscriber,
916        field::{Field as TracingField, Visit},
917        span::{Attributes, Id, Record},
918        subscriber::{Interest, with_default},
919    };
920
921    use super::{
922        DeltaBatchStream, DeltaTable, trace_execution_completed, trace_execution_dropped,
923        trace_execution_failed, trace_execution_started, trace_planning_completed,
924        trace_planning_failed, trace_planning_started,
925    };
926    use crate::{
927        DeltaScanExecutionOptions, DeltaScanMetrics, DeltaSnapshotSelection, DeltaStorageOptions,
928        ParquetReaderBackend,
929        delta::snapshot::load_delta_table_snapshot_blocking,
930        error::InvalidConfigurationSnafu,
931        reader::{
932            metrics::DeltaScanMetricsConfig,
933            scheduling::{
934                FileAdmissionDecision, FileAdmissionPolicy, FileBatchStream, FileExecutor,
935                FileReadPermit, OrderedPartitionStream, PartitionStream, ScanCancellation,
936                ScanReadLimiter,
937            },
938        },
939    };
940
941    static TRACING_TEST_LOCK: Mutex<()> = Mutex::new(());
942    static TRACING_TEST_GLOBAL_SUBSCRIBER: Once = Once::new();
943
944    #[derive(Clone, Default)]
945    struct EventFields(Arc<Mutex<Vec<BTreeMap<String, String>>>>);
946
947    impl Subscriber for EventFields {
948        fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest {
949            if metadata.target() == "delta_arrow_reader" && *metadata.level() == Level::DEBUG {
950                Interest::always()
951            } else {
952                Interest::sometimes()
953            }
954        }
955
956        fn enabled(&self, metadata: &Metadata<'_>) -> bool {
957            metadata.target() == "delta_arrow_reader" && *metadata.level() == Level::DEBUG
958        }
959
960        fn new_span(&self, _attributes: &Attributes<'_>) -> Id {
961            Id::from_u64(1)
962        }
963
964        fn record(&self, _span: &Id, _values: &Record<'_>) {}
965
966        fn record_follows_from(&self, _span: &Id, _follows: &Id) {}
967
968        fn event(&self, event: &Event<'_>) {
969            let metadata = event.metadata();
970            assert_eq!(metadata.target(), "delta_arrow_reader");
971            let mut fields = metadata
972                .fields()
973                .iter()
974                .map(|field| (field.name().to_owned(), "<empty>".to_owned()))
975                .collect();
976            event.record(&mut FieldVisitor(&mut fields));
977            self.0.lock().expect("event lock").push(fields);
978        }
979
980        fn enter(&self, _span: &Id) {}
981
982        fn exit(&self, _span: &Id) {}
983    }
984
985    struct FieldVisitor<'fields>(&'fields mut BTreeMap<String, String>);
986
987    impl Visit for FieldVisitor<'_> {
988        fn record_debug(&mut self, field: &TracingField, value: &dyn fmt::Debug) {
989            self.0.insert(field.name().to_owned(), format!("{value:?}"));
990        }
991
992        fn record_str(&mut self, field: &TracingField, value: &str) {
993            self.0.insert(field.name().to_owned(), value.to_owned());
994        }
995
996        fn record_u64(&mut self, field: &TracingField, value: u64) {
997            self.0.insert(field.name().to_owned(), value.to_string());
998        }
999
1000        fn record_i64(&mut self, field: &TracingField, value: i64) {
1001            self.0.insert(field.name().to_owned(), value.to_string());
1002        }
1003
1004        fn record_u128(&mut self, field: &TracingField, value: u128) {
1005            self.0.insert(field.name().to_owned(), value.to_string());
1006        }
1007    }
1008
1009    struct DeltaLogTable(PathBuf);
1010
1011    impl DeltaLogTable {
1012        fn new(name: &str) -> Result<Self, Box<dyn std::error::Error>> {
1013            let nanos = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
1014            let path = Path::new("target")
1015                .join("delta-arrow-reader-tracing-tests")
1016                .join(format!("{}-{name}-{nanos}", std::process::id()));
1017            fs::create_dir_all(path.join("_delta_log"))?;
1018            fs::write(
1019                path.join("_delta_log/00000000000000000000.json"),
1020                r#"{"protocol":{"minReaderVersion":1,"minWriterVersion":2}}
1021{"metaData":{"id":"tracing-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":[],"configuration":{},"createdTime":1587968585495}}
1022{"add":{"path":"secret-planning-object.parquet","partitionValues":{},"size":10,"modificationTime":1587968586000,"dataChange":true}}
1023"#,
1024            )?;
1025            Ok(Self(path))
1026        }
1027    }
1028
1029    impl Drop for DeltaLogTable {
1030        fn drop(&mut self) {
1031            let _ = fs::remove_dir_all(&self.0);
1032        }
1033    }
1034
1035    fn capture_events<T>(run: impl FnOnce() -> T) -> (T, Vec<BTreeMap<String, String>>) {
1036        let _lock = TRACING_TEST_LOCK
1037            .lock()
1038            .unwrap_or_else(std::sync::PoisonError::into_inner);
1039        let events = Arc::new(Mutex::new(Vec::new()));
1040        let subscriber = EventFields(Arc::clone(&events));
1041        TRACING_TEST_GLOBAL_SUBSCRIBER.call_once(|| {
1042            let _ = tracing::subscriber::set_global_default(EventFields::default());
1043        });
1044        let result = with_default(subscriber, || {
1045            tracing::callsite::rebuild_interest_cache();
1046            run()
1047        });
1048        tracing::callsite::rebuild_interest_cache();
1049        let captured = events
1050            .lock()
1051            .map(|events| events.clone())
1052            .unwrap_or_default();
1053        (result, captured)
1054    }
1055
1056    struct ControlledMerge {
1057        stream: DeltaBatchStream,
1058        limiter: Arc<ScanReadLimiter>,
1059        cancellation: ScanCancellation,
1060        metrics: DeltaScanMetrics,
1061        first_partition_gate: Arc<Notify>,
1062    }
1063
1064    fn schema() -> SchemaRef {
1065        Arc::new(Schema::new(vec![Field::new("id", DataType::Int32, false)]))
1066    }
1067
1068    fn batch(id: i32) -> RecordBatch {
1069        RecordBatch::try_new(schema(), vec![Arc::new(Int32Array::from(vec![id]))])
1070            .expect("valid test batch")
1071    }
1072
1073    fn batch_id(batch: &RecordBatch) -> i32 {
1074        batch
1075            .column(0)
1076            .as_any()
1077            .downcast_ref::<Int32Array>()
1078            .expect("Int32 id")
1079            .value(0)
1080    }
1081
1082    fn execution_options() -> Result<DeltaScanExecutionOptions, crate::DeltaReaderError> {
1083        DeltaScanExecutionOptions::new()
1084            .with_prefetch_files_per_partition(0)
1085            .with_max_concurrent_file_reads_per_partition(1)?
1086            .with_max_concurrent_file_reads_per_scan(Some(2))?
1087            .with_output_buffer_batches_per_partition(1)
1088    }
1089
1090    fn metrics() -> DeltaScanMetrics {
1091        DeltaScanMetrics::new(DeltaScanMetricsConfig {
1092            snapshot_version: 7,
1093            parquet_backend: ParquetReaderBackend::Direct,
1094            scan_partitions_planned: 2,
1095            files_planned: 2,
1096            add_actions_excluded_during_planning: Some(0),
1097            estimated_input_rows: Some(4),
1098            estimated_input_bytes: Some(4),
1099        })
1100    }
1101
1102    fn file_stream(permit: FileReadPermit, batches: Vec<RecordBatch>) -> FileBatchStream {
1103        Box::pin(stream::unfold(
1104            (VecDeque::from(batches), permit),
1105            |(mut batches, permit)| async move {
1106                batches
1107                    .pop_front()
1108                    .map(|batch| (Ok(batch), (batches, permit)))
1109            },
1110        ))
1111    }
1112
1113    fn gated_file_stream(
1114        permit: FileReadPermit,
1115        batches: Vec<RecordBatch>,
1116        gate: Arc<Notify>,
1117    ) -> FileBatchStream {
1118        Box::pin(stream::unfold(
1119            (false, VecDeque::from(batches), permit, gate),
1120            |(wait, mut batches, permit, gate)| async move {
1121                let batch = batches.pop_front()?;
1122                if wait {
1123                    gate.notified().await;
1124                }
1125                Some((Ok(batch), (true, batches, permit, gate)))
1126            },
1127        ))
1128    }
1129
1130    fn direct_stream(
1131        partitions: VecDeque<PartitionStream>,
1132        metrics: DeltaScanMetrics,
1133        scan_capacity: usize,
1134    ) -> DeltaBatchStream {
1135        DeltaBatchStream {
1136            schema: schema(),
1137            metrics,
1138            partitions: OrderedPartitionStream::new(partitions, scan_capacity),
1139            predicate: None,
1140            projection: None,
1141            remaining: None,
1142            snapshot_version: 7,
1143            backend: ParquetReaderBackend::Direct,
1144            partition_count: 2,
1145            started: false,
1146            done: false,
1147        }
1148    }
1149
1150    fn controlled_merge() -> Result<ControlledMerge, Box<dyn std::error::Error>> {
1151        let options = execution_options()?;
1152        let limiter = ScanReadLimiter::new(options, 2, 2);
1153        let cancellation = ScanCancellation::new();
1154        let metrics = metrics();
1155        let first_partition_gate = Arc::new(Notify::new());
1156        let executor: FileExecutor<i32, FileBatchStream> = {
1157            let gate = Arc::clone(&first_partition_gate);
1158            Arc::new(move |task, permit, _| {
1159                let gate = Arc::clone(&gate);
1160                async move {
1161                    let batches = vec![batch(task), batch(task * 2)];
1162                    Ok(if task == 1 {
1163                        gated_file_stream(permit, batches, gate)
1164                    } else {
1165                        file_stream(permit, batches)
1166                    })
1167                }
1168                .boxed()
1169            })
1170        };
1171        let admission: FileAdmissionPolicy<i32> =
1172            Arc::new(|_: &i32| Ok(FileAdmissionDecision::Admit));
1173        let first = PartitionStream::new(
1174            vec![1],
1175            limiter.partition(0)?,
1176            options,
1177            admission.clone(),
1178            Arc::clone(&executor),
1179            metrics.clone(),
1180            cancellation.clone(),
1181        );
1182        let second = PartitionStream::new(
1183            vec![10],
1184            limiter.partition(1)?,
1185            options,
1186            admission,
1187            executor,
1188            metrics.clone(),
1189            cancellation.clone(),
1190        );
1191
1192        Ok(ControlledMerge {
1193            stream: direct_stream(VecDeque::from([first, second]), metrics.clone(), 2),
1194            limiter,
1195            cancellation,
1196            metrics,
1197            first_partition_gate,
1198        })
1199    }
1200
1201    async fn wait_for_batches(metrics: &DeltaScanMetrics, expected: u64) {
1202        timeout(Duration::from_secs(5), async {
1203            while metrics.snapshot().scheduler_batches_emitted < expected {
1204                tokio::task::yield_now().await;
1205            }
1206        })
1207        .await
1208        .expect("batch production reached expected bound");
1209    }
1210
1211    #[test]
1212    fn lifecycle_tracing_has_only_bounded_fields() {
1213        let error = InvalidConfigurationSnafu { reason: "test" }.build();
1214
1215        let (_, events) = capture_events(|| {
1216            trace_planning_started(7, ParquetReaderBackend::Direct, "query_planning_cache");
1217            trace_planning_completed(7, ParquetReaderBackend::Direct, 2, "query_planning_cache");
1218            trace_planning_failed(
1219                7,
1220                ParquetReaderBackend::Direct,
1221                "query_planning_cache",
1222                &error,
1223            );
1224            trace_execution_started(7, ParquetReaderBackend::Direct, 2);
1225            trace_execution_completed(7, ParquetReaderBackend::Direct, 2);
1226            trace_execution_failed(7, ParquetReaderBackend::Direct, 2, &error);
1227            trace_execution_dropped(7, ParquetReaderBackend::Direct, 2);
1228        });
1229
1230        assert_eq!(events.len(), 7);
1231        let allowed = [
1232            "backend",
1233            "error_phase",
1234            "error_code",
1235            "event",
1236            "outcome",
1237            "partition_count",
1238            "scan_metadata_source",
1239            "snapshot_version",
1240        ];
1241        for fields in events.iter() {
1242            assert!(fields.keys().all(|field| allowed.contains(&field.as_str())));
1243            assert!(fields.contains_key("event"));
1244            assert!(fields.contains_key("snapshot_version"));
1245            assert!(fields.contains_key("backend"));
1246            assert!(fields.contains_key("partition_count"));
1247            assert!(fields.contains_key("outcome"));
1248            if fields
1249                .get("event")
1250                .is_some_and(|event| event.starts_with("scan_planning."))
1251            {
1252                assert_eq!(
1253                    fields.get("scan_metadata_source").map(String::as_str),
1254                    Some("query_planning_cache")
1255                );
1256            }
1257        }
1258    }
1259
1260    #[test]
1261    fn planning_tracing_reports_the_table_metadata_source_on_success_and_failure()
1262    -> Result<(), Box<dyn std::error::Error>> {
1263        const OBJECT_KEY: &str = "secret-planning-object.parquet";
1264        const STORAGE_VALUE: &str = "secret-planning-storage-value";
1265        let fixture = DeltaLogTable::new("planning-source")?;
1266        let mut storage_options = DeltaStorageOptions::new();
1267        storage_options.insert("secret-option".to_owned(), STORAGE_VALUE.to_owned());
1268        let snapshot = load_delta_table_snapshot_blocking(
1269            &fixture.0.to_string_lossy(),
1270            &storage_options,
1271            DeltaSnapshotSelection::Latest,
1272        )?;
1273        let eager_snapshot = snapshot.clone().materialize_eager_scan_metadata()?;
1274        let lazy = DeltaTable::new(snapshot, DeltaScanExecutionOptions::new());
1275        let eager = DeltaTable::new(eager_snapshot, DeltaScanExecutionOptions::new());
1276        let runtime = tokio::runtime::Builder::new_current_thread()
1277            .enable_all()
1278            .build()?;
1279
1280        let (eager_result, eager_events) =
1281            capture_events(|| runtime.block_on(eager.scan().build()));
1282        let _ = eager_result?;
1283        assert_eq!(eager_events.len(), 2);
1284        assert_eq!(
1285            eager_events[0].get("event").map(String::as_str),
1286            Some("scan_planning.started")
1287        );
1288        assert_eq!(
1289            eager_events[1].get("event").map(String::as_str),
1290            Some("scan_planning.completed")
1291        );
1292        assert!(eager_events.iter().all(|event| {
1293            event.get("scan_metadata_source").map(String::as_str) == Some("query_planning_cache")
1294        }));
1295
1296        let (failed_result, failed_events) =
1297            capture_events(|| runtime.block_on(eager.scan().with_projection(["missing"]).build()));
1298        assert!(failed_result.is_err());
1299        assert_eq!(failed_events.len(), 2);
1300        assert_eq!(
1301            failed_events[0].get("event").map(String::as_str),
1302            Some("scan_planning.started")
1303        );
1304        assert_eq!(
1305            failed_events[1].get("event").map(String::as_str),
1306            Some("scan_planning.failed")
1307        );
1308        assert!(failed_events.iter().all(|event| {
1309            event.get("scan_metadata_source").map(String::as_str) == Some("query_planning_cache")
1310        }));
1311
1312        let (lazy_result, lazy_events) = capture_events(|| runtime.block_on(lazy.scan().build()));
1313        let _ = lazy_result?;
1314        assert_eq!(lazy_events.len(), 2);
1315        assert!(lazy_events.iter().all(|event| {
1316            event.get("scan_metadata_source").map(String::as_str) == Some("delta_log")
1317        }));
1318
1319        let captured = format!("{eager_events:?}{failed_events:?}{lazy_events:?}");
1320        assert!(!captured.contains(&fixture.0.to_string_lossy().into_owned()));
1321        assert!(!captured.contains(OBJECT_KEY));
1322        assert!(!captured.contains(STORAGE_VALUE));
1323        Ok(())
1324    }
1325
1326    #[tokio::test]
1327    async fn merged_stream_stops_without_admitting_waiting_partitions()
1328    -> Result<(), Box<dyn std::error::Error>> {
1329        for stop in ["limit", "drop", "error"] {
1330            let options = execution_options()?.with_max_concurrent_file_reads_per_scan(Some(1))?;
1331            let limiter = ScanReadLimiter::new(options, 3, 3);
1332            let cancellation = ScanCancellation::new();
1333            let metrics = metrics();
1334            let executor: FileExecutor<i32, FileBatchStream> = Arc::new(move |task, permit, _| {
1335                async move {
1336                    Ok(if stop == "error" {
1337                        Box::pin(stream::once(async move {
1338                            let _permit = permit;
1339                            Err(InvalidConfigurationSnafu {
1340                                reason: "controlled_partition_failure",
1341                            }
1342                            .build())
1343                        })) as FileBatchStream
1344                    } else {
1345                        gated_file_stream(permit, vec![batch(task); 3], Arc::new(Notify::new()))
1346                    })
1347                }
1348                .boxed()
1349            });
1350            let partitions = (0..3)
1351                .map(|index| {
1352                    Ok(PartitionStream::new(
1353                        vec![index as i32; 2],
1354                        limiter.partition(index)?,
1355                        options,
1356                        Arc::new(|_| Ok(FileAdmissionDecision::Admit)),
1357                        Arc::clone(&executor),
1358                        metrics.clone(),
1359                        cancellation.clone(),
1360                    ))
1361                })
1362                .collect::<Result<VecDeque<_>, crate::DeltaReaderError>>()?;
1363            let mut stream = direct_stream(partitions, metrics.clone(), 1);
1364            if stop == "limit" {
1365                stream.remaining = Some(1);
1366            }
1367            let first = timeout(Duration::from_secs(5), stream.next())
1368                .await?
1369                .ok_or("missing result")?;
1370            if stop == "error" {
1371                assert_eq!(
1372                    first.expect_err("controlled failure").code(),
1373                    "invalid_configuration"
1374                );
1375            } else {
1376                assert_eq!(batch_id(&first?), 0);
1377            }
1378            if stop != "drop" {
1379                assert!(stream.next().await.is_none());
1380            }
1381            drop(stream);
1382            timeout(Duration::from_secs(5), async {
1383                while limiter.active_file_reads() != 0 {
1384                    tokio::task::yield_now().await;
1385                }
1386            })
1387            .await?;
1388            assert!(cancellation.is_cancelled());
1389            assert_eq!(metrics.snapshot().file_tasks_started, 1);
1390            assert_eq!(metrics.snapshot().scan_partitions_started, 1);
1391        }
1392        Ok(())
1393    }
1394
1395    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1396    async fn merged_stream_progresses_with_small_scan_read_limits()
1397    -> Result<(), Box<dyn std::error::Error>> {
1398        for backend in [
1399            ParquetReaderBackend::Direct,
1400            ParquetReaderBackend::DeltaKernel,
1401        ] {
1402            for cap in [1, 2, 4, 8] {
1403                for prefetch in [0, 2, usize::MAX] {
1404                    let options = DeltaScanExecutionOptions::new()
1405                        .with_parquet_backend(backend)
1406                        .with_max_concurrent_file_reads_per_scan(Some(cap))?
1407                        .with_prefetch_files_per_partition(prefetch);
1408                    let limiter = ScanReadLimiter::new(options, 5, 5);
1409                    let cancellation = ScanCancellation::new();
1410                    let metrics = metrics();
1411                    let executor: FileExecutor<i32, FileBatchStream> =
1412                        Arc::new(|task, permit, _| {
1413                            async move {
1414                                // Multiple batches fill later partitions' output queues while
1415                                // the front partition still needs permits for subsequent files.
1416                                Ok(file_stream(permit, vec![batch(task); 3]))
1417                            }
1418                            .boxed()
1419                        });
1420                    let partitions = (0..5)
1421                        .map(|partition| {
1422                            Ok(PartitionStream::new(
1423                                (partition * 4..partition * 4 + 4)
1424                                    .map(|id| id as i32)
1425                                    .collect(),
1426                                limiter.partition(partition)?,
1427                                options,
1428                                Arc::new(|_| Ok(FileAdmissionDecision::Admit)),
1429                                Arc::clone(&executor),
1430                                metrics.clone(),
1431                                cancellation.clone(),
1432                            ))
1433                        })
1434                        .collect::<Result<VecDeque<_>, crate::DeltaReaderError>>()?;
1435                    let mut stream = direct_stream(partitions, metrics.clone(), cap);
1436                    let ids = timeout(Duration::from_secs(2), async {
1437                        let mut ids = Vec::new();
1438                        while let Some(batch) = stream.next().await {
1439                            ids.push(batch_id(&batch?));
1440                            assert!(limiter.active_file_reads() <= cap);
1441                            tokio::task::yield_now().await;
1442                        }
1443                        Ok::<_, crate::DeltaReaderError>(ids)
1444                    })
1445                    .await
1446                    .unwrap_or_else(|_| {
1447                        panic!("stalled: {backend:?}, cap={cap}, prefetch={prefetch}")
1448                    })?;
1449                    assert_eq!(ids, (0..20).flat_map(|id| [id; 3]).collect::<Vec<_>>());
1450                    assert_eq!(limiter.active_file_reads(), 0);
1451                    assert_eq!(metrics.snapshot().file_tasks_completed, 20);
1452                    assert_eq!(metrics.snapshot().scan_partitions_completed, 5);
1453                }
1454            }
1455        }
1456        Ok(())
1457    }
1458
1459    #[tokio::test]
1460    async fn merged_stream_is_ordered_and_bounds_later_partition_queues()
1461    -> Result<(), Box<dyn std::error::Error>> {
1462        let ControlledMerge {
1463            mut stream,
1464            limiter,
1465            metrics,
1466            first_partition_gate,
1467            ..
1468        } = controlled_merge()?;
1469
1470        let first = stream.next().await.ok_or("first batch missing")??;
1471        assert_eq!(batch_id(&first), 1);
1472        wait_for_batches(&metrics, 2).await;
1473        for _ in 0..32 {
1474            tokio::task::yield_now().await;
1475        }
1476        assert_eq!(metrics.snapshot().scheduler_batches_emitted, 2);
1477        assert_eq!(limiter.active_file_reads(), 2);
1478
1479        first_partition_gate.notify_one();
1480        let mut ids = vec![batch_id(
1481            &stream.next().await.ok_or("second batch missing")??,
1482        )];
1483        while let Some(batch) = stream.next().await {
1484            ids.push(batch_id(&batch?));
1485        }
1486        assert_eq!(ids, [2, 10, 20]);
1487        assert_eq!(metrics.snapshot().scheduler_batches_emitted, 4);
1488        assert_eq!(metrics.snapshot().scan_partitions_completed, 2);
1489        assert_eq!(limiter.active_file_reads(), 0);
1490        Ok(())
1491    }
1492
1493    #[tokio::test]
1494    async fn merged_stream_drop_cancels_blocked_partitions_and_releases_permits()
1495    -> Result<(), Box<dyn std::error::Error>> {
1496        let ControlledMerge {
1497            mut stream,
1498            limiter,
1499            cancellation,
1500            metrics,
1501            ..
1502        } = controlled_merge()?;
1503
1504        let first = stream.next().await.ok_or("first batch missing")??;
1505        assert_eq!(batch_id(&first), 1);
1506        wait_for_batches(&metrics, 2).await;
1507        assert_eq!(limiter.active_file_reads(), 2);
1508        drop(stream);
1509
1510        assert!(cancellation.is_cancelled());
1511        timeout(Duration::from_secs(5), async {
1512            while limiter.active_file_reads() != 0 {
1513                tokio::task::yield_now().await;
1514            }
1515        })
1516        .await?;
1517        assert_eq!(metrics.snapshot().scheduler_batches_emitted, 2);
1518        assert_eq!(metrics.snapshot().scan_partitions_completed, 0);
1519        Ok(())
1520    }
1521
1522    #[tokio::test]
1523    async fn merged_stream_forwards_one_concurrent_error_and_releases_permits()
1524    -> Result<(), Box<dyn std::error::Error>> {
1525        let options = execution_options()?;
1526        let limiter = ScanReadLimiter::new(options, 3, 3);
1527        let cancellation = ScanCancellation::new();
1528        let metrics = metrics();
1529        let executor: FileExecutor<i32, FileBatchStream> = Arc::new(|task, permit, _| {
1530            async move {
1531                Ok(if task == 1 {
1532                    Box::pin(stream::once(async move {
1533                        let _permit = permit;
1534                        pending::<Result<RecordBatch, crate::DeltaReaderError>>().await
1535                    })) as FileBatchStream
1536                } else {
1537                    Box::pin(stream::once(async move {
1538                        let _permit = permit;
1539                        Err(InvalidConfigurationSnafu {
1540                            reason: "controlled_partition_failure",
1541                        }
1542                        .build())
1543                    })) as FileBatchStream
1544                })
1545            }
1546            .boxed()
1547        });
1548        let admission = Arc::new(|_: &i32| Ok(FileAdmissionDecision::Admit));
1549        let first = PartitionStream::new(
1550            vec![1],
1551            limiter.partition(0)?,
1552            options,
1553            admission.clone(),
1554            Arc::clone(&executor),
1555            metrics.clone(),
1556            cancellation.clone(),
1557        );
1558        let second = PartitionStream::new(
1559            vec![2],
1560            limiter.partition(1)?,
1561            options,
1562            admission.clone(),
1563            Arc::clone(&executor),
1564            metrics.clone(),
1565            cancellation.clone(),
1566        );
1567        let third = PartitionStream::new(
1568            vec![3],
1569            limiter.partition(2)?,
1570            options,
1571            admission,
1572            executor,
1573            metrics.clone(),
1574            cancellation.clone(),
1575        );
1576        let mut stream = direct_stream(VecDeque::from([first, second, third]), metrics.clone(), 2);
1577
1578        let error = timeout(Duration::from_secs(5), stream.next())
1579            .await?
1580            .ok_or("error item missing")?
1581            .expect_err("controlled partition must fail");
1582        assert_eq!(error.code(), "invalid_configuration");
1583        assert!(stream.next().await.is_none());
1584        assert!(cancellation.is_cancelled());
1585        assert_eq!(metrics.snapshot().file_tasks_started, 2);
1586        timeout(Duration::from_secs(5), async {
1587            while limiter.active_file_reads() != 0 {
1588                tokio::task::yield_now().await;
1589            }
1590        })
1591        .await?;
1592        Ok(())
1593    }
1594}