1use crate::BatchPipelinePhase;
4use crate::support::{sanitize_text_for_display, sanitize_uri_for_display};
5
6use snafu::Snafu;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum DeltaScanFileReadPhase {
11 TableUriParsing,
13 FileMetadataConversion,
15 FilePathResolution,
17 ObjectStoreEngineConstruction,
19 ParquetReadSetup,
21 ParquetBatchRead,
23 RowIndexGeneration,
25 PredicateEvaluation,
27 ArrowConversion,
29 TransformApplication,
31 UnsupportedReadMode,
33 DeletionVectorPredicateRejection,
35 DeletionVectorMasking,
37}
38
39impl std::fmt::Display for DeltaScanFileReadPhase {
40 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 formatter.write_str(match self {
42 Self::TableUriParsing => "table URI parsing",
43 Self::FileMetadataConversion => "file metadata conversion",
44 Self::FilePathResolution => "file path resolution",
45 Self::ObjectStoreEngineConstruction => "object store engine construction",
46 Self::ParquetReadSetup => "Parquet read setup",
47 Self::ParquetBatchRead => "Parquet batch read",
48 Self::RowIndexGeneration => "row-index generation",
49 Self::PredicateEvaluation => "physical predicate evaluation",
50 Self::ArrowConversion => "Arrow conversion",
51 Self::TransformApplication => "physical-to-logical transform application",
52 Self::UnsupportedReadMode => "unsupported read mode",
53 Self::DeletionVectorPredicateRejection => "deletion-vector predicate read rejection",
54 Self::DeletionVectorMasking => "deletion-vector masking",
55 })
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum DeltaScanDeletionVectorPhase {
62 TableUriParsing,
64 ObjectStoreEngineConstruction,
66 DescriptorAccess,
68 PayloadRead,
70 SelectionVectorLengthMismatch,
72 SelectionVectorExhaustion,
74}
75
76impl std::fmt::Display for DeltaScanDeletionVectorPhase {
77 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 formatter.write_str(match self {
79 Self::TableUriParsing => "table URI parsing",
80 Self::ObjectStoreEngineConstruction => "object store engine construction",
81 Self::DescriptorAccess => "deletion-vector descriptor access",
82 Self::PayloadRead => "deletion-vector payload read",
83 Self::SelectionVectorLengthMismatch => "selection-vector length mismatch",
84 Self::SelectionVectorExhaustion => "selection-vector exhaustion",
85 })
86 }
87}
88
89#[derive(Debug, Snafu)]
91#[snafu(visibility(pub(crate)))]
92pub enum DeltaFunnelError {
93 #[snafu(display("configuration error: {message}"))]
95 Config {
96 message: String,
98 },
99
100 #[snafu(display(
102 "invalid Delta source name `{}`: {reason}",
103 sanitize_source_name_for_display(name)
104 ))]
105 InvalidSourceName {
106 name: String,
108 reason: &'static str,
110 },
111
112 #[snafu(display(
114 "duplicate Delta source name `{}`",
115 sanitize_source_name_for_display(name)
116 ))]
117 DuplicateSourceName {
118 name: String,
120 },
121
122 #[snafu(display("invalid Delta source URI: {reason}"))]
124 InvalidSourceUri {
125 reason: &'static str,
127 },
128
129 #[snafu(display("Delta source engine error: {reason}"))]
131 DeltaSourceEngine {
132 reason: &'static str,
134 },
135
136 #[snafu(display("Delta snapshot load error: {reason}"))]
138 DeltaSnapshotLoad {
139 reason: String,
141 },
142
143 #[snafu(display(
145 "Delta protocol compatibility error for source `{}` at snapshot version {snapshot_version} ({}): {reason}",
146 sanitize_source_name_for_display(source_name),
147 sanitize_uri_for_display(table_uri)
148 ))]
149 DeltaProtocolCompatibility {
150 source_name: String,
152 table_uri: String,
154 snapshot_version: u64,
156 reason: String,
158 },
159
160 #[snafu(display(
162 "Delta source schema error for source `{}` ({}): {}",
163 sanitize_source_name_for_display(source_name),
164 sanitize_uri_for_display(table_uri),
165 sanitize_reason_for_display(reason)
166 ))]
167 DeltaSourceSchema {
168 source_name: String,
170 table_uri: String,
172 reason: String,
174 },
175
176 #[snafu(display(
178 "DataFusion registration error for source `{}` ({}): {}",
179 sanitize_source_name_for_display(source_name),
180 sanitize_uri_for_display(table_uri),
181 sanitize_reason_for_display(reason)
182 ))]
183 DataFusionRegistration {
184 source_name: String,
186 table_uri: String,
188 reason: String,
190 },
191
192 #[snafu(display(
194 "SQL table error during {phase}: {}",
195 sanitize_reason_for_display(message)
196 ))]
197 SqlTable {
198 phase: SqlTablePhase,
200 message: String,
202 },
203
204 #[snafu(display(
206 "Delta scan projection error for source `{}` ({}): {}",
207 sanitize_source_name_for_display(source_name),
208 sanitize_uri_for_display(table_uri),
209 sanitize_reason_for_display(reason)
210 ))]
211 DeltaScanProjection {
212 source_name: String,
214 table_uri: String,
216 reason: String,
218 },
219
220 #[snafu(display(
222 "Delta scan filter error for source `{}` ({}): {}",
223 sanitize_source_name_for_display(source_name),
224 sanitize_uri_for_display(table_uri),
225 sanitize_reason_for_display(reason)
226 ))]
227 DeltaScanFilter {
228 source_name: String,
230 table_uri: String,
232 reason: String,
234 },
235
236 #[snafu(display(
238 "Delta scan construction error for source `{}` ({}): {}",
239 sanitize_source_name_for_display(source_name),
240 sanitize_uri_for_display(table_uri),
241 sanitize_reason_for_display(&source.to_string())
242 ))]
243 DeltaScanConstruction {
244 source_name: String,
246 table_uri: String,
248 #[snafu(source(from(delta_kernel::Error, Box::new)))]
250 source: Box<delta_kernel::Error>,
251 },
252
253 #[snafu(display(
255 "Delta scan metadata expansion error for source `{}` at snapshot version {snapshot_version} ({}): {}",
256 sanitize_source_name_for_display(source_name),
257 sanitize_uri_for_display(table_uri),
258 sanitize_reason_for_display(&source.to_string())
259 ))]
260 DeltaScanMetadataExpansion {
261 source_name: String,
263 table_uri: String,
265 snapshot_version: u64,
267 #[snafu(source(from(delta_kernel::Error, Box::new)))]
269 source: Box<delta_kernel::Error>,
270 },
271
272 #[snafu(display(
274 "Delta scan file task planning error for source `{}` at snapshot version {snapshot_version} ({}), file `{}`: {}",
275 sanitize_source_name_for_display(source_name),
276 sanitize_uri_for_display(table_uri),
277 sanitize_reason_for_display(path),
278 sanitize_reason_for_display(reason)
279 ))]
280 DeltaScanFileTaskPlanning {
281 source_name: String,
283 table_uri: String,
285 snapshot_version: u64,
287 path: String,
289 reason: String,
291 },
292
293 #[snafu(display(
295 "Delta scan file task partition planning error for source `{}` at snapshot version {snapshot_version} ({}): {}",
296 sanitize_source_name_for_display(source_name),
297 sanitize_uri_for_display(table_uri),
298 sanitize_reason_for_display(reason)
299 ))]
300 DeltaScanFileTaskPartitionPlanning {
301 source_name: String,
303 table_uri: String,
305 snapshot_version: u64,
307 reason: String,
309 },
310
311 #[snafu(display(
313 "Delta scan file read error for source `{}` at snapshot version {snapshot_version} ({}), file `{}` during {phase}: {}",
314 sanitize_source_name_for_display(source_name),
315 sanitize_uri_for_display(table_uri),
316 sanitize_reason_for_display(path),
317 sanitize_reason_for_display(&source.to_string())
318 ))]
319 DeltaScanFileRead {
320 source_name: String,
322 table_uri: String,
324 snapshot_version: u64,
326 path: String,
328 phase: DeltaScanFileReadPhase,
330 #[snafu(source(from(delta_kernel::Error, Box::new)))]
332 source: Box<delta_kernel::Error>,
333 },
334
335 #[snafu(display(
337 "Delta scan deletion-vector error for source `{}` at snapshot version {snapshot_version} ({}), file `{}` during {phase}: {}",
338 sanitize_source_name_for_display(source_name),
339 sanitize_uri_for_display(table_uri),
340 sanitize_reason_for_display(path),
341 sanitize_reason_for_display(&source.to_string())
342 ))]
343 DeltaScanDeletionVector {
344 source_name: String,
346 table_uri: String,
348 snapshot_version: u64,
350 path: String,
352 phase: DeltaScanDeletionVectorPhase,
354 #[snafu(source(from(delta_kernel::Error, Box::new)))]
356 source: Box<delta_kernel::Error>,
357 },
358
359 #[snafu(display("dependency compatibility error: {message}"))]
361 DependencyCompatibility {
362 message: String,
364 },
365
366 #[snafu(display(
368 "batch pipeline {phase} error for option `{option}`: {}",
369 sanitize_reason_for_display(message)
370 ))]
371 BatchPipeline {
372 phase: BatchPipelinePhase,
374 option: &'static str,
376 message: String,
378 },
379
380 #[snafu(display(
382 "MSSQL target configuration error for option `{option}`: {}",
383 sanitize_reason_for_display(message)
384 ))]
385 MssqlTargetConfig {
386 option: &'static str,
388 message: String,
390 },
391
392 #[snafu(display(
394 "MSSQL target for output `{}` has no effective connection",
395 sanitize_text_for_display(output_name)
396 ))]
397 MissingMssqlConnection {
398 output_name: String,
400 },
401
402 #[snafu(display(
404 "MSSQL schema planning error for output `{}`: {reason}",
405 sanitize_text_for_display(output_name)
406 ))]
407 InvalidMssqlOutputIdentity {
408 output_name: String,
410 reason: &'static str,
412 },
413
414 #[snafu(display(
416 "MSSQL schema planning error for output `{}`: duplicate field name `{}` at indexes {first_index} and {duplicate_index}",
417 sanitize_text_for_display(output_name),
418 sanitize_text_for_display(field_name)
419 ))]
420 DuplicateMssqlOutputField {
421 output_name: String,
423 field_name: String,
425 first_index: usize,
427 duplicate_index: usize,
429 },
430
431 #[snafu(display(
433 "MSSQL schema planning error for output `{}`: arrow-tiberius returned {} diagnostic(s)",
434 sanitize_text_for_display(output_name),
435 diagnostics.len()
436 ))]
437 MssqlSchemaPlanning {
438 output_name: String,
440 diagnostics: arrow_tiberius::DiagnosticSet,
442 },
443
444 #[snafu(display(
446 "MSSQL schema planning error for output `{}`: {}",
447 sanitize_text_for_display(output_name),
448 sanitize_reason_for_display(&source.to_string())
449 ))]
450 MssqlSchemaPlanningFailed {
451 output_name: String,
453 source: arrow_tiberius::Error,
455 },
456
457 #[snafu(display(
459 "MSSQL DDL planning error for output `{}`: {}",
460 sanitize_text_for_display(output_name),
461 sanitize_reason_for_display(&source.to_string())
462 ))]
463 MssqlDdlTargetIdentifier {
464 output_name: String,
466 source: arrow_tiberius::Error,
468 },
469
470 #[snafu(display(
472 "MSSQL DDL planning error for output `{}`: {}",
473 sanitize_text_for_display(output_name),
474 sanitize_reason_for_display(message)
475 ))]
476 MssqlDdlPlanning {
477 output_name: String,
479 message: String,
481 },
482
483 #[snafu(display(
485 "MSSQL lifecycle planning error for output `{}`: {}",
486 sanitize_text_for_display(output_name),
487 sanitize_reason_for_display(message)
488 ))]
489 MssqlLifecyclePlanning {
490 output_name: String,
492 message: String,
494 },
495
496 #[snafu(display(
498 "MSSQL write error: {}",
499 sanitize_reason_for_display(&source.to_string())
500 ))]
501 MssqlWrite {
502 source: arrow_tiberius::Error,
504 },
505
506 #[snafu(display(
508 "MSSQL write error for output `{}` during {}: {}",
509 sanitize_text_for_display(context.output_name()),
510 context.phase(),
511 sanitize_reason_for_display(message)
512 ))]
513 MssqlWritePhase {
514 context: Box<crate::MssqlWriteFailureContext>,
516 message: String,
518 },
519
520 #[snafu(display(
522 "MSSQL write error for output `{}` during {}: {}",
523 sanitize_text_for_display(context.output_name()),
524 context.phase(),
525 sanitize_reason_for_display(&source.to_string())
526 ))]
527 MssqlBatchSchemaValidation {
528 context: Box<crate::MssqlWriteFailureContext>,
530 source: arrow_tiberius::Error,
532 },
533
534 #[snafu(display(
536 "MSSQL workflow planning error: {}",
537 sanitize_reason_for_display(message)
538 ))]
539 MssqlWorkflowPlanning {
540 message: String,
542 },
543}
544
545#[derive(Debug, Clone, Copy, PartialEq, Eq)]
547pub enum SqlTablePhase {
548 ValidateSql,
550 PlanSql,
552 RegisterDerivedAlias,
554}
555
556impl std::fmt::Display for SqlTablePhase {
557 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
558 formatter.write_str(match self {
559 Self::ValidateSql => "SQL validation",
560 Self::PlanSql => "SQL planning",
561 Self::RegisterDerivedAlias => "derived alias registration",
562 })
563 }
564}
565
566fn sanitize_source_name_for_display(name: &str) -> String {
567 sanitize_text_for_display(name)
568}
569
570fn sanitize_reason_for_display(reason: &str) -> String {
571 sanitize_text_for_display(reason)
572}
573
574#[cfg(test)]
575mod tests {
576 use std::error::Error;
577
578 use super::DeltaFunnelError;
579
580 #[test]
581 fn config_error_has_sanitized_display() {
582 let error = DeltaFunnelError::Config {
583 message: "max_concurrent_file_reads_per_scan must be greater than zero".to_owned(),
584 };
585
586 assert_eq!(
587 error.to_string(),
588 "configuration error: max_concurrent_file_reads_per_scan must be greater than zero"
589 );
590 }
591
592 #[test]
593 fn dependency_error_has_sanitized_display() {
594 let error = DeltaFunnelError::DependencyCompatibility {
595 message: "delta_kernel API smoke test failed".to_owned(),
596 };
597
598 assert_eq!(
599 error.to_string(),
600 "dependency compatibility error: delta_kernel API smoke test failed"
601 );
602 }
603
604 #[test]
605 fn batch_pipeline_error_has_sanitized_display() {
606 let error = DeltaFunnelError::BatchPipeline {
607 phase: super::BatchPipelinePhase::Configuration,
608 option: "output_batch_size",
609 message: "must be greater than zero".to_owned(),
610 };
611
612 assert_eq!(
613 error.to_string(),
614 "batch pipeline configuration error for option `output_batch_size`: must be greater than zero"
615 );
616 }
617
618 #[test]
619 fn batch_pipeline_error_display_escapes_control_characters() {
620 let error = DeltaFunnelError::BatchPipeline {
621 phase: super::BatchPipelinePhase::HandoffSetup,
622 option: "consumer_capacity",
623 message: "invalid\nvalue\tprovided".to_owned(),
624 };
625
626 let display = error.to_string();
627
628 assert!(!display.contains('\n'));
629 assert!(!display.contains('\t'));
630 assert!(display.contains(r"invalid\nvalue\tprovided"));
631 }
632
633 #[test]
634 fn mssql_write_error_has_sanitized_display() {
635 let error = DeltaFunnelError::MssqlWrite {
636 source: arrow_tiberius::Error::BackendUnavailable {
637 backend: arrow_tiberius::WriteBackend::DirectRawBulk,
638 reason: "not available\nfor test".to_owned(),
639 },
640 };
641
642 let display = error.to_string();
643
644 assert!(!display.contains('\n'));
645 assert!(display.contains(r"not available\nfor test"));
646 }
647
648 #[test]
649 fn mssql_write_phase_error_has_sanitized_display_and_context() -> Result<(), DeltaFunnelError> {
650 let connection = crate::MssqlConnectionConfig::new(
651 "server=tcp:sql.example.com;database=warehouse;user=admin;password=secret-token",
652 )?
653 .with_display_label("warehouse-primary");
654 let target_config =
655 crate::MssqlTargetConfig::new(crate::MssqlTargetTable::new("dbo", "orders")?);
656 let schema = arrow_schema::Schema::new(vec![arrow_schema::Field::new(
657 "order_id",
658 arrow_schema::DataType::Int64,
659 false,
660 )]);
661 let output_plan = crate::plan_mssql_target_for_output(
662 schema,
663 "orders_output",
664 &target_config,
665 Some(&connection),
666 arrow_tiberius::PlanOptions::default(),
667 )?;
668 let context = crate::MssqlWriteFailureContext::from_output_plan(
669 &output_plan,
670 crate::MssqlWritePhase::WriteBatch,
671 42,
672 3,
673 125,
674 true,
675 crate::MssqlTargetCleanupStatus::NotApplicable,
676 );
677
678 let error = DeltaFunnelError::MssqlWritePhase {
679 context: Box::new(context),
680 message: "batch failed\nwhile writing".to_owned(),
681 };
682
683 let display = error.to_string();
684
685 assert!(display.contains("orders_output"));
686 assert!(display.contains("write batch"));
687 assert!(!display.contains('\n'));
688 assert!(display.contains(r"batch failed\nwhile writing"));
689 assert!(!display.contains("secret-token"));
690 assert!(!display.contains("server=tcp"));
691 let DeltaFunnelError::MssqlWritePhase { context, .. } = error else {
692 return Err(DeltaFunnelError::Config {
693 message: "expected MssqlWritePhase error".to_owned(),
694 });
695 };
696 assert_eq!(context.phase(), crate::MssqlWritePhase::WriteBatch);
697 assert_eq!(context.output_name(), "orders_output");
698 assert_eq!(context.stats().rows_written(), 42);
699 assert!(context.partial_write_possible());
700 Ok(())
701 }
702
703 #[test]
704 fn mssql_batch_schema_validation_error_has_sanitized_display_and_context()
705 -> Result<(), DeltaFunnelError> {
706 let connection = crate::MssqlConnectionConfig::new(
707 "server=tcp:sql.example.com;database=warehouse;user=admin;password=secret-token",
708 )?
709 .with_display_label("warehouse-primary");
710 let target_config =
711 crate::MssqlTargetConfig::new(crate::MssqlTargetTable::new("dbo", "orders")?);
712 let schema = arrow_schema::Schema::new(vec![arrow_schema::Field::new(
713 "order_id",
714 arrow_schema::DataType::Int64,
715 false,
716 )]);
717 let output_plan = crate::plan_mssql_target_for_output(
718 schema,
719 "orders_output",
720 &target_config,
721 Some(&connection),
722 arrow_tiberius::PlanOptions::default(),
723 )?;
724 let context = crate::MssqlWriteFailureContext::from_output_plan(
725 &output_plan,
726 crate::MssqlWritePhase::ValidateBatchSchema,
727 0,
728 0,
729 0,
730 false,
731 crate::MssqlTargetCleanupStatus::NotApplicable,
732 );
733
734 let error = DeltaFunnelError::MssqlBatchSchemaValidation {
735 context: Box::new(context),
736 source: arrow_tiberius::Error::BackendUnavailable {
737 backend: arrow_tiberius::WriteBackend::DirectRawBulk,
738 reason: "schema mismatch\nfor test".to_owned(),
739 },
740 };
741
742 let display = error.to_string();
743
744 assert!(display.contains("orders_output"));
745 assert!(display.contains("validate batch schema"));
746 assert!(!display.contains('\n'));
747 assert!(display.contains(r"schema mismatch\nfor test"));
748 assert!(!display.contains("secret-token"));
749 assert!(!display.contains("server=tcp"));
750 let DeltaFunnelError::MssqlBatchSchemaValidation { context, source } = error else {
751 return Err(DeltaFunnelError::Config {
752 message: "expected MssqlBatchSchemaValidation error".to_owned(),
753 });
754 };
755 assert_eq!(context.phase(), crate::MssqlWritePhase::ValidateBatchSchema);
756 assert_eq!(context.output_name(), "orders_output");
757 assert_eq!(context.stats().rows_written(), 0);
758 assert!(!context.partial_write_possible());
759 assert!(matches!(
760 source,
761 arrow_tiberius::Error::BackendUnavailable { .. }
762 ));
763 Ok(())
764 }
765
766 #[test]
767 fn invalid_source_name_error_has_sanitized_display() {
768 let error = DeltaFunnelError::InvalidSourceName {
769 name: "orders.latest".to_owned(),
770 reason: "source names may contain only ASCII letters, digits, and underscores",
771 };
772
773 assert_eq!(
774 error.to_string(),
775 "invalid Delta source name `orders.latest`: source names may contain only ASCII letters, digits, and underscores"
776 );
777 }
778
779 #[test]
780 fn invalid_source_name_display_escapes_control_characters() {
781 let error = DeltaFunnelError::InvalidSourceName {
782 name: "orders\nlatest\tname".to_owned(),
783 reason: "source names may contain only ASCII letters, digits, and underscores",
784 };
785
786 let display = error.to_string();
787
788 assert!(!display.contains('\n'));
789 assert!(!display.contains('\t'));
790 assert!(display.contains(r"orders\nlatest\tname"));
791 }
792
793 #[test]
794 fn duplicate_source_name_error_has_sanitized_display() {
795 let error = DeltaFunnelError::DuplicateSourceName {
796 name: "Orders".to_owned(),
797 };
798
799 assert_eq!(error.to_string(), "duplicate Delta source name `Orders`");
800 }
801
802 #[test]
803 fn invalid_source_uri_error_has_sanitized_display() {
804 let error = DeltaFunnelError::InvalidSourceUri {
805 reason: "table location could not be parsed or normalized",
806 };
807
808 assert_eq!(
809 error.to_string(),
810 "invalid Delta source URI: table location could not be parsed or normalized"
811 );
812 }
813
814 #[test]
815 fn source_engine_error_has_sanitized_display() {
816 let error = DeltaFunnelError::DeltaSourceEngine {
817 reason: "object store engine could not be constructed",
818 };
819
820 assert_eq!(
821 error.to_string(),
822 "Delta source engine error: object store engine could not be constructed"
823 );
824 }
825
826 #[test]
827 fn snapshot_load_error_has_sanitized_display() {
828 let error = DeltaFunnelError::DeltaSnapshotLoad {
829 reason: "snapshot could not be loaded".to_owned(),
830 };
831
832 assert_eq!(
833 error.to_string(),
834 "Delta snapshot load error: snapshot could not be loaded"
835 );
836 }
837
838 #[test]
839 fn protocol_compatibility_error_has_sanitized_display() {
840 let error = DeltaFunnelError::DeltaProtocolCompatibility {
841 source_name: "orders\nlatest".to_owned(),
842 table_uri: "s3://user:password@example.com/table?token=secret".to_owned(),
843 snapshot_version: 7,
844 reason: "unsupported Delta reader feature `deletionVectors`".to_owned(),
845 };
846
847 let display = error.to_string();
848
849 assert!(display.contains(r"orders\nlatest"));
850 assert!(display.contains("snapshot version 7"));
851 assert!(display.contains("s3://example.com/table"));
852 assert!(display.contains("deletionVectors"));
853 assert!(!display.contains('\n'));
854 assert!(!display.contains("user"));
855 assert!(!display.contains("password"));
856 assert!(!display.contains("token"));
857 assert!(!display.contains("secret"));
858 }
859
860 #[test]
861 fn source_schema_error_has_sanitized_display() {
862 let error = DeltaFunnelError::DeltaSourceSchema {
863 source_name: "orders\nlatest".to_owned(),
864 table_uri: "s3://user:password@example.com/table?token=secret".to_owned(),
865 reason: "field\nname could not be converted".to_owned(),
866 };
867
868 let display = error.to_string();
869
870 assert!(display.contains(r"orders\nlatest"));
871 assert!(display.contains("s3://example.com/table"));
872 assert!(display.contains(r"field\nname"));
873 assert!(!display.contains('\n'));
874 assert!(!display.contains("user"));
875 assert!(!display.contains("password"));
876 assert!(!display.contains("token"));
877 assert!(!display.contains("secret"));
878 }
879
880 #[test]
881 fn datafusion_registration_error_has_sanitized_display() {
882 let error = DeltaFunnelError::DataFusionRegistration {
883 source_name: "orders\nlatest".to_owned(),
884 table_uri: "s3://user:password@example.com/table?token=secret".to_owned(),
885 reason: "table\nalready exists".to_owned(),
886 };
887
888 let display = error.to_string();
889
890 assert!(display.contains(r"orders\nlatest"));
891 assert!(display.contains("s3://example.com/table"));
892 assert!(display.contains(r"table\nalready exists"));
893 assert!(!display.contains('\n'));
894 assert!(!display.contains("user"));
895 assert!(!display.contains("password"));
896 assert!(!display.contains("token"));
897 assert!(!display.contains("secret"));
898 }
899
900 #[test]
901 fn scan_metadata_expansion_error_has_sanitized_display()
902 -> Result<(), Box<dyn std::error::Error>> {
903 let error = DeltaFunnelError::DeltaScanMetadataExpansion {
904 source_name: "orders\nlatest".to_owned(),
905 table_uri: "s3://user:password@example.com/table?token=secret".to_owned(),
906 snapshot_version: 7,
907 source: Box::new(delta_kernel::Error::generic(
908 "scan\nmetadata expansion failed",
909 )),
910 };
911
912 let display = error.to_string();
913
914 assert!(display.contains(r"orders\nlatest"));
915 assert!(display.contains("snapshot version 7"));
916 assert!(display.contains("s3://example.com/table"));
917 assert!(display.contains(r"scan\nmetadata expansion failed"));
918 assert!(!display.contains('\n'));
919 assert!(!display.contains("user"));
920 assert!(!display.contains("password"));
921 assert!(!display.contains("token"));
922 assert!(!display.contains("secret"));
923
924 let source = Error::source(&error)
925 .ok_or("metadata expansion error must preserve its kernel source")?;
926 assert!(
927 source
928 .to_string()
929 .contains("scan\nmetadata expansion failed")
930 );
931
932 Ok(())
933 }
934
935 #[test]
936 fn scan_file_task_planning_error_has_sanitized_display() {
937 let error = DeltaFunnelError::DeltaScanFileTaskPlanning {
938 source_name: "orders\nlatest".to_owned(),
939 table_uri: "s3://user:password@example.com/table?token=secret".to_owned(),
940 snapshot_version: 7,
941 path: "part\n00000.parquet".to_owned(),
942 reason: "kernel\nsize was negative".to_owned(),
943 };
944
945 let display = error.to_string();
946
947 assert!(display.contains(r"orders\nlatest"));
948 assert!(display.contains("snapshot version 7"));
949 assert!(display.contains("s3://example.com/table"));
950 assert!(display.contains(r"part\n00000.parquet"));
951 assert!(display.contains(r"kernel\nsize was negative"));
952 assert!(!display.contains('\n'));
953 assert!(!display.contains("user"));
954 assert!(!display.contains("password"));
955 assert!(!display.contains("token"));
956 assert!(!display.contains("secret"));
957 }
958
959 #[test]
960 fn scan_file_task_partition_planning_error_has_sanitized_display() {
961 let error = DeltaFunnelError::DeltaScanFileTaskPartitionPlanning {
962 source_name: "orders\nlatest".to_owned(),
963 table_uri: "s3://user:password@example.com/table?token=secret".to_owned(),
964 snapshot_version: 7,
965 reason: "target\npartitions was zero".to_owned(),
966 };
967
968 let display = error.to_string();
969
970 assert!(display.contains(r"orders\nlatest"));
971 assert!(display.contains("snapshot version 7"));
972 assert!(display.contains("s3://example.com/table"));
973 assert!(display.contains(r"target\npartitions was zero"));
974 assert!(!display.contains('\n'));
975 assert!(!display.contains("user"));
976 assert!(!display.contains("password"));
977 assert!(!display.contains("token"));
978 assert!(!display.contains("secret"));
979 }
980}