Skip to main content

delta_funnel/query_engine/
datafusion.rs

1//! DataFusion integration.
2
3use std::{collections::HashSet, error::Error, fmt, sync::Arc};
4
5use datafusion::common::DataFusionError;
6use datafusion::execution::TaskContext;
7use datafusion::physical_plan::{
8    EmptyRecordBatchStream, ExecutionPlan, SendableRecordBatchStream,
9    coalesce_partitions::CoalescePartitionsExec,
10};
11use delta_arrow_reader::{
12    DeltaScanMetricsSnapshot, ParquetReaderBackend,
13    datafusion::{ScanMetrics, ScanMetricsSnapshot, collect_scan_metrics},
14};
15
16use crate::DeltaFunnelError;
17
18mod catalog;
19pub(crate) mod execution_profile;
20mod operator_activity;
21mod planning_activity;
22mod profiled_execution;
23mod session;
24
25#[cfg(feature = "perfetto-profile")]
26pub(crate) use operator_activity::initialize_datafusion_task_tracing;
27pub(crate) use operator_activity::instrument_query_execution_plan;
28pub(crate) use planning_activity::with_query_planning_activity;
29pub(crate) use profiled_execution::{
30    QueryTraceIdentity, profiled_datafusion_query_output_stream_with_effective_root,
31};
32
33pub use catalog::registration::{
34    RegisteredDeltaSource, RegisteredDeltaSources, register_delta_sources,
35    register_delta_sources_with_scan_execution_options,
36};
37pub(crate) use catalog::registration::{
38    register_delta_source_with_scan_options, reject_existing_delta_registration_name,
39    validate_delta_table_snapshot_protocol,
40};
41pub use session::{QueryOptions, datafusion_session_config, datafusion_session_context};
42
43/// Stable Delta Funnel view of one Delta reader scan's metrics.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct DeltaReadMetricsSnapshot {
46    /// Delta snapshot version selected for the scan.
47    pub snapshot_version: u64,
48    /// Parquet reader backend selected for the scan.
49    pub reader_backend: ParquetReaderBackend,
50    /// Whether planning exhausted the Delta scan metadata.
51    pub scan_metadata_exhausted: Option<bool>,
52    /// Final execution partitions planned for the scan.
53    pub scan_partitions_planned: u64,
54    /// Data files selected during planning.
55    pub files_planned: u64,
56    /// Add actions excluded during planning, when known.
57    pub files_filtered_during_planning: Option<u64>,
58    /// Estimated input rows, when known for every selected file.
59    pub estimated_rows: Option<u64>,
60    /// Estimated input bytes, when known for every selected file.
61    pub estimated_bytes: Option<u64>,
62    /// Scan partitions whose execution started.
63    pub scan_partitions_started: u64,
64    /// Scan partitions that completed normally.
65    pub scan_partitions_completed: u64,
66    /// Data-file tasks that started.
67    pub files_started: u64,
68    /// Data-file tasks that completed normally.
69    pub files_completed: u64,
70    /// Batches emitted by the Delta scheduler.
71    pub batches_produced: u64,
72    /// Rows emitted by the Delta scheduler.
73    pub rows_produced: u64,
74    /// Deletion-vector payloads loaded.
75    pub deletion_vector_payloads_loaded: u64,
76    /// Deletion-vector masks applied.
77    pub deletion_vectors_applied: u64,
78    /// Rows removed by deletion-vector masks.
79    pub deletion_vector_rows_deleted: u64,
80    /// Deletion-vector read or masking failures.
81    pub deletion_vector_failures: u64,
82    /// Deletion-vector operations rejected by safety checks.
83    pub deletion_vector_rejections: u64,
84    /// Direct Parquet ranged GET operations, when available.
85    pub parquet_data_file_range_get_operations: Option<u64>,
86    /// Direct Parquet full GET operations, when available.
87    pub parquet_data_file_full_get_operations: Option<u64>,
88    /// Direct Parquet payload bytes received, when available.
89    pub parquet_data_file_bytes_received: Option<u64>,
90    /// Estimated Parquet task bytes admitted, when available.
91    pub parquet_data_file_opened_bytes: Option<u64>,
92}
93
94/// Stable Delta Funnel view of one DataFusion Delta scan's metrics.
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct DeltaDataFusionMetricsSnapshot {
97    /// Delta reader planning and execution metrics.
98    pub reader: DeltaReadMetricsSnapshot,
99    /// Whether the provider requested Arrow view arrays.
100    pub use_view_types: bool,
101    /// Configured DataFusion batch row target observed at execution.
102    pub output_batch_size: Option<u64>,
103    /// File tasks pruned by dynamic partition filters.
104    pub dynamic_partition_files_pruned: u64,
105    /// File tasks kept after dynamic partition filtering.
106    pub dynamic_partition_files_kept: u64,
107    /// Physical filters offered to the dynamic filter hook.
108    pub dynamic_filters_received: u64,
109    /// Offered dynamic filters retained for pruning.
110    pub dynamic_filters_accepted: u64,
111    /// Offered dynamic filters rejected by policy.
112    pub dynamic_filters_unsupported: u64,
113    /// Dynamic partition filter checks during file admission.
114    pub dynamic_filter_snapshots: u64,
115    /// Kept tasks with unusable partition metadata.
116    pub dynamic_files_not_pruned_missing_metadata: u64,
117    /// Kept tasks whose dynamic filter could not be evaluated.
118    pub dynamic_files_not_pruned_unsupported_expression: u64,
119}
120
121impl From<ScanMetricsSnapshot> for DeltaDataFusionMetricsSnapshot {
122    fn from(snapshot: ScanMetricsSnapshot) -> Self {
123        Self {
124            reader: snapshot.reader_metrics.into(),
125            use_view_types: snapshot.uses_arrow_view_types,
126            output_batch_size: snapshot.configured_batch_size_rows,
127            dynamic_partition_files_pruned: snapshot.dynamic_partition_tasks_pruned,
128            dynamic_partition_files_kept: snapshot.dynamic_partition_tasks_kept,
129            dynamic_filters_received: snapshot.dynamic_filters_received,
130            dynamic_filters_accepted: snapshot.dynamic_filters_accepted,
131            dynamic_filters_unsupported: snapshot.dynamic_filters_rejected,
132            dynamic_filter_snapshots: snapshot.dynamic_partition_filter_checks,
133            dynamic_files_not_pruned_missing_metadata: snapshot
134                .dynamic_partition_tasks_kept_unusable_metadata,
135            dynamic_files_not_pruned_unsupported_expression: snapshot
136                .dynamic_partition_tasks_kept_unevaluable_filter,
137        }
138    }
139}
140
141impl From<DeltaScanMetricsSnapshot> for DeltaReadMetricsSnapshot {
142    fn from(snapshot: DeltaScanMetricsSnapshot) -> Self {
143        Self {
144            snapshot_version: snapshot.snapshot_version,
145            reader_backend: snapshot.parquet_backend,
146            scan_metadata_exhausted: Some(true),
147            scan_partitions_planned: snapshot.scan_partitions_planned,
148            files_planned: snapshot.files_planned,
149            files_filtered_during_planning: snapshot.add_actions_excluded_during_planning,
150            estimated_rows: snapshot.estimated_input_rows,
151            estimated_bytes: snapshot.estimated_input_bytes,
152            scan_partitions_started: snapshot.scan_partitions_started,
153            scan_partitions_completed: snapshot.scan_partitions_completed,
154            files_started: snapshot.file_tasks_started,
155            files_completed: snapshot.file_tasks_completed,
156            batches_produced: snapshot.scheduler_batches_emitted,
157            rows_produced: snapshot.scheduler_rows_emitted,
158            deletion_vector_payloads_loaded: snapshot.deletion_vector_payloads_loaded,
159            deletion_vectors_applied: snapshot.deletion_vectors_applied,
160            deletion_vector_rows_deleted: snapshot.deletion_vector_rows_deleted,
161            deletion_vector_failures: snapshot.deletion_vector_failures,
162            deletion_vector_rejections: snapshot.deletion_vector_coordinate_rejections,
163            parquet_data_file_range_get_operations: snapshot.parquet_data_file_range_get_operations,
164            parquet_data_file_full_get_operations: snapshot.parquet_data_file_full_get_operations,
165            parquet_data_file_bytes_received: snapshot.parquet_data_file_bytes_received,
166            parquet_data_file_opened_bytes: snapshot.estimated_parquet_task_bytes_admitted,
167        }
168    }
169}
170
171/// One standalone metrics handle for a Delta provider scan.
172#[derive(Clone)]
173pub(crate) struct DeltaProviderReadStatsHandle {
174    metrics: ScanMetrics,
175    plan_identity: usize,
176}
177
178impl DeltaProviderReadStatsHandle {
179    pub(crate) fn same_instance(&self, other: &Self) -> bool {
180        self.plan_identity == other.plan_identity
181    }
182
183    pub(crate) fn snapshot(&self) -> DeltaDataFusionMetricsSnapshot {
184        self.metrics.snapshot().into()
185    }
186
187    pub(crate) fn source_name(&self) -> Option<&str> {
188        self.metrics.registration_name()
189    }
190}
191
192pub(crate) fn delta_datafusion_metrics_for_plan(
193    plan: &dyn ExecutionPlan,
194) -> Option<DeltaProviderReadStatsHandle> {
195    let plan = operator_activity::unprofiled_execution_plan(plan);
196    if !plan.children().is_empty() {
197        return None;
198    }
199    let metrics = collect_scan_metrics(plan);
200    let [metrics] = metrics.as_slice() else {
201        return None;
202    };
203    Some(DeltaProviderReadStatsHandle {
204        metrics: metrics.clone(),
205        plan_identity: plan as *const dyn ExecutionPlan as *const () as usize,
206    })
207}
208
209impl From<DeltaFunnelError> for DataFusionError {
210    fn from(error: DeltaFunnelError) -> Self {
211        Self::External(Box::new(error))
212    }
213}
214
215/// Collects distinct shared read stats counters without retaining the physical plan.
216///
217/// Repeated references to the same metrics instance are omitted while
218/// preserving the first-seen physical-plan traversal order.
219pub(crate) fn collect_delta_provider_read_stats_handles(
220    plan: &dyn ExecutionPlan,
221) -> Vec<DeltaProviderReadStatsHandle> {
222    fn collect(
223        plan: &dyn ExecutionPlan,
224        seen: &mut HashSet<usize>,
225        found: &mut Vec<DeltaProviderReadStatsHandle>,
226    ) {
227        let plan = operator_activity::unprofiled_execution_plan(plan);
228        let plan_identity = plan as *const dyn ExecutionPlan as *const () as usize;
229        if !seen.insert(plan_identity) {
230            return;
231        }
232
233        let children = plan.children();
234        if let Some(handle) = delta_datafusion_metrics_for_plan(plan)
235            && !found.iter().any(|existing| existing.same_instance(&handle))
236        {
237            found.push(handle);
238        }
239        for child in children {
240            collect(child.as_ref(), seen, found);
241        }
242    }
243
244    let mut seen = HashSet::new();
245    let mut found = Vec::new();
246    collect(plan, &mut seen, &mut found);
247    found
248}
249
250/// Creates point-in-time snapshots from shared live read counters.
251pub(crate) fn snapshot_delta_provider_read_stats(
252    handles: &[DeltaProviderReadStatsHandle],
253) -> Vec<DeltaDataFusionMetricsSnapshot> {
254    handles.iter().map(|stats| stats.snapshot()).collect()
255}
256
257#[derive(Clone)]
258pub(crate) struct NamedDeltaDataFusionMetricsSnapshot {
259    pub(crate) source_name: String,
260    pub(crate) snapshot: DeltaDataFusionMetricsSnapshot,
261}
262
263pub(crate) fn collect_named_delta_datafusion_metrics(
264    plan: &dyn ExecutionPlan,
265) -> Vec<NamedDeltaDataFusionMetricsSnapshot> {
266    collect_scan_metrics(plan)
267        .into_iter()
268        .map(|metrics| NamedDeltaDataFusionMetricsSnapshot {
269            source_name: metrics.registration_name().unwrap_or_default().to_owned(),
270            snapshot: metrics.snapshot().into(),
271        })
272        .collect()
273}
274
275/// Executes one selected DataFusion query output as a single merged stream.
276///
277/// DataFusion physical plans can have multiple output partitions. This helper
278/// uses DataFusion's own `execute_stream` behavior, which merges those
279/// partitions into one `RecordBatch` stream while still letting partition tasks
280/// run concurrently. DeltaFunnel's downstream MSSQL writer can then stay a
281/// single awaited consumer without forcing serial partition execution.
282pub fn datafusion_query_output_stream(
283    plan: Arc<dyn ExecutionPlan>,
284    task_context: Arc<TaskContext>,
285) -> Result<SendableRecordBatchStream, DataFusionError> {
286    let DFQueryExecution {
287        stream,
288        effective_profile_root,
289    } = datafusion_query_output_stream_with_effective_root(plan, task_context)
290        .map_err(|failure| failure.source)?;
291    drop(effective_profile_root);
292    Ok(stream)
293}
294
295pub(crate) struct DFQueryExecution {
296    pub(crate) stream: SendableRecordBatchStream,
297    pub(crate) effective_profile_root: Arc<dyn ExecutionPlan>,
298}
299
300#[derive(Debug)]
301pub(crate) struct DFQueryExecutionSetupError {
302    pub(crate) source: DataFusionError,
303    pub(crate) effective_profile_root: Arc<dyn ExecutionPlan>,
304}
305
306impl fmt::Display for DFQueryExecutionSetupError {
307    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
308        self.source.fmt(formatter)
309    }
310}
311
312impl Error for DFQueryExecutionSetupError {
313    fn source(&self) -> Option<&(dyn Error + 'static)> {
314        Some(&self.source)
315    }
316}
317
318pub(crate) fn datafusion_query_output_stream_with_effective_root(
319    plan: Arc<dyn ExecutionPlan>,
320    task_context: Arc<TaskContext>,
321) -> Result<DFQueryExecution, DFQueryExecutionSetupError> {
322    let (effective_profile_root, execute) = prepare_datafusion_query_output(plan);
323    execute_datafusion_query_output(effective_profile_root, execute, task_context)
324}
325
326pub(super) fn prepare_datafusion_query_output(
327    plan: Arc<dyn ExecutionPlan>,
328) -> (Arc<dyn ExecutionPlan>, bool) {
329    // Keep these branches in sync with DataFusion's `execute_stream`.
330    match plan.properties().output_partitioning().partition_count() {
331        // DataFusion returns an empty stream without executing a partition, but
332        // profiling still needs the real planned root.
333        0 => (plan, false),
334        // The only output partition has the zero-based index 0.
335        1 => (plan, true),
336        2.. => {
337            // The wrapper exposes one output partition at index 0 and consumes
338            // every output partition from the original plan.
339            (
340                Arc::new(CoalescePartitionsExec::new(plan)) as Arc<dyn ExecutionPlan>,
341                true,
342            )
343        }
344    }
345}
346
347pub(super) fn execute_datafusion_query_output(
348    effective_profile_root: Arc<dyn ExecutionPlan>,
349    execute: bool,
350    task_context: Arc<TaskContext>,
351) -> Result<DFQueryExecution, DFQueryExecutionSetupError> {
352    let stream = if execute {
353        effective_profile_root
354            .execute(0, task_context)
355            .map_err(|source| DFQueryExecutionSetupError {
356                source,
357                effective_profile_root: Arc::clone(&effective_profile_root),
358            })?
359    } else {
360        Box::pin(EmptyRecordBatchStream::new(effective_profile_root.schema()))
361    };
362    Ok(DFQueryExecution {
363        stream,
364        effective_profile_root,
365    })
366}
367
368#[cfg(test)]
369pub(crate) mod test_support {
370    #![allow(dead_code, missing_docs)]
371
372    use std::collections::HashMap;
373    use std::fs;
374    use std::path::{Path, PathBuf};
375    use std::sync::{
376        Arc, Mutex, MutexGuard,
377        atomic::{AtomicBool, Ordering},
378    };
379    use std::time::{SystemTime, UNIX_EPOCH};
380
381    use async_trait::async_trait;
382    use datafusion::catalog::{CatalogProvider, SchemaProvider};
383    use datafusion::common::{DataFusionError, Result as DataFusionResult};
384    use datafusion::datasource::TableProvider;
385    use datafusion::prelude::SessionContext;
386    use delta_arrow_reader::{
387        DeltaTableBuilder,
388        datafusion::{ScanOptions, register_table},
389    };
390
391    pub(crate) struct DeltaLogTable {
392        path: PathBuf,
393    }
394
395    impl Drop for DeltaLogTable {
396        fn drop(&mut self) {
397            let _ = fs::remove_dir_all(&self.path);
398        }
399    }
400
401    impl DeltaLogTable {
402        pub(crate) fn new(name: &str) -> Result<Self, Box<dyn std::error::Error>> {
403            Self::new_with_schema(
404                name,
405                DEFAULT_SCHEMA_FIELDS_JSON,
406                "[]",
407                r#""partitionValues":{}"#,
408            )
409        }
410
411        pub(crate) fn new_with_schema(
412            name: &str,
413            schema_fields_json: &str,
414            partition_columns_json: &str,
415            add_partition_values_json: &str,
416        ) -> Result<Self, Box<dyn std::error::Error>> {
417            Self::new_with_schema_and_adds(
418                name,
419                schema_fields_json,
420                partition_columns_json,
421                &[add_partition_values_json],
422            )
423        }
424
425        pub(crate) fn new_with_schema_and_adds(
426            name: &str,
427            schema_fields_json: &str,
428            partition_columns_json: &str,
429            add_partition_values_jsons: &[&str],
430        ) -> Result<Self, Box<dyn std::error::Error>> {
431            Self::new_with_schema_protocol_and_adds(
432                name,
433                PROTOCOL_JSON,
434                schema_fields_json,
435                partition_columns_json,
436                add_partition_values_jsons,
437            )
438        }
439
440        pub(crate) fn new_with_schema_protocol_and_adds(
441            name: &str,
442            protocol_json: &str,
443            schema_fields_json: &str,
444            partition_columns_json: &str,
445            add_partition_values_jsons: &[&str],
446        ) -> Result<Self, Box<dyn std::error::Error>> {
447            let add_partition_values_and_sizes = add_partition_values_jsons
448                .iter()
449                .map(|partition_values_json| (*partition_values_json, 0))
450                .collect::<Vec<_>>();
451
452            Self::new_with_schema_protocol_and_sized_adds(
453                name,
454                protocol_json,
455                schema_fields_json,
456                partition_columns_json,
457                &add_partition_values_and_sizes,
458            )
459        }
460
461        pub(crate) fn new_with_schema_and_sized_adds(
462            name: &str,
463            schema_fields_json: &str,
464            partition_columns_json: &str,
465            add_partition_values_and_sizes: &[(&str, u64)],
466        ) -> Result<Self, Box<dyn std::error::Error>> {
467            Self::new_with_schema_protocol_and_sized_adds(
468                name,
469                PROTOCOL_JSON,
470                schema_fields_json,
471                partition_columns_json,
472                add_partition_values_and_sizes,
473            )
474        }
475
476        pub(crate) fn new_with_schema_protocol_and_sized_adds(
477            name: &str,
478            protocol_json: &str,
479            schema_fields_json: &str,
480            partition_columns_json: &str,
481            add_partition_values_and_sizes: &[(&str, u64)],
482        ) -> Result<Self, Box<dyn std::error::Error>> {
483            let path = Path::new("target")
484                .join("delta-funnel-datafusion-provider-tests")
485                .join(unique_name(name)?);
486            let log_path = path.join("_delta_log");
487            fs::create_dir_all(&log_path)?;
488            fs::write(
489                log_path.join("00000000000000000000.json"),
490                format!(
491                    "{}\n{}\n",
492                    protocol_json,
493                    metadata_json(schema_fields_json, partition_columns_json)
494                ),
495            )?;
496            let add_actions = add_partition_values_and_sizes
497                .iter()
498                .enumerate()
499                .map(|(index, (partition_values_json, size))| {
500                    add_json(
501                        &format!("part-{index:05}.parquet"),
502                        partition_values_json,
503                        *size,
504                    )
505                })
506                .collect::<Vec<_>>()
507                .join("\n");
508            fs::write(
509                log_path.join("00000000000000000001.json"),
510                format!("{add_actions}\n"),
511            )?;
512
513            Ok(Self { path })
514        }
515
516        pub(crate) fn path(&self) -> &Path {
517            &self.path
518        }
519    }
520
521    const PROTOCOL_JSON: &str = r#"{"protocol":{"minReaderVersion":1,"minWriterVersion":2}}"#;
522    pub(crate) const DEFAULT_SCHEMA_FIELDS_JSON: &str = r#"[{\"name\":\"id\",\"type\":\"integer\",\"nullable\":false,\"metadata\":{}},{\"name\":\"customer_name\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}]"#;
523    pub(crate) const PARTITIONED_SCHEMA_FIELDS_JSON: &str = r#"[{\"name\":\"id\",\"type\":\"integer\",\"nullable\":false,\"metadata\":{}},{\"name\":\"region\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}]"#;
524    pub(crate) const NESTED_SCHEMA_FIELDS_JSON: &str = r#"[{\"name\":\"id\",\"type\":\"integer\",\"nullable\":false,\"metadata\":{}},{\"name\":\"profile\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"age\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"tags\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}}]"#;
525    pub(crate) const DEEP_NESTED_WITH_CITY_SCHEMA_FIELDS_JSON: &str = r#"[{\"name\":\"profile\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"address\",\"type\":{\"type\":\"struct\",\"fields\":[{\"name\":\"city\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}}]},\"nullable\":true,\"metadata\":{}},{\"name\":\"city\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}]"#;
526    pub(crate) const INVALID_NESTED_IDS_SCHEMA_FIELDS_JSON: &str = r#"[{\"name\":\"bad_array\",\"type\":{\"type\":\"array\",\"elementType\":\"string\",\"containsNull\":true},\"nullable\":true,\"metadata\":{\"delta.columnMapping.nested.ids\":\"not an object\"}}]"#;
527
528    fn metadata_json(schema_fields_json: &str, partition_columns_json: &str) -> String {
529        format!(
530            r#"{{"metaData":{{"id":"delta-funnel-test","format":{{"provider":"parquet","options":{{}}}},"schemaString":"{{\"type\":\"struct\",\"fields\":{schema_fields_json}}}","partitionColumns":{partition_columns_json},"configuration":{{}},"createdTime":1587968585495}}}}"#
531        )
532    }
533
534    fn add_json(path: &str, partition_values_json: &str, size: u64) -> String {
535        format!(
536            r#"{{"add":{{"path":"{path}",{partition_values_json},"size":{size},"modificationTime":1587968586000,"dataChange":true}}}}"#
537        )
538    }
539
540    fn unique_name(name: &str) -> Result<String, Box<dyn std::error::Error>> {
541        let nanos = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
542
543        Ok(format!("{}-{}-{nanos}", std::process::id(), name))
544    }
545
546    pub(crate) async fn register_fixture_source(
547        ctx: &SessionContext,
548        source_name: &str,
549        fixture_name: &str,
550    ) -> Result<DeltaLogTable, Box<dyn std::error::Error>> {
551        let table = DeltaLogTable::new(fixture_name)?;
552        let loaded = DeltaTableBuilder::new(table.path.to_string_lossy())
553            .load_table()
554            .await?;
555        register_table(ctx, source_name, loaded, ScanOptions::default())?;
556
557        Ok(table)
558    }
559
560    #[derive(Debug, Default)]
561    pub(crate) struct FailsOnCustomersSchemaProvider {
562        tables: Mutex<HashMap<String, Arc<dyn TableProvider>>>,
563        allow_customers: AtomicBool,
564    }
565
566    impl FailsOnCustomersSchemaProvider {
567        pub(crate) fn allow_customers(&self) {
568            self.allow_customers.store(true, Ordering::Relaxed);
569        }
570
571        fn tables(&self) -> MutexGuard<'_, HashMap<String, Arc<dyn TableProvider>>> {
572            self.tables
573                .lock()
574                .unwrap_or_else(|poisoned| poisoned.into_inner())
575        }
576    }
577
578    #[async_trait]
579    impl SchemaProvider for FailsOnCustomersSchemaProvider {
580        fn table_names(&self) -> Vec<String> {
581            self.tables().keys().cloned().collect()
582        }
583
584        async fn table(
585            &self,
586            name: &str,
587        ) -> DataFusionResult<Option<Arc<dyn TableProvider>>, DataFusionError> {
588            Ok(self.tables().get(name).cloned())
589        }
590
591        fn register_table(
592            &self,
593            name: String,
594            table: Arc<dyn TableProvider>,
595        ) -> DataFusionResult<Option<Arc<dyn TableProvider>>> {
596            if name == "customers" && !self.allow_customers.load(Ordering::Relaxed) {
597                return Err(DataFusionError::Execution(
598                    "forced customers registration failure".to_owned(),
599                ));
600            }
601
602            Ok(self.tables().insert(name, table))
603        }
604
605        fn deregister_table(&self, name: &str) -> DataFusionResult<Option<Arc<dyn TableProvider>>> {
606            Ok(self.tables().remove(name))
607        }
608
609        fn table_exist(&self, name: &str) -> bool {
610            self.tables().contains_key(name)
611        }
612    }
613
614    #[derive(Debug)]
615    pub(crate) struct SingleSchemaCatalogProvider {
616        schema: Arc<dyn SchemaProvider>,
617    }
618
619    impl SingleSchemaCatalogProvider {
620        pub(crate) fn new(schema: Arc<dyn SchemaProvider>) -> Self {
621            Self { schema }
622        }
623    }
624
625    impl CatalogProvider for SingleSchemaCatalogProvider {
626        fn schema_names(&self) -> Vec<String> {
627            vec!["public".to_owned()]
628        }
629
630        fn schema(&self, name: &str) -> Option<Arc<dyn SchemaProvider>> {
631            (name == "public").then(|| Arc::clone(&self.schema))
632        }
633    }
634}
635
636#[cfg(test)]
637mod tests {
638    use std::{error::Error, sync::Arc, time::Duration};
639
640    use datafusion::{
641        arrow::{
642            array::Int32Array,
643            datatypes::{DataType, Field, Schema, SchemaRef},
644            record_batch::RecordBatch,
645        },
646        common::DataFusionError,
647        execution::TaskContext,
648        physical_plan::{
649            ExecutionPlan,
650            coalesce_partitions::CoalescePartitionsExec,
651            execute_stream,
652            test::{
653                TestMemoryExec, assert_is_pending,
654                exec::{
655                    BarrierExec, BlockingExec, ErrorExec, MockExec,
656                    assert_strong_count_converges_to_zero,
657                },
658            },
659            union::UnionExec,
660        },
661        prelude::SessionContext,
662    };
663    use futures_util::{FutureExt, StreamExt, TryStreamExt};
664
665    use super::{
666        collect_delta_provider_read_stats_handles, datafusion_query_output_stream,
667        datafusion_query_output_stream_with_effective_root, test_support::register_fixture_source,
668    };
669
670    #[tokio::test]
671    async fn read_stats_handles_deduplicate_repeated_metrics_instance() -> Result<(), Box<dyn Error>>
672    {
673        let context = SessionContext::new();
674        let _table = register_fixture_source(&context, "orders", "shared-scan-handle").await?;
675        let plan = delta_plan(&context).await?;
676        let original = collect_delta_provider_read_stats_handles(plan.as_ref());
677        let repeated_plan = UnionExec::try_new(vec![Arc::clone(&plan), plan])?;
678
679        let found = collect_delta_provider_read_stats_handles(repeated_plan.as_ref());
680
681        assert_eq!(original.len(), 1);
682        assert_eq!(found.len(), 1);
683        assert!(found[0].same_instance(&original[0]));
684        Ok(())
685    }
686
687    #[tokio::test]
688    async fn read_stats_handles_keep_distinct_metrics_in_first_seen_order()
689    -> Result<(), Box<dyn Error>> {
690        let context = SessionContext::new();
691        let _table = register_fixture_source(&context, "orders", "distinct-scan-handles").await?;
692        let first_plan = delta_plan(&context).await?;
693        let second_plan = delta_plan(&context).await?;
694        let first = collect_delta_provider_read_stats_handles(first_plan.as_ref());
695        let second = collect_delta_provider_read_stats_handles(second_plan.as_ref());
696        let combined_plan =
697            UnionExec::try_new(vec![Arc::clone(&second_plan), first_plan, second_plan])?;
698
699        let found = collect_delta_provider_read_stats_handles(combined_plan.as_ref());
700
701        assert_eq!(first.len(), 1);
702        assert_eq!(second.len(), 1);
703        assert!(!first[0].same_instance(&second[0]));
704        assert_eq!(found.len(), 2);
705        assert!(found[0].same_instance(&second[0]));
706        assert!(found[1].same_instance(&first[0]));
707        Ok(())
708    }
709
710    #[tokio::test]
711    async fn query_output_stream_effective_root_matches_datafusion_for_all_partition_counts()
712    -> Result<(), Box<dyn Error>> {
713        let schema = schema();
714        let cases: [Vec<Vec<i32>>; 3] = [vec![], vec![vec![1, 2]], vec![vec![1, 2], vec![3, 4]]];
715
716        for partition_values in cases {
717            let partitions = partition_values
718                .iter()
719                .map(|values| int_batch(Arc::clone(&schema), values).map(|batch| vec![batch]))
720                .collect::<Result<Vec<_>, _>>()?;
721            let plan: Arc<dyn ExecutionPlan> =
722                TestMemoryExec::try_new_exec(&partitions, Arc::clone(&schema), None)?;
723            let mut expected = collect_stream_batch_values(execute_stream(
724                Arc::clone(&plan),
725                Arc::new(TaskContext::default()),
726            )?)
727            .await?;
728
729            let output = datafusion_query_output_stream_with_effective_root(
730                Arc::clone(&plan),
731                Arc::new(TaskContext::default()),
732            )?;
733            let actual_schema = output.stream.schema();
734            let mut actual = collect_stream_batch_values(output.stream).await?;
735
736            if partition_values.len() < 2 {
737                assert!(Arc::ptr_eq(&output.effective_profile_root, &plan));
738            } else {
739                let effective_root = output
740                    .effective_profile_root
741                    .downcast_ref::<CoalescePartitionsExec>()
742                    .ok_or("expected CoalescePartitionsExec")?;
743                assert!(Arc::ptr_eq(effective_root.input(), &plan));
744                // DataFusion does not guarantee ordering between partitions.
745                expected.sort_unstable();
746                actual.sort_unstable();
747            }
748            assert_eq!(actual_schema, schema);
749            assert_eq!(actual, expected);
750        }
751        Ok(())
752    }
753
754    #[test]
755    fn query_output_stream_effective_root_matches_datafusion_setup_errors()
756    -> Result<(), Box<dyn Error>> {
757        let expected = setup_error_message(execute_stream(
758            Arc::new(ErrorExec::new()),
759            Arc::new(TaskContext::default()),
760        ))?;
761        let actual = setup_error_message(datafusion_query_output_stream_with_effective_root(
762            Arc::new(ErrorExec::new()),
763            Arc::new(TaskContext::default()),
764        ))?;
765
766        assert_eq!(actual, expected);
767        Ok(())
768    }
769
770    #[tokio::test]
771    async fn query_output_stream_effective_root_matches_datafusion_stream_errors()
772    -> Result<(), Box<dyn Error>> {
773        let expected = first_stream_error(execute_stream(
774            stream_error_plan()?,
775            Arc::new(TaskContext::default()),
776        )?)
777        .await?;
778        let actual = first_stream_error(
779            datafusion_query_output_stream_with_effective_root(
780                stream_error_plan()?,
781                Arc::new(TaskContext::default()),
782            )?
783            .stream,
784        )
785        .await?;
786
787        assert_eq!(actual, expected);
788        Ok(())
789    }
790
791    #[tokio::test]
792    async fn query_output_stream_effective_root_preserves_backpressure_and_wakes()
793    -> Result<(), Box<dyn Error>> {
794        let mut expected = exercise_backpressure_and_wakes(ExecutionPath::DataFusion).await?;
795        let mut actual = exercise_backpressure_and_wakes(ExecutionPath::DeltaFunnel).await?;
796
797        expected.sort_unstable();
798        actual.sort_unstable();
799        assert_eq!(actual, expected);
800        Ok(())
801    }
802
803    #[tokio::test]
804    async fn query_output_stream_effective_root_matches_datafusion_cancellation()
805    -> Result<(), Box<dyn Error>> {
806        assert_cancellation_releases_plan(ExecutionPath::DataFusion).await?;
807        assert_cancellation_releases_plan(ExecutionPath::DeltaFunnel).await?;
808        Ok(())
809    }
810
811    #[tokio::test]
812    async fn public_query_output_stream_still_merges_multiple_partitions()
813    -> Result<(), Box<dyn Error>> {
814        let schema = schema();
815        let plan = TestMemoryExec::try_new_exec(
816            &[
817                vec![int_batch(Arc::clone(&schema), &[1, 2])?],
818                vec![int_batch(Arc::clone(&schema), &[3, 4])?],
819            ],
820            schema,
821            None,
822        )?;
823        let stream = datafusion_query_output_stream(plan, Arc::new(TaskContext::default()))?;
824        let mut values = collect_stream_values(stream).await?;
825
826        values.sort_unstable();
827
828        assert_eq!(values, vec![1, 2, 3, 4]);
829        Ok(())
830    }
831
832    #[test]
833    fn public_query_output_stream_does_not_retain_zero_partition_root() -> Result<(), Box<dyn Error>>
834    {
835        let partitions: Vec<Vec<RecordBatch>> = Vec::new();
836        let plan: Arc<dyn ExecutionPlan> =
837            TestMemoryExec::try_new_exec(&partitions, schema(), None)?;
838        let weak_plan = Arc::downgrade(&plan);
839
840        let stream = datafusion_query_output_stream(plan, Arc::new(TaskContext::default()))?;
841
842        assert!(weak_plan.upgrade().is_none());
843        drop(stream);
844        Ok(())
845    }
846
847    async fn delta_plan(
848        context: &SessionContext,
849    ) -> Result<Arc<dyn ExecutionPlan>, Box<dyn Error>> {
850        Ok(context
851            .sql("select * from orders")
852            .await?
853            .create_physical_plan()
854            .await?)
855    }
856
857    fn int_batch(schema: SchemaRef, values: &[i32]) -> Result<RecordBatch, Box<dyn Error>> {
858        RecordBatch::try_new(schema, vec![Arc::new(Int32Array::from(values.to_vec()))])
859            .map_err(Into::into)
860    }
861
862    fn batch_values(batch: &RecordBatch) -> Result<Vec<i32>, Box<dyn Error>> {
863        let values = batch
864            .column(0)
865            .as_any()
866            .downcast_ref::<Int32Array>()
867            .ok_or("expected Int32Array")?;
868
869        Ok((0..values.len()).map(|index| values.value(index)).collect())
870    }
871
872    async fn collect_stream_values(
873        stream: datafusion::physical_plan::SendableRecordBatchStream,
874    ) -> Result<Vec<i32>, Box<dyn Error>> {
875        Ok(collect_stream_batch_values(stream)
876            .await?
877            .into_iter()
878            .flatten()
879            .collect())
880    }
881
882    async fn collect_stream_batch_values(
883        mut stream: datafusion::physical_plan::SendableRecordBatchStream,
884    ) -> Result<Vec<Vec<i32>>, Box<dyn Error>> {
885        let mut batches = Vec::new();
886        while let Some(batch) = stream.next().await {
887            batches.push(batch_values(&batch?)?);
888        }
889        Ok(batches)
890    }
891
892    fn setup_error_message<T, E>(result: Result<T, E>) -> Result<String, Box<dyn Error>>
893    where
894        E: Error,
895    {
896        match result {
897            Ok(_) => Err("expected stream setup error".into()),
898            Err(error) => Ok(error.to_string()),
899        }
900    }
901
902    fn stream_error_plan() -> Result<Arc<dyn ExecutionPlan>, Box<dyn Error>> {
903        let schema = schema();
904        let success: Arc<dyn ExecutionPlan> = Arc::new(MockExec::new(
905            vec![Ok(int_batch(Arc::clone(&schema), &[1])?)],
906            Arc::clone(&schema),
907        ));
908        let failure: Arc<dyn ExecutionPlan> = Arc::new(MockExec::new(
909            vec![Err(DataFusionError::Execution(
910                "injected stream failure".to_owned(),
911            ))],
912            schema,
913        ));
914        Ok(UnionExec::try_new(vec![success, failure])?)
915    }
916
917    async fn first_stream_error(
918        mut stream: datafusion::physical_plan::SendableRecordBatchStream,
919    ) -> Result<String, Box<dyn Error>> {
920        while let Some(batch) = stream.next().await {
921            if let Err(error) = batch {
922                return Ok(error.to_string());
923            }
924        }
925        Err("expected stream error".into())
926    }
927
928    enum ExecutionPath {
929        DataFusion,
930        DeltaFunnel,
931    }
932
933    async fn exercise_backpressure_and_wakes(
934        path: ExecutionPath,
935    ) -> Result<Vec<i32>, Box<dyn Error>> {
936        let schema = schema();
937        let partitions = (0..2)
938            .map(|partition| {
939                (0..8)
940                    .map(|offset| int_batch(Arc::clone(&schema), &[partition * 8 + offset]))
941                    .collect::<Result<Vec<_>, _>>()
942            })
943            .collect::<Result<Vec<_>, _>>()?;
944        let plan = Arc::new(
945            BarrierExec::new(partitions, Arc::clone(&schema))
946                .with_log(false)
947                .with_finish_barrier(),
948        );
949        let execution_plan: Arc<dyn ExecutionPlan> = plan.clone();
950        let stream = match path {
951            ExecutionPath::DataFusion => {
952                execute_stream(execution_plan, Arc::new(TaskContext::default()))?
953            }
954            ExecutionPath::DeltaFunnel => {
955                datafusion_query_output_stream_with_effective_root(
956                    execution_plan,
957                    Arc::new(TaskContext::default()),
958                )?
959                .stream
960            }
961        };
962
963        tokio::time::timeout(Duration::from_secs(5), plan.wait()).await?;
964        let drained_without_consumer = tokio::time::timeout(Duration::from_millis(100), async {
965            while !plan.is_finish_barrier_reached() {
966                tokio::task::yield_now().await;
967            }
968        })
969        .await;
970        assert!(drained_without_consumer.is_err());
971
972        let collection = tokio::spawn(async move { stream.try_collect::<Vec<_>>().await });
973        tokio::time::timeout(Duration::from_secs(5), plan.wait_finish()).await?;
974        let batches = tokio::time::timeout(Duration::from_secs(5), collection).await???;
975        let mut values = Vec::new();
976        for batch in &batches {
977            values.extend(batch_values(batch)?);
978        }
979        Ok(values)
980    }
981
982    async fn assert_cancellation_releases_plan(path: ExecutionPath) -> Result<(), Box<dyn Error>> {
983        let plan = Arc::new(BlockingExec::new(schema(), 2));
984        let refs = plan.refs();
985        let execution_plan: Arc<dyn ExecutionPlan> = plan;
986        let (mut stream, effective_profile_root) = match path {
987            ExecutionPath::DataFusion => (
988                execute_stream(execution_plan, Arc::new(TaskContext::default()))?,
989                None,
990            ),
991            ExecutionPath::DeltaFunnel => {
992                let execution = datafusion_query_output_stream_with_effective_root(
993                    execution_plan,
994                    Arc::new(TaskContext::default()),
995                )?;
996                (execution.stream, Some(execution.effective_profile_root))
997            }
998        };
999        let mut next = stream.next().boxed();
1000
1001        assert_is_pending(&mut next);
1002        drop(next);
1003        drop(stream);
1004        drop(effective_profile_root);
1005        assert_strong_count_converges_to_zero(refs).await;
1006        Ok(())
1007    }
1008
1009    fn schema() -> SchemaRef {
1010        Arc::new(Schema::new(vec![Field::new(
1011            "value",
1012            DataType::Int32,
1013            false,
1014        )]))
1015    }
1016}