1use std::sync::Arc;
4
5use datafusion::arrow::datatypes::SchemaRef;
6use datafusion::datasource::TableProvider;
7use datafusion::prelude::SessionContext;
8
9use crate::{
10 DeltaFunnelError, DeltaProtocolReport, PlannedDeltaSource, ProtocolPreflight,
11 error::DataFusionRegistrationSnafu, observability, support::sanitize_uri_for_display,
12 table_formats::validate_table_source_names,
13};
14
15use super::super::execution::DeltaProviderScanExecutionOptions;
16use super::provider::DeltaTableProvider;
17
18pub struct DeltaTableProviderConfig {
20 pub source: PlannedDeltaSource,
22 pub protocol: ProtocolPreflight,
24 pub scan_target_partitions: Option<usize>,
29}
30
31#[derive(Debug, Clone)]
33pub struct RegisteredDeltaSources {
34 pub sources: Vec<RegisteredDeltaSource>,
36}
37
38#[derive(Debug, Clone)]
40pub struct RegisteredDeltaSource {
41 pub name: String,
43 pub table_uri: String,
45 pub snapshot_version: u64,
47 pub schema: SchemaRef,
49 pub protocol: DeltaProtocolReport,
51}
52
53pub fn register_delta_sources(
61 ctx: &SessionContext,
62 configs: Vec<DeltaTableProviderConfig>,
63) -> Result<RegisteredDeltaSources, DeltaFunnelError> {
64 register_delta_sources_with_options(ctx, configs, DeltaProviderScanExecutionOptions::default())
65}
66
67pub fn register_delta_sources_with_scan_execution_options(
76 ctx: &SessionContext,
77 configs: Vec<DeltaTableProviderConfig>,
78 execution_options: DeltaProviderScanExecutionOptions,
79) -> Result<RegisteredDeltaSources, DeltaFunnelError> {
80 execution_options.validate()?;
81 register_delta_sources_with_options(ctx, configs, execution_options)
82}
83
84fn register_delta_sources_with_options(
85 ctx: &SessionContext,
86 configs: Vec<DeltaTableProviderConfig>,
87 execution_options: DeltaProviderScanExecutionOptions,
88) -> Result<RegisteredDeltaSources, DeltaFunnelError> {
89 reject_duplicate_registration_names(&configs)?;
90 let providers = configs
91 .into_iter()
92 .map(|config| {
93 DeltaTableProvider::try_new_with_execution_options(
94 config.source,
95 config.protocol,
96 config.scan_target_partitions,
97 execution_options,
98 )
99 })
100 .collect::<Result<Vec<_>, _>>()?;
101
102 reject_existing_registration_names(ctx, &providers)?;
103
104 let sources = register_delta_providers(ctx, providers)?;
105
106 Ok(RegisteredDeltaSources { sources })
107}
108
109fn reject_duplicate_registration_names(
110 configs: &[DeltaTableProviderConfig],
111) -> Result<(), DeltaFunnelError> {
112 validate_table_source_names(configs.iter().map(|config| config.source.name()))
113}
114
115fn reject_existing_registration_names(
116 ctx: &SessionContext,
117 providers: &[DeltaTableProvider],
118) -> Result<(), DeltaFunnelError> {
119 let state = ctx.state();
120 let catalog_options = &state.config_options().catalog;
121 let default_catalog = ctx.catalog(&catalog_options.default_catalog);
122 let default_schema = default_catalog
123 .as_ref()
124 .and_then(|catalog| catalog.schema(&catalog_options.default_schema));
125 let existing_names = default_schema
126 .as_ref()
127 .map_or_else(Vec::new, |schema| schema.table_names());
128
129 for provider in providers {
130 if let Some(existing_name) = existing_names
131 .iter()
132 .find(|existing_name| existing_name.eq_ignore_ascii_case(provider.source_name()))
133 {
134 return DataFusionRegistrationSnafu {
135 source_name: provider.source_name().to_owned(),
136 table_uri: provider.source_table_uri().to_owned(),
137 reason: format!("table already exists: {existing_name}"),
138 }
139 .fail();
140 }
141 }
142
143 Ok(())
144}
145
146fn register_delta_provider(
147 ctx: &SessionContext,
148 provider: DeltaTableProvider,
149) -> Result<RegisteredDeltaSource, DeltaFunnelError> {
150 let registered = RegisteredDeltaSource {
151 name: provider.source_name().to_owned(),
152 table_uri: sanitize_uri_for_display(provider.source_table_uri()),
153 snapshot_version: provider.snapshot_version(),
154 schema: provider.schema(),
155 protocol: provider.protocol().clone(),
156 };
157 let table_uri = provider.source_table_uri().to_owned();
158
159 if let Err(error) = ctx.register_table(registered.name.as_str(), Arc::new(provider)) {
160 return DataFusionRegistrationSnafu {
161 source_name: registered.name.clone(),
162 table_uri,
163 reason: error.to_string(),
164 }
165 .fail();
166 }
167
168 Ok(registered)
169}
170
171fn register_delta_provider_with_tracing(
172 ctx: &SessionContext,
173 provider: DeltaTableProvider,
174) -> Result<RegisteredDeltaSource, DeltaFunnelError> {
175 let source_name = provider.source_name().to_owned();
176 let snapshot_version = provider.snapshot_version();
177 observability::datafusion_registration_started(&source_name, snapshot_version);
178
179 let result = register_delta_provider(ctx, provider);
180 match &result {
181 Ok(registered) => {
182 observability::datafusion_registration_completed(®istered.name, snapshot_version);
183 }
184 Err(error) => {
185 observability::datafusion_registration_failed(&source_name, snapshot_version, error);
186 }
187 }
188 result
189}
190
191fn register_delta_providers(
192 ctx: &SessionContext,
193 providers: Vec<DeltaTableProvider>,
194) -> Result<Vec<RegisteredDeltaSource>, DeltaFunnelError> {
195 let mut registered_sources = Vec::with_capacity(providers.len());
196 let mut registered_names = Vec::with_capacity(providers.len());
197
198 for provider in providers {
199 let registered = match register_delta_provider_with_tracing(ctx, provider) {
200 Ok(registered) => registered,
201 Err(error) => {
202 rollback_registered_delta_sources(ctx, ®istered_names);
203 return Err(error);
204 }
205 };
206
207 registered_names.push(registered.name.clone());
208 registered_sources.push(registered);
209 }
210
211 Ok(registered_sources)
212}
213
214fn rollback_registered_delta_sources(ctx: &SessionContext, names: &[String]) {
215 for name in names.iter().rev() {
216 let _ = ctx.deregister_table(name.as_str());
217 }
218}
219
220pub(super) fn reject_mismatched_preflight(
221 source: &PlannedDeltaSource,
222 protocol: &DeltaProtocolReport,
223) -> Result<(), DeltaFunnelError> {
224 let source_table_uri = sanitize_uri_for_display(source.table_uri());
225
226 if protocol.source_name != source.name()
227 || protocol.snapshot_version != source.version()
228 || protocol.table_uri != source_table_uri
229 {
230 return DataFusionRegistrationSnafu {
231 source_name: source.name().to_owned(),
232 table_uri: source.table_uri().to_owned(),
233 reason: format!(
234 "protocol preflight belongs to source `{}` at snapshot version {} ({})",
235 protocol.source_name, protocol.snapshot_version, protocol.table_uri
236 ),
237 }
238 .fail();
239 }
240
241 Ok(())
242}
243
244#[cfg(test)]
245mod tests {
246 use std::sync::Arc;
247
248 use datafusion::arrow::datatypes::{DataType, Field, Schema};
249 use datafusion::datasource::TableType;
250 use datafusion::datasource::empty::EmptyTable;
251 use datafusion::prelude::{SessionConfig, SessionContext};
252
253 use super::*;
254 use crate::query_engine::datafusion::execution::DeltaProviderReaderBackend;
255 use crate::query_engine::datafusion::test_support::{
256 DeltaLogTable, FailsOnCustomersSchemaProvider, INVALID_NESTED_IDS_SCHEMA_FIELDS_JSON,
257 SingleSchemaCatalogProvider, register_fixture_source,
258 };
259 use crate::{DeltaFunnelError, DeltaSourceConfig, load_delta_source, preflight_delta_protocol};
260
261 #[test]
262 fn registers_preflighted_delta_source() -> Result<(), Box<dyn std::error::Error>> {
263 let table = DeltaLogTable::new("registration")?;
264 let source = load_delta_source(DeltaSourceConfig {
265 name: "orders".to_owned(),
266 table_uri: table.path().to_string_lossy().to_string(),
267 version: None,
268 storage_options: Default::default(),
269 })?;
270 let preflight = preflight_delta_protocol(&source)?;
271 let ctx = SessionContext::new();
272
273 let registered = register_delta_sources(
274 &ctx,
275 vec![DeltaTableProviderConfig {
276 source,
277 protocol: preflight,
278 scan_target_partitions: None,
279 }],
280 )?;
281
282 assert_eq!(registered.sources.len(), 1);
283 assert_eq!(registered.sources[0].name, "orders");
284 assert!(registered.sources[0].table_uri.starts_with("file://"));
285 assert_eq!(registered.sources[0].snapshot_version, 1);
286 assert_eq!(registered.sources[0].schema.field(0).name(), "id");
287 assert_eq!(registered.sources[0].protocol.source_name, "orders");
288
289 Ok(())
290 }
291
292 #[test]
293 fn registration_rejects_zero_execution_bounds_before_registration()
294 -> Result<(), Box<dyn std::error::Error>> {
295 let table = DeltaLogTable::new("registration-zero-execution-bounds")?;
296 let source = load_delta_source(DeltaSourceConfig {
297 name: "orders".to_owned(),
298 table_uri: table.path().to_string_lossy().to_string(),
299 version: None,
300 storage_options: Default::default(),
301 })?;
302 let preflight = preflight_delta_protocol(&source)?;
303 let ctx = SessionContext::new();
304
305 let result = register_delta_sources_with_scan_execution_options(
306 &ctx,
307 vec![DeltaTableProviderConfig {
308 source,
309 protocol: preflight,
310 scan_target_partitions: None,
311 }],
312 DeltaProviderScanExecutionOptions {
313 reader_backend: DeltaProviderReaderBackend::OfficialKernel,
314 max_concurrent_file_reads_per_scan: Some(0),
315 max_concurrent_file_reads_per_partition: 1,
316 output_buffer_capacity_per_partition: 1,
317 native_async_prefetch_file_count_per_partition: 0,
318 },
319 );
320
321 assert!(matches!(
322 result,
323 Err(DeltaFunnelError::Config { message })
324 if message == "max_concurrent_file_reads_per_scan must be greater than zero"
325 ));
326 assert!(!ctx.table_exist("orders")?);
327
328 Ok(())
329 }
330
331 #[test]
332 fn registration_accepts_native_async_backend_for_local_file_uri()
333 -> Result<(), Box<dyn std::error::Error>> {
334 let table = DeltaLogTable::new("registration-native-async-local-file")?;
335 let source = load_delta_source(DeltaSourceConfig {
336 name: "orders".to_owned(),
337 table_uri: table.path().to_string_lossy().to_string(),
338 version: None,
339 storage_options: Default::default(),
340 })?;
341 let preflight = preflight_delta_protocol(&source)?;
342 let ctx = SessionContext::new();
343
344 let registered = register_delta_sources_with_scan_execution_options(
345 &ctx,
346 vec![DeltaTableProviderConfig {
347 source,
348 protocol: preflight,
349 scan_target_partitions: None,
350 }],
351 DeltaProviderScanExecutionOptions {
352 reader_backend: DeltaProviderReaderBackend::NativeAsync,
353 max_concurrent_file_reads_per_scan: Some(1),
354 max_concurrent_file_reads_per_partition: 1,
355 output_buffer_capacity_per_partition: 1,
356 native_async_prefetch_file_count_per_partition: 0,
357 },
358 )?;
359
360 assert_eq!(registered.sources.len(), 1);
361 assert_eq!(registered.sources[0].name, "orders");
362 assert!(ctx.table_exist("orders")?);
363
364 Ok(())
365 }
366
367 #[tokio::test]
368 async fn catalog_inspection_exposes_registered_provider_schema()
369 -> Result<(), Box<dyn std::error::Error>> {
370 let ctx = SessionContext::new();
371 let _table = register_fixture_source(&ctx, "orders", "catalog-inspection")?;
372
373 let catalog = ctx.catalog("datafusion").ok_or("missing default catalog")?;
374 let schema = catalog.schema("public").ok_or("missing default schema")?;
375 let provider = schema
376 .table("orders")
377 .await?
378 .ok_or("missing registered table provider")?;
379 let provider_schema = provider.schema();
380
381 assert!(schema.table_names().contains(&"orders".to_owned()));
382 assert_eq!(provider.table_type(), TableType::Base);
383 assert_eq!(provider_schema.fields().len(), 2);
384 assert_eq!(provider_schema.field(0).name(), "id");
385 assert_eq!(provider_schema.field(0).data_type(), &DataType::Int32);
386 assert!(!provider_schema.field(0).is_nullable());
387 assert_eq!(provider_schema.field(1).name(), "customer_name");
388 assert_eq!(provider_schema.field(1).data_type(), &DataType::Utf8);
389 assert!(provider_schema.field(1).is_nullable());
390
391 Ok(())
392 }
393
394 #[test]
395 fn registration_failure_reports_source_context() -> Result<(), Box<dyn std::error::Error>> {
396 let table = DeltaLogTable::new("registration-failure")?;
397 let source = load_delta_source(DeltaSourceConfig {
398 name: "orders".to_owned(),
399 table_uri: table.path().to_string_lossy().to_string(),
400 version: None,
401 storage_options: Default::default(),
402 })?;
403 let preflight = preflight_delta_protocol(&source)?;
404 let ctx = SessionContext::new();
405 let placeholder_schema = Arc::new(Schema::new(vec![Field::new(
406 "existing",
407 DataType::Utf8,
408 true,
409 )]));
410
411 ctx.register_table("orders", Arc::new(EmptyTable::new(placeholder_schema)))?;
412 let result = register_delta_sources(
413 &ctx,
414 vec![DeltaTableProviderConfig {
415 source,
416 protocol: preflight,
417 scan_target_partitions: None,
418 }],
419 );
420
421 assert!(matches!(
422 result,
423 Err(DeltaFunnelError::DataFusionRegistration {
424 source_name,
425 reason,
426 ..
427 }) if source_name == "orders" && reason.contains("already exists")
428 ));
429
430 Ok(())
431 }
432
433 #[test]
434 fn mismatched_preflight_is_rejected_before_registration()
435 -> Result<(), Box<dyn std::error::Error>> {
436 let orders = DeltaLogTable::new("mismatched-preflight-orders")?;
437 let customers = DeltaLogTable::new("mismatched-preflight-customers")?;
438 let orders_source = load_delta_source(DeltaSourceConfig {
439 name: "orders".to_owned(),
440 table_uri: orders.path().to_string_lossy().to_string(),
441 version: None,
442 storage_options: Default::default(),
443 })?;
444 let customers_source = load_delta_source(DeltaSourceConfig {
445 name: "customers".to_owned(),
446 table_uri: customers.path().to_string_lossy().to_string(),
447 version: None,
448 storage_options: Default::default(),
449 })?;
450 let customers_preflight = preflight_delta_protocol(&customers_source)?;
451 let ctx = SessionContext::new();
452
453 let result = register_delta_sources(
454 &ctx,
455 vec![DeltaTableProviderConfig {
456 source: orders_source,
457 protocol: customers_preflight,
458 scan_target_partitions: None,
459 }],
460 );
461
462 assert!(matches!(
463 result,
464 Err(DeltaFunnelError::DataFusionRegistration {
465 source_name,
466 reason,
467 ..
468 }) if source_name == "orders"
469 && reason.contains("protocol preflight belongs to source `customers`")
470 ));
471 assert!(!ctx.table_exist("orders")?);
472
473 Ok(())
474 }
475
476 #[test]
477 fn existing_table_conflict_fails_before_partial_registration()
478 -> Result<(), Box<dyn std::error::Error>> {
479 let orders = DeltaLogTable::new("existing-conflict-orders")?;
480 let customers = DeltaLogTable::new("existing-conflict-customers")?;
481 let orders_source = load_delta_source(DeltaSourceConfig {
482 name: "orders".to_owned(),
483 table_uri: orders.path().to_string_lossy().to_string(),
484 version: None,
485 storage_options: Default::default(),
486 })?;
487 let customers_source = load_delta_source(DeltaSourceConfig {
488 name: "customers".to_owned(),
489 table_uri: customers.path().to_string_lossy().to_string(),
490 version: None,
491 storage_options: Default::default(),
492 })?;
493 let orders_preflight = preflight_delta_protocol(&orders_source)?;
494 let customers_preflight = preflight_delta_protocol(&customers_source)?;
495 let ctx = SessionContext::new();
496 let placeholder_schema = Arc::new(Schema::new(vec![Field::new(
497 "existing",
498 DataType::Utf8,
499 true,
500 )]));
501
502 ctx.register_table("customers", Arc::new(EmptyTable::new(placeholder_schema)))?;
503 let result = register_delta_sources(
504 &ctx,
505 vec![
506 DeltaTableProviderConfig {
507 source: orders_source,
508 protocol: orders_preflight,
509 scan_target_partitions: None,
510 },
511 DeltaTableProviderConfig {
512 source: customers_source,
513 protocol: customers_preflight,
514 scan_target_partitions: None,
515 },
516 ],
517 );
518
519 assert!(matches!(
520 result,
521 Err(DeltaFunnelError::DataFusionRegistration {
522 source_name,
523 reason,
524 ..
525 }) if source_name == "customers" && reason.contains("already exists")
526 ));
527 assert!(!ctx.table_exist("orders")?);
528 assert!(ctx.table_exist("customers")?);
529
530 Ok(())
531 }
532
533 #[test]
534 fn late_registration_failure_rolls_back_prior_sources() -> Result<(), Box<dyn std::error::Error>>
535 {
536 let orders = DeltaLogTable::new("rollback-orders")?;
537 let customers = DeltaLogTable::new("rollback-customers")?;
538 let orders_source = load_delta_source(DeltaSourceConfig {
539 name: "orders".to_owned(),
540 table_uri: orders.path().to_string_lossy().to_string(),
541 version: None,
542 storage_options: Default::default(),
543 })?;
544 let customers_source = load_delta_source(DeltaSourceConfig {
545 name: "customers".to_owned(),
546 table_uri: customers.path().to_string_lossy().to_string(),
547 version: None,
548 storage_options: Default::default(),
549 })?;
550 let orders_preflight = preflight_delta_protocol(&orders_source)?;
551 let customers_preflight = preflight_delta_protocol(&customers_source)?;
552 let ctx = SessionContext::new();
553 let failing_schema: Arc<dyn datafusion::catalog::SchemaProvider> =
554 Arc::new(FailsOnCustomersSchemaProvider::default());
555
556 ctx.register_catalog(
557 "datafusion",
558 Arc::new(SingleSchemaCatalogProvider::new(Arc::clone(
559 &failing_schema,
560 ))),
561 );
562 let result = register_delta_sources(
563 &ctx,
564 vec![
565 DeltaTableProviderConfig {
566 source: orders_source,
567 protocol: orders_preflight,
568 scan_target_partitions: None,
569 },
570 DeltaTableProviderConfig {
571 source: customers_source,
572 protocol: customers_preflight,
573 scan_target_partitions: None,
574 },
575 ],
576 );
577
578 assert!(matches!(
579 result,
580 Err(DeltaFunnelError::DataFusionRegistration {
581 source_name,
582 reason,
583 ..
584 }) if source_name == "customers"
585 && reason.contains("forced customers registration failure")
586 ));
587 assert!(!ctx.table_exist("orders")?);
588 assert!(!ctx.table_exist("customers")?);
589
590 Ok(())
591 }
592
593 #[test]
594 fn existing_table_conflict_uses_configured_default_catalog_and_schema()
595 -> Result<(), Box<dyn std::error::Error>> {
596 let orders = DeltaLogTable::new("custom-default-orders")?;
597 let customers = DeltaLogTable::new("custom-default-customers")?;
598 let orders_source = load_delta_source(DeltaSourceConfig {
599 name: "orders".to_owned(),
600 table_uri: orders.path().to_string_lossy().to_string(),
601 version: None,
602 storage_options: Default::default(),
603 })?;
604 let customers_source = load_delta_source(DeltaSourceConfig {
605 name: "customers".to_owned(),
606 table_uri: customers.path().to_string_lossy().to_string(),
607 version: None,
608 storage_options: Default::default(),
609 })?;
610 let orders_preflight = preflight_delta_protocol(&orders_source)?;
611 let customers_preflight = preflight_delta_protocol(&customers_source)?;
612 let ctx = SessionContext::new_with_config(
613 SessionConfig::new().with_default_catalog_and_schema("custom_catalog", "custom_schema"),
614 );
615 let placeholder_schema = Arc::new(Schema::new(vec![Field::new(
616 "existing",
617 DataType::Utf8,
618 true,
619 )]));
620
621 ctx.register_table("customers", Arc::new(EmptyTable::new(placeholder_schema)))?;
622 let result = register_delta_sources(
623 &ctx,
624 vec![
625 DeltaTableProviderConfig {
626 source: orders_source,
627 protocol: orders_preflight,
628 scan_target_partitions: None,
629 },
630 DeltaTableProviderConfig {
631 source: customers_source,
632 protocol: customers_preflight,
633 scan_target_partitions: None,
634 },
635 ],
636 );
637
638 assert!(matches!(
639 result,
640 Err(DeltaFunnelError::DataFusionRegistration {
641 source_name,
642 reason,
643 ..
644 }) if source_name == "customers" && reason.contains("already exists")
645 ));
646 assert!(!ctx.table_exist("orders")?);
647 assert!(ctx.table_exist("customers")?);
648
649 Ok(())
650 }
651
652 #[test]
653 fn schema_conversion_failure_fails_before_partial_registration()
654 -> Result<(), Box<dyn std::error::Error>> {
655 let orders = DeltaLogTable::new("schema-partial-orders")?;
656 let customers = DeltaLogTable::new_with_schema(
657 "schema-partial-customers",
658 INVALID_NESTED_IDS_SCHEMA_FIELDS_JSON,
659 "[]",
660 r#""partitionValues":{}"#,
661 )?;
662 let orders_source = load_delta_source(DeltaSourceConfig {
663 name: "orders".to_owned(),
664 table_uri: orders.path().to_string_lossy().to_string(),
665 version: None,
666 storage_options: Default::default(),
667 })?;
668 let customers_source = load_delta_source(DeltaSourceConfig {
669 name: "customers".to_owned(),
670 table_uri: customers.path().to_string_lossy().to_string(),
671 version: None,
672 storage_options: Default::default(),
673 })?;
674 let orders_preflight = preflight_delta_protocol(&orders_source)?;
675 let customers_preflight = preflight_delta_protocol(&customers_source)?;
676 let ctx = SessionContext::new();
677
678 let result = register_delta_sources(
679 &ctx,
680 vec![
681 DeltaTableProviderConfig {
682 source: orders_source,
683 protocol: orders_preflight,
684 scan_target_partitions: None,
685 },
686 DeltaTableProviderConfig {
687 source: customers_source,
688 protocol: customers_preflight,
689 scan_target_partitions: None,
690 },
691 ],
692 );
693
694 assert!(matches!(
695 result,
696 Err(DeltaFunnelError::DeltaSourceSchema {
697 source_name,
698 reason,
699 ..
700 }) if source_name == "customers"
701 && reason.contains("bad_array")
702 && reason.contains("delta.columnMapping.nested.ids")
703 ));
704 assert!(!ctx.table_exist("orders")?);
705 assert!(!ctx.table_exist("customers")?);
706
707 Ok(())
708 }
709
710 #[test]
711 fn duplicate_registration_names_fail_before_partial_registration()
712 -> Result<(), Box<dyn std::error::Error>> {
713 let orders = DeltaLogTable::new("duplicate-orders")?;
714 let customers = DeltaLogTable::new("duplicate-customers")?;
715 let orders_source = load_delta_source(DeltaSourceConfig {
716 name: "orders".to_owned(),
717 table_uri: orders.path().to_string_lossy().to_string(),
718 version: None,
719 storage_options: Default::default(),
720 })?;
721 let customers_source = load_delta_source(DeltaSourceConfig {
722 name: "Orders".to_owned(),
723 table_uri: customers.path().to_string_lossy().to_string(),
724 version: None,
725 storage_options: Default::default(),
726 })?;
727 let orders_preflight = preflight_delta_protocol(&orders_source)?;
728 let customers_preflight = preflight_delta_protocol(&customers_source)?;
729 let ctx = SessionContext::new();
730
731 let result = register_delta_sources(
732 &ctx,
733 vec![
734 DeltaTableProviderConfig {
735 source: orders_source,
736 protocol: orders_preflight,
737 scan_target_partitions: None,
738 },
739 DeltaTableProviderConfig {
740 source: customers_source,
741 protocol: customers_preflight,
742 scan_target_partitions: None,
743 },
744 ],
745 );
746
747 assert!(matches!(
748 result,
749 Err(DeltaFunnelError::DuplicateSourceName { name }) if name == "Orders"
750 ));
751 assert!(!ctx.table_exist("orders")?);
752
753 Ok(())
754 }
755}