1use 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#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct DeltaReadMetricsSnapshot {
46 pub snapshot_version: u64,
48 pub reader_backend: ParquetReaderBackend,
50 pub scan_metadata_exhausted: Option<bool>,
52 pub scan_partitions_planned: u64,
54 pub files_planned: u64,
56 pub files_filtered_during_planning: Option<u64>,
58 pub estimated_rows: Option<u64>,
60 pub estimated_bytes: Option<u64>,
62 pub scan_partitions_started: u64,
64 pub scan_partitions_completed: u64,
66 pub files_started: u64,
68 pub files_completed: u64,
70 pub batches_produced: u64,
72 pub rows_produced: u64,
74 pub deletion_vector_payloads_loaded: u64,
76 pub deletion_vectors_applied: u64,
78 pub deletion_vector_rows_deleted: u64,
80 pub deletion_vector_failures: u64,
82 pub deletion_vector_rejections: u64,
84 pub parquet_data_file_range_get_operations: Option<u64>,
86 pub parquet_data_file_full_get_operations: Option<u64>,
88 pub parquet_data_file_bytes_received: Option<u64>,
90 pub parquet_data_file_opened_bytes: Option<u64>,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct DeltaDataFusionMetricsSnapshot {
97 pub reader: DeltaReadMetricsSnapshot,
99 pub use_view_types: bool,
101 pub output_batch_size: Option<u64>,
103 pub dynamic_partition_files_pruned: u64,
105 pub dynamic_partition_files_kept: u64,
107 pub dynamic_filters_received: u64,
109 pub dynamic_filters_accepted: u64,
111 pub dynamic_filters_unsupported: u64,
113 pub dynamic_filter_snapshots: u64,
115 pub dynamic_files_not_pruned_missing_metadata: u64,
117 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#[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
215pub(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
250pub(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
275pub 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 match plan.properties().output_partitioning().partition_count() {
331 0 => (plan, false),
334 1 => (plan, true),
336 2.. => {
337 (
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 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}