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