1use std::{
4 collections::VecDeque,
5 fmt,
6 pin::Pin,
7 sync::Arc,
8 task::{Context, Poll},
9};
10
11use arrow::{datatypes::SchemaRef, record_batch::RecordBatch};
12use futures_util::Stream;
13use snafu::ResultExt;
14
15use crate::{
16 DeltaPredicate, DeltaProtocolInfo, DeltaReadMetrics, DeltaReaderBackend, DeltaReaderError,
17 DeltaReaderExecutionOptions, DeltaSnapshotSelection, DeltaStorageOptions,
18 error::{DataFileReadSnafu, InvalidConfigurationSnafu, ScanPlanningSnafu},
19 kernel::{delta_predicate_kernel_pruning_is_exact, delta_predicate_to_kernel_pruning},
20 planning::{
21 DeltaScanPartitionTargetOptions, DeltaScanPlan, plan_scan, validate_backend_available,
22 },
23 predicate::{evaluate_predicate, referenced_columns, validate_predicate},
24 protocol::validate_protocol,
25 scheduling::{
26 DeltaScanExecution, FileAdmission, FileAdmissionFn, FileBatchStream, FileExecutor,
27 PartitionStream,
28 },
29 snapshot::{
30 LoadedDeltaTableSnapshot, StagedDeltaTableSnapshot, load_delta_table_snapshot_async,
31 load_delta_table_snapshot_blocking, load_staged_delta_table_snapshot_async,
32 load_staged_delta_table_snapshot_blocking,
33 },
34};
35
36const TRACING_TARGET: &str = "delta_arrow_reader";
37
38pub struct DeltaTableBuilder {
73 table_uri: String,
74 storage_options: DeltaStorageOptions,
75 snapshot_selection: DeltaSnapshotSelection,
76 execution_options: DeltaReaderExecutionOptions,
77}
78
79impl DeltaTableBuilder {
80 pub fn new(table_uri: impl Into<String>) -> Self {
82 Self {
83 table_uri: table_uri.into(),
84 storage_options: DeltaStorageOptions::new(),
85 snapshot_selection: DeltaSnapshotSelection::Latest,
86 execution_options: DeltaReaderExecutionOptions::new(),
87 }
88 }
89
90 pub fn with_storage_options(mut self, value: DeltaStorageOptions) -> Self {
92 self.storage_options = value;
93 self
94 }
95
96 pub const fn with_snapshot_selection(mut self, value: DeltaSnapshotSelection) -> Self {
98 self.snapshot_selection = value;
99 self
100 }
101
102 pub const fn with_execution_options(mut self, value: DeltaReaderExecutionOptions) -> Self {
104 self.execution_options = value;
105 self
106 }
107
108 pub fn load(self) -> Result<DeltaTable, DeltaReaderError> {
110 validate_direct_execution_options(self.execution_options)?;
111 let snapshot = load_delta_table_snapshot_blocking(
112 &self.table_uri,
113 &self.storage_options,
114 self.snapshot_selection,
115 )?;
116 Ok(DeltaTable::new(snapshot, self.execution_options))
117 }
118
119 pub async fn load_async(self) -> Result<DeltaTable, DeltaReaderError> {
121 validate_direct_execution_options(self.execution_options)?;
122 let snapshot = load_delta_table_snapshot_async(
123 self.table_uri,
124 self.storage_options,
125 self.snapshot_selection,
126 )
127 .await?;
128 Ok(DeltaTable::new(snapshot, self.execution_options))
129 }
130
131 pub fn load_snapshot(self) -> Result<DeltaTableSnapshot, DeltaReaderError> {
133 validate_direct_execution_options(self.execution_options)?;
134 let snapshot = load_staged_delta_table_snapshot_blocking(
135 &self.table_uri,
136 &self.storage_options,
137 self.snapshot_selection,
138 )?;
139 Ok(DeltaTableSnapshot::new(snapshot, self.execution_options))
140 }
141
142 pub async fn load_snapshot_async(self) -> Result<DeltaTableSnapshot, DeltaReaderError> {
144 validate_direct_execution_options(self.execution_options)?;
145 let snapshot = load_staged_delta_table_snapshot_async(
146 self.table_uri,
147 self.storage_options,
148 self.snapshot_selection,
149 )
150 .await?;
151 Ok(DeltaTableSnapshot::new(snapshot, self.execution_options))
152 }
153}
154
155impl fmt::Debug for DeltaTableBuilder {
156 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
157 formatter
158 .debug_struct("DeltaTableBuilder")
159 .field("table_uri", &"<redacted>")
160 .field("storage_options", &"<redacted>")
161 .field("snapshot_selection", &self.snapshot_selection)
162 .field("execution_options", &self.execution_options)
163 .finish()
164 }
165}
166
167pub struct DeltaTableSnapshot {
169 snapshot: StagedDeltaTableSnapshot,
170 execution_options: DeltaReaderExecutionOptions,
171}
172
173impl DeltaTableSnapshot {
174 fn new(
175 snapshot: StagedDeltaTableSnapshot,
176 execution_options: DeltaReaderExecutionOptions,
177 ) -> Self {
178 Self {
179 snapshot,
180 execution_options,
181 }
182 }
183
184 pub fn version(&self) -> u64 {
186 self.snapshot.version()
187 }
188
189 pub fn protocol(&self) -> &DeltaProtocolInfo {
191 self.snapshot.protocol_info()
192 }
193
194 pub fn table_uri(&self) -> &str {
198 self.snapshot.table_uri()
199 }
200
201 pub fn validate_protocol(&self) -> Result<(), DeltaReaderError> {
203 validate_protocol(self.protocol())
204 }
205
206 pub fn into_table(self) -> Result<DeltaTable, DeltaReaderError> {
208 Ok(DeltaTable::new(
209 self.snapshot.into_loaded()?,
210 self.execution_options,
211 ))
212 }
213}
214
215impl fmt::Debug for DeltaTableSnapshot {
216 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
217 formatter
218 .debug_struct("DeltaTableSnapshot")
219 .field("version", &self.version())
220 .finish_non_exhaustive()
221 }
222}
223
224#[derive(Clone)]
226pub struct DeltaTable {
227 snapshot: Arc<LoadedDeltaTableSnapshot>,
228 version: u64,
229 execution_options: DeltaReaderExecutionOptions,
230}
231
232impl DeltaTable {
233 fn new(
234 snapshot: LoadedDeltaTableSnapshot,
235 execution_options: DeltaReaderExecutionOptions,
236 ) -> Self {
237 let version = snapshot.version();
238 Self {
239 snapshot: Arc::new(snapshot),
240 version,
241 execution_options,
242 }
243 }
244
245 pub const fn version(&self) -> u64 {
247 self.version
248 }
249
250 pub fn schema(&self) -> &SchemaRef {
252 self.snapshot.schema_ref()
253 }
254
255 pub fn protocol(&self) -> &DeltaProtocolInfo {
257 self.snapshot.protocol_info()
258 }
259
260 pub fn table_uri(&self) -> &str {
264 self.snapshot.table_uri()
265 }
266
267 #[allow(dead_code)]
268 pub(crate) fn partition_columns(&self) -> &[String] {
269 self.snapshot.partition_columns()
270 }
271
272 #[allow(dead_code)]
273 pub(crate) fn snapshot(&self) -> &LoadedDeltaTableSnapshot {
274 self.snapshot.as_ref()
275 }
276
277 pub fn validate_protocol(&self) -> Result<(), DeltaReaderError> {
279 validate_protocol(self.protocol())
280 }
281
282 pub fn scan(&self) -> DeltaScanBuilder<'_> {
284 DeltaScanBuilder {
285 table: self,
286 projection: None,
287 predicate: None,
288 limit: None,
289 target_partitions: None,
290 execution_options: self.execution_options,
291 }
292 }
293}
294
295impl fmt::Debug for DeltaTable {
296 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
297 formatter
298 .debug_struct("DeltaTable")
299 .field("version", &self.version)
300 .finish_non_exhaustive()
301 }
302}
303
304pub struct DeltaScanBuilder<'table> {
306 table: &'table DeltaTable,
307 projection: Option<Vec<String>>,
308 predicate: Option<DeltaPredicate>,
309 limit: Option<usize>,
310 target_partitions: Option<usize>,
311 execution_options: DeltaReaderExecutionOptions,
312}
313
314impl<'table> DeltaScanBuilder<'table> {
315 pub fn with_projection(mut self, logical_columns: Vec<String>) -> Self {
317 self.projection = Some(logical_columns);
318 self
319 }
320
321 pub fn with_predicate(mut self, predicate: DeltaPredicate) -> Self {
323 self.predicate = Some(predicate);
324 self
325 }
326
327 pub const fn with_limit(mut self, limit: usize) -> Self {
329 self.limit = Some(limit);
330 self
331 }
332
333 pub fn with_target_partitions(mut self, value: usize) -> Result<Self, DeltaReaderError> {
335 if value == 0 {
336 return InvalidConfigurationSnafu {
337 reason: "scan_partition_target_must_be_positive",
338 }
339 .fail();
340 }
341 self.target_partitions = Some(value);
342 Ok(self)
343 }
344
345 pub fn with_execution_options(
347 mut self,
348 value: DeltaReaderExecutionOptions,
349 ) -> Result<Self, DeltaReaderError> {
350 validate_direct_execution_options(value)?;
351 self.execution_options = value;
352 Ok(self)
353 }
354
355 pub async fn build(self) -> Result<DeltaScan, DeltaReaderError> {
357 self.table.validate_protocol()?;
358 validate_direct_execution_options(self.execution_options)?;
359 if let Some(predicate) = self.predicate.as_ref() {
360 validate_predicate(predicate, self.table.schema().as_ref())?;
361 }
362
363 let snapshot_version = self.table.version();
364 let backend = self.execution_options.reader_backend();
365 trace_planning_started(snapshot_version, backend);
366 let snapshot = Arc::clone(&self.table.snapshot);
367 let projection = self.projection;
368 let predicate = self.predicate;
369 let hidden_columns = predicate
370 .as_ref()
371 .map(referenced_columns)
372 .unwrap_or_default();
373 let enforce_physical_predicate_rows = predicate
374 .as_ref()
375 .is_some_and(delta_predicate_kernel_pruning_is_exact);
376 let kernel_predicate = predicate
377 .as_ref()
378 .and_then(delta_predicate_to_kernel_pruning);
379 let include_stats = kernel_predicate.is_some();
380 let execution_options = self.execution_options;
381 let target_partitions = self.target_partitions;
382 let result = tokio::task::spawn_blocking(move || {
383 plan_scan(
384 snapshot.as_ref(),
385 projection.as_deref(),
386 &hidden_columns,
387 kernel_predicate,
388 include_stats,
389 execution_options,
390 DeltaScanPartitionTargetOptions {
391 explicit_target_partitions: target_partitions,
392 caller_target_partitions: None,
393 },
394 )
395 })
396 .await
397 .boxed()
398 .context(ScanPlanningSnafu {
399 reason: "scan_planning_task_failed",
400 })
401 .and_then(|result| result);
402
403 match result {
404 Ok(plan) => {
405 trace_planning_completed(snapshot_version, backend, plan.partitions.len());
406 Ok(DeltaScan {
407 partition_count: plan.partitions.len(),
408 plan: Arc::new(plan),
409 predicate,
410 limit: self.limit,
411 enforce_physical_predicate_rows,
412 })
413 }
414 Err(error) => {
415 trace_planning_failed(snapshot_version, backend, &error);
416 Err(error)
417 }
418 }
419 }
420}
421
422pub struct DeltaScan {
443 plan: Arc<DeltaScanPlan>,
444 predicate: Option<DeltaPredicate>,
445 limit: Option<usize>,
446 partition_count: usize,
447 enforce_physical_predicate_rows: bool,
448}
449
450impl DeltaScan {
451 pub fn schema(&self) -> &SchemaRef {
453 &self.plan.projected_schema
454 }
455
456 pub const fn partition_count(&self) -> usize {
458 self.partition_count
459 }
460
461 pub async fn execute(self) -> Result<DeltaBatchStream, DeltaReaderError> {
463 let metrics = self.plan.metrics.clone();
464 let schema = Arc::clone(&self.plan.projected_schema);
465 let partition_count = self.plan.partitions.len();
466 let snapshot_version = self.plan.snapshot_version;
467 let backend = self.plan.execution_options.reader_backend();
468 let projection = (self.plan.logical_schema.as_ref() != schema.as_ref())
469 .then(|| (0..schema.fields().len()).collect::<Vec<_>>());
470 let mut partitions = VecDeque::new();
471
472 if self.limit != Some(0) {
473 let execution = DeltaScanExecution::new(Arc::clone(&self.plan));
474 let admission: FileAdmissionFn<_> = Arc::new(|_| Ok(FileAdmission::Admit));
475 let executor = match backend {
476 DeltaReaderBackend::NativeAsync => native_async_executor(
477 &self.plan,
478 None,
479 self.enforce_physical_predicate_rows
480 .then(|| self.plan.physical_predicate.clone())
481 .flatten(),
482 )?,
483 DeltaReaderBackend::OfficialKernel => official_kernel_executor(&self.plan)?,
484 };
485 for partition in 0..partition_count {
486 partitions.push_back(execution.partition_stream(
487 partition,
488 Arc::clone(&admission),
489 Arc::clone(&executor),
490 )?);
491 }
492 }
493
494 Ok(DeltaBatchStream {
495 schema,
496 metrics,
497 partitions,
498 predicate: self.predicate,
499 projection,
500 remaining: self.limit,
501 snapshot_version,
502 backend,
503 partition_count,
504 started: false,
505 done: false,
506 })
507 }
508}
509
510pub struct DeltaBatchStream {
523 schema: SchemaRef,
524 metrics: DeltaReadMetrics,
525 partitions: VecDeque<PartitionStream>,
526 predicate: Option<DeltaPredicate>,
527 projection: Option<Vec<usize>>,
528 remaining: Option<usize>,
529 snapshot_version: u64,
530 backend: DeltaReaderBackend,
531 partition_count: usize,
532 started: bool,
533 done: bool,
534}
535
536impl DeltaBatchStream {
537 pub fn schema(&self) -> &SchemaRef {
539 &self.schema
540 }
541
542 pub fn metrics(&self) -> DeltaReadMetrics {
544 self.metrics.clone()
545 }
546
547 fn start(&mut self) {
548 if self.started {
549 return;
550 }
551 self.started = true;
552 trace_execution_started(self.snapshot_version, self.backend, self.partition_count);
553 for partition in &mut self.partitions {
554 partition.start();
555 }
556 }
557
558 fn complete(&mut self) {
559 if self.done {
560 return;
561 }
562 self.partitions.clear();
563 self.done = true;
564 trace_execution_completed(self.snapshot_version, self.backend, self.partition_count);
565 }
566
567 fn fail(&mut self, error: &DeltaReaderError) {
568 self.partitions.clear();
569 self.done = true;
570 trace_execution_failed(
571 self.snapshot_version,
572 self.backend,
573 self.partition_count,
574 error,
575 );
576 }
577
578 fn finalize_batch(&self, mut batch: RecordBatch) -> Result<RecordBatch, DeltaReaderError> {
579 if let Some(predicate) = self.predicate.as_ref() {
580 batch = evaluate_predicate(&batch, predicate)?;
581 }
582 if let Some(projection) = self.projection.as_ref() {
583 batch = batch
584 .project(projection)
585 .boxed()
586 .context(DataFileReadSnafu {
587 reason: "direct_projection_failed",
588 })?;
589 }
590 Ok(batch)
591 }
592}
593
594impl Stream for DeltaBatchStream {
595 type Item = Result<RecordBatch, DeltaReaderError>;
596
597 fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
598 let this = self.get_mut();
599 if this.done {
600 return Poll::Ready(None);
601 }
602 this.start();
603
604 loop {
605 let Some(partition) = this.partitions.front_mut() else {
606 this.complete();
607 return Poll::Ready(None);
608 };
609 match Pin::new(partition).poll_next(context) {
610 Poll::Ready(Some(Ok(batch))) => {
611 let mut batch = match this.finalize_batch(batch) {
612 Ok(batch) => batch,
613 Err(error) => {
614 this.fail(&error);
615 return Poll::Ready(Some(Err(error)));
616 }
617 };
618 if let Some(remaining) = this.remaining.as_mut() {
619 if batch.num_rows() >= *remaining {
620 batch = batch.slice(0, *remaining);
621 *remaining = 0;
622 this.complete();
623 } else {
624 *remaining -= batch.num_rows();
625 }
626 }
627 return Poll::Ready(Some(Ok(batch)));
628 }
629 Poll::Ready(Some(Err(error))) => {
630 this.fail(&error);
631 return Poll::Ready(Some(Err(error)));
632 }
633 Poll::Ready(None) => {
634 this.partitions.pop_front();
635 }
636 Poll::Pending => return Poll::Pending,
637 }
638 }
639 }
640}
641
642impl Drop for DeltaBatchStream {
643 fn drop(&mut self) {
644 if self.done {
645 return;
646 }
647 self.partitions.clear();
648 self.done = true;
649 trace_execution_dropped(self.snapshot_version, self.backend, self.partition_count);
650 }
651}
652
653fn validate_direct_execution_options(
654 options: DeltaReaderExecutionOptions,
655) -> Result<(), DeltaReaderError> {
656 options.validate()?;
657 validate_backend_available(options)?;
658 Ok(())
659}
660
661#[cfg(feature = "native-async")]
662pub(crate) fn native_async_executor(
663 plan: &Arc<DeltaScanPlan>,
664 output_batch_size: Option<usize>,
665 row_predicate: Option<crate::kernel::DeltaKernelPredicate>,
666) -> Result<FileExecutor<crate::planning::DeltaScanFileTask, FileBatchStream>, DeltaReaderError> {
667 Ok(crate::native_async_reader::native_async_file_executor(
668 plan,
669 output_batch_size,
670 row_predicate,
671 ))
672}
673
674#[cfg(not(feature = "native-async"))]
675pub(crate) fn native_async_executor(
676 _plan: &Arc<DeltaScanPlan>,
677 _output_batch_size: Option<usize>,
678 _row_predicate: Option<crate::kernel::DeltaKernelPredicate>,
679) -> Result<FileExecutor<crate::planning::DeltaScanFileTask, FileBatchStream>, DeltaReaderError> {
680 crate::error::UnsupportedBackendSnafu {
681 reason: "native_async_feature_disabled",
682 }
683 .fail()
684}
685
686#[cfg(feature = "official-kernel")]
687pub(crate) fn official_kernel_executor(
688 plan: &Arc<DeltaScanPlan>,
689) -> Result<FileExecutor<crate::planning::DeltaScanFileTask, FileBatchStream>, DeltaReaderError> {
690 Ok(crate::official_kernel_reader::official_kernel_file_executor(plan))
691}
692
693#[cfg(not(feature = "official-kernel"))]
694pub(crate) fn official_kernel_executor(
695 _plan: &Arc<DeltaScanPlan>,
696) -> Result<FileExecutor<crate::planning::DeltaScanFileTask, FileBatchStream>, DeltaReaderError> {
697 crate::error::UnsupportedBackendSnafu {
698 reason: "official_kernel_feature_disabled",
699 }
700 .fail()
701}
702
703fn trace_planning_started(snapshot_version: u64, backend: DeltaReaderBackend) {
704 tracing::debug!(
705 target: TRACING_TARGET,
706 event = "scan_planning.started",
707 snapshot_version,
708 backend = ?backend,
709 partition_count = tracing::field::Empty,
710 outcome = "started"
711 );
712}
713
714fn trace_planning_completed(
715 snapshot_version: u64,
716 backend: DeltaReaderBackend,
717 partition_count: usize,
718) {
719 tracing::debug!(
720 target: TRACING_TARGET,
721 event = "scan_planning.completed",
722 snapshot_version,
723 backend = ?backend,
724 partition_count,
725 outcome = "completed"
726 );
727}
728
729fn trace_planning_failed(
730 snapshot_version: u64,
731 backend: DeltaReaderBackend,
732 error: &DeltaReaderError,
733) {
734 tracing::debug!(
735 target: TRACING_TARGET,
736 event = "scan_planning.failed",
737 snapshot_version,
738 backend = ?backend,
739 partition_count = tracing::field::Empty,
740 outcome = "failed",
741 error_variant = error.as_str(),
742 error_phase = error.phase().as_str()
743 );
744}
745
746fn trace_execution_started(
747 snapshot_version: u64,
748 backend: DeltaReaderBackend,
749 partition_count: usize,
750) {
751 tracing::debug!(
752 target: TRACING_TARGET,
753 event = "scan_execution.started",
754 snapshot_version,
755 backend = ?backend,
756 partition_count,
757 outcome = "started"
758 );
759}
760
761fn trace_execution_completed(
762 snapshot_version: u64,
763 backend: DeltaReaderBackend,
764 partition_count: usize,
765) {
766 tracing::debug!(
767 target: TRACING_TARGET,
768 event = "scan_execution.completed",
769 snapshot_version,
770 backend = ?backend,
771 partition_count,
772 outcome = "completed"
773 );
774}
775
776fn trace_execution_failed(
777 snapshot_version: u64,
778 backend: DeltaReaderBackend,
779 partition_count: usize,
780 error: &DeltaReaderError,
781) {
782 tracing::debug!(
783 target: TRACING_TARGET,
784 event = "scan_execution.failed",
785 snapshot_version,
786 backend = ?backend,
787 partition_count,
788 outcome = "failed",
789 error_variant = error.as_str(),
790 error_phase = error.phase().as_str()
791 );
792}
793
794fn trace_execution_dropped(
795 snapshot_version: u64,
796 backend: DeltaReaderBackend,
797 partition_count: usize,
798) {
799 tracing::debug!(
800 target: TRACING_TARGET,
801 event = "scan_execution.dropped",
802 snapshot_version,
803 backend = ?backend,
804 partition_count,
805 outcome = "dropped"
806 );
807}
808
809#[cfg(test)]
810mod tests {
811 use std::{
812 collections::VecDeque,
813 future::pending,
814 sync::{Arc, Mutex},
815 time::Duration,
816 };
817
818 use arrow::{
819 array::Int32Array,
820 datatypes::{DataType, Field, Schema, SchemaRef},
821 record_batch::RecordBatch,
822 };
823 use futures_util::{FutureExt, StreamExt, stream};
824 use tokio::{sync::Notify, time::timeout};
825 use tracing::{
826 Event, Level, Metadata, Subscriber,
827 span::{Attributes, Id, Record},
828 subscriber::{Interest, with_default},
829 };
830
831 use super::{
832 DeltaBatchStream, trace_execution_completed, trace_execution_dropped,
833 trace_execution_failed, trace_execution_started, trace_planning_completed,
834 trace_planning_failed, trace_planning_started,
835 };
836 use crate::{
837 DeltaReadMetrics, DeltaReaderBackend, DeltaReaderExecutionOptions,
838 error::InvalidConfigurationSnafu,
839 metrics::DeltaReadMetricsConfig,
840 scheduling::{
841 FileAdmission, FileAdmissionFn, FileBatchStream, FileExecutor, FileReadPermit,
842 PartitionStream, ScanCancellation, ScanReadLimiter,
843 },
844 };
845
846 #[derive(Clone, Default)]
847 struct EventFields(Arc<Mutex<Vec<Vec<String>>>>);
848
849 impl Subscriber for EventFields {
850 fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest {
851 if metadata.target() == "delta_arrow_reader" && *metadata.level() == Level::DEBUG {
852 Interest::always()
853 } else {
854 Interest::sometimes()
855 }
856 }
857
858 fn enabled(&self, metadata: &Metadata<'_>) -> bool {
859 metadata.target() == "delta_arrow_reader" && *metadata.level() == Level::DEBUG
860 }
861
862 fn new_span(&self, _attributes: &Attributes<'_>) -> Id {
863 Id::from_u64(1)
864 }
865
866 fn record(&self, _span: &Id, _values: &Record<'_>) {}
867
868 fn record_follows_from(&self, _span: &Id, _follows: &Id) {}
869
870 fn event(&self, event: &Event<'_>) {
871 let metadata = event.metadata();
872 assert_eq!(metadata.target(), "delta_arrow_reader");
873 self.0.lock().expect("event lock").push(
874 metadata
875 .fields()
876 .iter()
877 .map(|field| field.name().to_owned())
878 .collect(),
879 );
880 }
881
882 fn enter(&self, _span: &Id) {}
883
884 fn exit(&self, _span: &Id) {}
885 }
886
887 struct ControlledMerge {
888 stream: DeltaBatchStream,
889 limiter: Arc<ScanReadLimiter>,
890 cancellation: ScanCancellation,
891 metrics: DeltaReadMetrics,
892 first_partition_gate: Arc<Notify>,
893 }
894
895 fn schema() -> SchemaRef {
896 Arc::new(Schema::new(vec![Field::new("id", DataType::Int32, false)]))
897 }
898
899 fn batch(id: i32) -> RecordBatch {
900 RecordBatch::try_new(schema(), vec![Arc::new(Int32Array::from(vec![id]))])
901 .expect("valid test batch")
902 }
903
904 fn batch_id(batch: &RecordBatch) -> i32 {
905 batch
906 .column(0)
907 .as_any()
908 .downcast_ref::<Int32Array>()
909 .expect("Int32 id")
910 .value(0)
911 }
912
913 fn execution_options() -> Result<DeltaReaderExecutionOptions, crate::DeltaReaderError> {
914 DeltaReaderExecutionOptions::new()
915 .with_native_async_prefetch_file_count_per_partition(0)?
916 .with_max_concurrent_file_reads_per_partition(1)?
917 .with_max_concurrent_file_reads_per_scan(Some(2))?
918 .with_output_buffer_capacity_per_partition(1)
919 }
920
921 fn metrics() -> DeltaReadMetrics {
922 DeltaReadMetrics::new(DeltaReadMetricsConfig {
923 snapshot_version: 7,
924 reader_backend: DeltaReaderBackend::NativeAsync,
925 scan_metadata_exhausted: Some(true),
926 scan_partitions_planned: 2,
927 files_planned: 2,
928 files_filtered_during_planning: Some(0),
929 estimated_rows: Some(4),
930 estimated_bytes: Some(4),
931 })
932 }
933
934 fn file_stream(permit: FileReadPermit, batches: Vec<RecordBatch>) -> FileBatchStream {
935 Box::pin(stream::unfold(
936 (VecDeque::from(batches), permit),
937 |(mut batches, permit)| async move {
938 batches
939 .pop_front()
940 .map(|batch| (Ok(batch), (batches, permit)))
941 },
942 ))
943 }
944
945 fn gated_file_stream(
946 permit: FileReadPermit,
947 batches: Vec<RecordBatch>,
948 gate: Arc<Notify>,
949 ) -> FileBatchStream {
950 Box::pin(stream::unfold(
951 (false, VecDeque::from(batches), permit, gate),
952 |(wait, mut batches, permit, gate)| async move {
953 let batch = batches.pop_front()?;
954 if wait {
955 gate.notified().await;
956 }
957 Some((Ok(batch), (true, batches, permit, gate)))
958 },
959 ))
960 }
961
962 fn direct_stream(
963 partitions: VecDeque<PartitionStream>,
964 metrics: DeltaReadMetrics,
965 ) -> DeltaBatchStream {
966 DeltaBatchStream {
967 schema: schema(),
968 metrics,
969 partitions,
970 predicate: None,
971 projection: None,
972 remaining: None,
973 snapshot_version: 7,
974 backend: DeltaReaderBackend::NativeAsync,
975 partition_count: 2,
976 started: false,
977 done: false,
978 }
979 }
980
981 fn controlled_merge() -> Result<ControlledMerge, Box<dyn std::error::Error>> {
982 let options = execution_options()?;
983 let limiter = ScanReadLimiter::new(options, 2, 2);
984 let cancellation = ScanCancellation::new();
985 let metrics = metrics();
986 let first_partition_gate = Arc::new(Notify::new());
987 let executor: FileExecutor<i32, FileBatchStream> = {
988 let gate = Arc::clone(&first_partition_gate);
989 Arc::new(move |task, permit, _| {
990 let gate = Arc::clone(&gate);
991 async move {
992 let batches = vec![batch(task), batch(task * 2)];
993 Ok(if task == 1 {
994 gated_file_stream(permit, batches, gate)
995 } else {
996 file_stream(permit, batches)
997 })
998 }
999 .boxed()
1000 })
1001 };
1002 let admission: FileAdmissionFn<i32> = Arc::new(|_: &i32| Ok(FileAdmission::Admit));
1003 let first = PartitionStream::new(
1004 vec![1],
1005 limiter.partition(0)?,
1006 options,
1007 admission.clone(),
1008 Arc::clone(&executor),
1009 metrics.clone(),
1010 cancellation.clone(),
1011 );
1012 let second = PartitionStream::new(
1013 vec![10],
1014 limiter.partition(1)?,
1015 options,
1016 admission,
1017 executor,
1018 metrics.clone(),
1019 cancellation.clone(),
1020 );
1021
1022 Ok(ControlledMerge {
1023 stream: direct_stream(VecDeque::from([first, second]), metrics.clone()),
1024 limiter,
1025 cancellation,
1026 metrics,
1027 first_partition_gate,
1028 })
1029 }
1030
1031 async fn wait_for_batches(metrics: &DeltaReadMetrics, expected: u64) {
1032 timeout(Duration::from_secs(5), async {
1033 while metrics.snapshot().batches_produced < expected {
1034 tokio::task::yield_now().await;
1035 }
1036 })
1037 .await
1038 .expect("batch production reached expected bound");
1039 }
1040
1041 #[test]
1042 fn lifecycle_tracing_has_only_bounded_fields() {
1043 let events = Arc::new(Mutex::new(Vec::new()));
1044 let subscriber = EventFields(Arc::clone(&events));
1045 let error = InvalidConfigurationSnafu { reason: "test" }.build();
1046
1047 let _ = tracing::subscriber::set_global_default(EventFields::default());
1048 with_default(subscriber, || {
1049 tracing::callsite::rebuild_interest_cache();
1050 trace_planning_started(7, DeltaReaderBackend::NativeAsync);
1051 trace_planning_completed(7, DeltaReaderBackend::NativeAsync, 2);
1052 trace_planning_failed(7, DeltaReaderBackend::NativeAsync, &error);
1053 trace_execution_started(7, DeltaReaderBackend::NativeAsync, 2);
1054 trace_execution_completed(7, DeltaReaderBackend::NativeAsync, 2);
1055 trace_execution_failed(7, DeltaReaderBackend::NativeAsync, 2, &error);
1056 trace_execution_dropped(7, DeltaReaderBackend::NativeAsync, 2);
1057 });
1058 tracing::callsite::rebuild_interest_cache();
1059
1060 let events = events.lock().expect("event lock");
1061 assert_eq!(events.len(), 7);
1062 let allowed = [
1063 "backend",
1064 "error_phase",
1065 "error_variant",
1066 "event",
1067 "outcome",
1068 "partition_count",
1069 "snapshot_version",
1070 ];
1071 for fields in events.iter() {
1072 assert!(fields.iter().all(|field| allowed.contains(&field.as_str())));
1073 assert!(fields.contains(&"event".to_owned()));
1074 assert!(fields.contains(&"snapshot_version".to_owned()));
1075 assert!(fields.contains(&"backend".to_owned()));
1076 assert!(fields.contains(&"partition_count".to_owned()));
1077 assert!(fields.contains(&"outcome".to_owned()));
1078 }
1079 }
1080
1081 #[tokio::test]
1082 async fn merged_stream_is_ordered_and_bounds_later_partition_queues()
1083 -> Result<(), Box<dyn std::error::Error>> {
1084 let ControlledMerge {
1085 mut stream,
1086 limiter,
1087 metrics,
1088 first_partition_gate,
1089 ..
1090 } = controlled_merge()?;
1091
1092 let first = stream.next().await.ok_or("first batch missing")??;
1093 assert_eq!(batch_id(&first), 1);
1094 wait_for_batches(&metrics, 2).await;
1095 for _ in 0..32 {
1096 tokio::task::yield_now().await;
1097 }
1098 assert_eq!(metrics.snapshot().batches_produced, 2);
1099 assert_eq!(limiter.active_file_reads(), 2);
1100
1101 first_partition_gate.notify_one();
1102 let mut ids = vec![batch_id(
1103 &stream.next().await.ok_or("second batch missing")??,
1104 )];
1105 while let Some(batch) = stream.next().await {
1106 ids.push(batch_id(&batch?));
1107 }
1108 assert_eq!(ids, [2, 10, 20]);
1109 assert_eq!(metrics.snapshot().batches_produced, 4);
1110 assert_eq!(metrics.snapshot().scan_partitions_completed, 2);
1111 assert_eq!(limiter.active_file_reads(), 0);
1112 Ok(())
1113 }
1114
1115 #[tokio::test]
1116 async fn merged_stream_drop_cancels_blocked_partitions_and_releases_permits()
1117 -> Result<(), Box<dyn std::error::Error>> {
1118 let ControlledMerge {
1119 mut stream,
1120 limiter,
1121 cancellation,
1122 metrics,
1123 ..
1124 } = controlled_merge()?;
1125
1126 let first = stream.next().await.ok_or("first batch missing")??;
1127 assert_eq!(batch_id(&first), 1);
1128 wait_for_batches(&metrics, 2).await;
1129 assert_eq!(limiter.active_file_reads(), 2);
1130 drop(stream);
1131
1132 assert!(cancellation.is_cancelled());
1133 timeout(Duration::from_secs(5), async {
1134 while limiter.active_file_reads() != 0 {
1135 tokio::task::yield_now().await;
1136 }
1137 })
1138 .await?;
1139 assert_eq!(metrics.snapshot().batches_produced, 2);
1140 assert_eq!(metrics.snapshot().scan_partitions_completed, 0);
1141 Ok(())
1142 }
1143
1144 #[tokio::test]
1145 async fn merged_stream_forwards_one_concurrent_error_and_releases_permits()
1146 -> Result<(), Box<dyn std::error::Error>> {
1147 let options = execution_options()?;
1148 let limiter = ScanReadLimiter::new(options, 2, 2);
1149 let cancellation = ScanCancellation::new();
1150 let metrics = metrics();
1151 let executor: FileExecutor<i32, FileBatchStream> = Arc::new(|task, permit, _| {
1152 async move {
1153 Ok(if task == 1 {
1154 Box::pin(stream::once(async move {
1155 let _permit = permit;
1156 pending::<Result<RecordBatch, crate::DeltaReaderError>>().await
1157 })) as FileBatchStream
1158 } else {
1159 Box::pin(stream::once(async move {
1160 let _permit = permit;
1161 Err(InvalidConfigurationSnafu {
1162 reason: "controlled_partition_failure",
1163 }
1164 .build())
1165 })) as FileBatchStream
1166 })
1167 }
1168 .boxed()
1169 });
1170 let admission = Arc::new(|_: &i32| Ok(FileAdmission::Admit));
1171 let first = PartitionStream::new(
1172 vec![1],
1173 limiter.partition(0)?,
1174 options,
1175 admission.clone(),
1176 Arc::clone(&executor),
1177 metrics.clone(),
1178 cancellation.clone(),
1179 );
1180 let second = PartitionStream::new(
1181 vec![2],
1182 limiter.partition(1)?,
1183 options,
1184 admission,
1185 executor,
1186 metrics.clone(),
1187 cancellation.clone(),
1188 );
1189 let mut stream = direct_stream(VecDeque::from([first, second]), metrics);
1190
1191 let error = timeout(Duration::from_secs(5), stream.next())
1192 .await?
1193 .ok_or("error item missing")?
1194 .expect_err("controlled partition must fail");
1195 assert_eq!(error.as_str(), "invalid_configuration");
1196 assert!(stream.next().await.is_none());
1197 assert!(cancellation.is_cancelled());
1198 timeout(Duration::from_secs(5), async {
1199 while limiter.active_file_reads() != 0 {
1200 tokio::task::yield_now().await;
1201 }
1202 })
1203 .await?;
1204 Ok(())
1205 }
1206}