Skip to main content

delta_funnel/
error.rs

1//! Shared error pattern for DeltaFunnel.
2
3use crate::BatchPipelinePhase;
4use crate::support::{sanitize_text_for_display, sanitize_uri_for_display};
5
6use snafu::Snafu;
7
8/// Error type used by DeltaFunnel APIs.
9#[derive(Debug, Snafu)]
10#[snafu(visibility(pub(crate)))]
11pub enum DeltaFunnelError {
12    /// Caller configuration is invalid.
13    #[snafu(display("configuration error: {message}"))]
14    Config {
15        /// Sanitized message suitable for logs and Python-facing errors.
16        message: String,
17    },
18
19    /// A bounded preview failed after its operation lifecycle began.
20    #[snafu(display(
21        "preview failed during {}: {}",
22        context.failed_phase(),
23        sanitize_reason_for_display(&source.to_string())
24    ))]
25    PreviewFailed {
26        /// Structured, redacted preview failure context.
27        context: Box<crate::PreviewFailureContext>,
28        /// Original Delta Funnel failure with its source chain preserved.
29        source: Box<DeltaFunnelError>,
30    },
31
32    /// A cache-enabled `write_all` call failed after cache selection.
33    #[snafu(display(
34        "write_all cache failed: {}",
35        sanitize_reason_for_display(&source.to_string())
36    ))]
37    WriteAllCache {
38        /// Failed cache attempts and any completed output workflow.
39        failure: Box<crate::WriteAllCacheFailure>,
40        /// Original primary failure with its source chain preserved.
41        source: Box<DeltaFunnelError>,
42    },
43
44    /// A Delta source name is not valid for registration.
45    #[snafu(display(
46        "invalid Delta source name `{}`: {reason}",
47        sanitize_source_name_for_display(name)
48    ))]
49    InvalidSourceName {
50        /// Caller-provided source name.
51        name: String,
52        /// Sanitized reason for the validation failure.
53        reason: &'static str,
54    },
55
56    /// Two configured Delta sources use the same registration name.
57    #[snafu(display(
58        "duplicate Delta source name `{}`",
59        sanitize_source_name_for_display(name)
60    ))]
61    DuplicateSourceName {
62        /// Caller-provided duplicate source name.
63        name: String,
64    },
65
66    /// A Delta source URI is not valid for snapshot loading.
67    #[snafu(display("invalid Delta source URI: {reason}"))]
68    InvalidSourceUri {
69        /// Sanitized reason for the validation failure.
70        reason: &'static str,
71    },
72
73    /// A Delta source engine could not be constructed.
74    #[snafu(display("Delta source engine error: {reason}"))]
75    DeltaSourceEngine {
76        /// Sanitized reason for the engine construction failure.
77        reason: &'static str,
78    },
79
80    /// A Delta snapshot could not be loaded.
81    #[snafu(display("Delta snapshot load error: {reason}"))]
82    DeltaSnapshotLoad {
83        /// Sanitized reason for the snapshot load failure.
84        reason: String,
85    },
86
87    /// A Delta source requires an unsupported reader protocol.
88    #[snafu(display(
89        "Delta protocol compatibility error for source `{}` at snapshot version {snapshot_version} ({}): {reason}",
90        sanitize_source_name_for_display(source_name),
91        sanitize_uri_for_display(table_uri)
92    ))]
93    DeltaProtocolCompatibility {
94        /// Caller-provided source name.
95        source_name: String,
96        /// Sanitized or sanitizable Delta table URI context.
97        table_uri: String,
98        /// Resolved Delta snapshot version.
99        snapshot_version: u64,
100        /// Sanitized reason for the compatibility failure.
101        reason: String,
102    },
103
104    /// A Delta source schema could not be exposed to the query engine.
105    #[snafu(display(
106        "Delta source schema error for source `{}` ({}): {}",
107        sanitize_source_name_for_display(source_name),
108        sanitize_uri_for_display(table_uri),
109        sanitize_reason_for_display(reason)
110    ))]
111    DeltaSourceSchema {
112        /// Caller-provided source name.
113        source_name: String,
114        /// Sanitized or sanitizable Delta table URI context.
115        table_uri: String,
116        /// Sanitized reason for the schema failure.
117        reason: String,
118    },
119
120    /// A Delta source could not be registered with DataFusion.
121    #[snafu(display(
122        "DataFusion registration error for source `{}` ({}): {}",
123        sanitize_source_name_for_display(source_name),
124        sanitize_uri_for_display(table_uri),
125        sanitize_reason_for_display(reason)
126    ))]
127    DataFusionRegistration {
128        /// Caller-provided source name.
129        source_name: String,
130        /// Sanitized or sanitizable Delta table URI context.
131        table_uri: String,
132        /// Sanitized reason for the registration failure.
133        reason: String,
134    },
135
136    /// Lazy SQL-derived table planning or alias registration failed.
137    #[snafu(display(
138        "SQL table error during {phase}: {}",
139        sanitize_reason_for_display(message)
140    ))]
141    SqlTable {
142        /// Session SQL table phase that failed.
143        phase: SqlTablePhase,
144        /// Sanitized reason for the failure.
145        message: String,
146    },
147
148    /// A required dependency contract is unavailable or incompatible.
149    #[snafu(display("dependency compatibility error: {message}"))]
150    DependencyCompatibility {
151        /// Sanitized message suitable for logs and Python-facing errors.
152        message: String,
153    },
154
155    /// Batch pipeline setup or configuration is invalid.
156    #[snafu(display(
157        "batch pipeline {phase} error for option `{option}`: {}",
158        sanitize_reason_for_display(message)
159    ))]
160    BatchPipeline {
161        /// Batch pipeline phase associated with the failure.
162        phase: BatchPipelinePhase,
163        /// Stable option name associated with the failure.
164        option: &'static str,
165        /// Sanitized message suitable for logs and Python-facing errors.
166        message: String,
167    },
168
169    /// SQL Server target configuration is invalid.
170    #[snafu(display(
171        "MSSQL target configuration error for option `{option}`: {}",
172        sanitize_reason_for_display(message)
173    ))]
174    MssqlTargetConfig {
175        /// Stable option name associated with the failure.
176        option: &'static str,
177        /// Sanitized message suitable for logs and Python-facing errors.
178        message: String,
179    },
180
181    /// A SQL Server target could not resolve a connection.
182    #[snafu(display(
183        "MSSQL target for output `{}` has no effective connection",
184        sanitize_text_for_display(output_name)
185    ))]
186    MissingMssqlConnection {
187        /// Selected output name associated with the target.
188        output_name: String,
189    },
190
191    /// A selected output identity is missing or invalid for SQL Server schema planning.
192    #[snafu(display(
193        "MSSQL schema planning error for output `{}`: {reason}",
194        sanitize_text_for_display(output_name)
195    ))]
196    InvalidMssqlOutputIdentity {
197        /// Selected output name associated with the schema.
198        output_name: String,
199        /// Stable reason for the validation failure.
200        reason: &'static str,
201    },
202
203    /// A selected output has duplicate field names before SQL Server planning.
204    #[snafu(display(
205        "MSSQL schema planning error for output `{}`: duplicate field name `{}` at indexes {first_index} and {duplicate_index}",
206        sanitize_text_for_display(output_name),
207        sanitize_text_for_display(field_name)
208    ))]
209    DuplicateMssqlOutputField {
210        /// Selected output name associated with the schema.
211        output_name: String,
212        /// Duplicate output field name.
213        field_name: String,
214        /// First index where the field name was seen.
215        first_index: usize,
216        /// Duplicate index where the field name was seen again.
217        duplicate_index: usize,
218    },
219
220    /// SQL Server schema planning failed with structured arrow-sql-server diagnostics.
221    #[snafu(display(
222        "MSSQL schema planning error for output `{}`: arrow-sql-server returned {} diagnostic(s)",
223        sanitize_text_for_display(output_name),
224        diagnostics.len()
225    ))]
226    MssqlSchemaPlanning {
227        /// Selected output name associated with the schema.
228        output_name: String,
229        /// Structured diagnostics returned by arrow-sql-server.
230        diagnostics: arrow_sql_server::DiagnosticSet,
231    },
232
233    /// SQL Server schema planning failed before producing diagnostics.
234    #[snafu(display(
235        "MSSQL schema planning error for output `{}`: {}",
236        sanitize_text_for_display(output_name),
237        sanitize_reason_for_display(&source.to_string())
238    ))]
239    MssqlSchemaPlanningFailed {
240        /// Selected output name associated with the schema.
241        output_name: String,
242        /// Underlying arrow-sql-server planning failure.
243        source: arrow_sql_server::Error,
244    },
245
246    /// SQL Server DDL planning failed because a target identifier was invalid.
247    #[snafu(display(
248        "MSSQL DDL planning error for output `{}`: {}",
249        sanitize_text_for_display(output_name),
250        sanitize_reason_for_display(&source.to_string())
251    ))]
252    MssqlDdlTargetIdentifier {
253        /// Selected output name associated with the target.
254        output_name: String,
255        /// Underlying arrow-sql-server identifier validation failure.
256        source: arrow_sql_server::Error,
257    },
258
259    /// SQL Server DDL planning failed for a DeltaFunnel-owned lifecycle reason.
260    #[snafu(display(
261        "MSSQL DDL planning error for output `{}`: {}",
262        sanitize_text_for_display(output_name),
263        sanitize_reason_for_display(message)
264    ))]
265    MssqlDdlPlanning {
266        /// Selected output name associated with the target.
267        output_name: String,
268        /// Sanitized reason for the DDL planning failure.
269        message: String,
270    },
271
272    /// SQL Server lifecycle planning failed for a DeltaFunnel-owned reason.
273    #[snafu(display(
274        "MSSQL lifecycle planning error for output `{}`: {}",
275        sanitize_text_for_display(output_name),
276        sanitize_reason_for_display(message)
277    ))]
278    MssqlLifecyclePlanning {
279        /// Selected output name associated with the target.
280        output_name: String,
281        /// Sanitized reason for the lifecycle planning failure.
282        message: String,
283    },
284
285    /// SQL Server batch writing failed.
286    #[snafu(display(
287        "MSSQL write error: {}",
288        sanitize_reason_for_display(&source.to_string())
289    ))]
290    MssqlWrite {
291        /// Underlying Arrow-to-TDS or Tiberius writer failure.
292        source: arrow_sql_server::Error,
293    },
294
295    /// SQL Server write execution failed during a known phase.
296    #[snafu(display(
297        "MSSQL write error for output `{}` during {}: {}",
298        sanitize_text_for_display(context.output_name()),
299        context.phase(),
300        sanitize_reason_for_display(message)
301    ))]
302    MssqlWritePhase {
303        /// Structured, redacted failure context.
304        context: Box<crate::MssqlWriteFailureContext>,
305        /// Sanitized reason for the write failure.
306        message: String,
307    },
308
309    /// SQL Server output query preparation failed during a known phase.
310    #[snafu(display(
311        "MSSQL write error for output `{}` during {}: {}",
312        sanitize_text_for_display(context.output_name()),
313        context.phase(),
314        sanitize_reason_for_display(&source.to_string())
315    ))]
316    MssqlQueryPhase {
317        /// Structured, redacted failure context.
318        context: Box<crate::MssqlWriteFailureContext>,
319        /// Original query preparation failure with its source chain preserved.
320        source: Box<DeltaFunnelError>,
321    },
322
323    /// SQL Server output batch schema validation failed.
324    #[snafu(display(
325        "MSSQL write error for output `{}` during {}: {}",
326        sanitize_text_for_display(context.output_name()),
327        context.phase(),
328        sanitize_reason_for_display(&source.to_string())
329    ))]
330    MssqlBatchSchemaValidation {
331        /// Structured, redacted failure context.
332        context: Box<crate::MssqlWriteFailureContext>,
333        /// Underlying arrow-sql-server schema validation failure.
334        source: arrow_sql_server::Error,
335    },
336
337    /// SQL Server multi-output workflow planning failed before output writes.
338    #[snafu(display(
339        "MSSQL workflow planning error: {}",
340        sanitize_reason_for_display(message)
341    ))]
342    MssqlWorkflowPlanning {
343        /// Sanitized reason for the workflow planning failure.
344        message: String,
345    },
346}
347
348/// Phase associated with lazy SQL table construction.
349#[derive(Debug, Clone, Copy, PartialEq, Eq)]
350pub enum SqlTablePhase {
351    /// Caller SQL text failed local validation.
352    ValidateSql,
353    /// DataFusion could not produce a lazy logical table.
354    PlanSql,
355    /// A derived table alias could not be registered in the session catalog.
356    RegisterDerivedAlias,
357}
358
359impl std::fmt::Display for SqlTablePhase {
360    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361        formatter.write_str(match self {
362            Self::ValidateSql => "SQL validation",
363            Self::PlanSql => "SQL planning",
364            Self::RegisterDerivedAlias => "derived alias registration",
365        })
366    }
367}
368
369fn sanitize_source_name_for_display(name: &str) -> String {
370    sanitize_text_for_display(name)
371}
372
373fn sanitize_reason_for_display(reason: &str) -> String {
374    sanitize_text_for_display(reason)
375}
376
377#[cfg(test)]
378mod tests {
379    use std::error::Error;
380
381    use super::DeltaFunnelError;
382
383    #[test]
384    fn config_error_has_sanitized_display() {
385        let error = DeltaFunnelError::Config {
386            message: "max_concurrent_file_reads_per_scan must be greater than zero".to_owned(),
387        };
388
389        assert_eq!(
390            error.to_string(),
391            "configuration error: max_concurrent_file_reads_per_scan must be greater than zero"
392        );
393    }
394
395    #[test]
396    fn preview_failure_sanitizes_display_and_preserves_source() {
397        let context = crate::PreviewFailureContext::new(
398            "preview_dataframe_planning".to_owned(),
399            vec![crate::PhaseTimingReport::failed(
400                "preview_dataframe_planning",
401                std::time::Duration::ZERO,
402            )],
403            None,
404        );
405        let error = DeltaFunnelError::PreviewFailed {
406            context: Box::new(context),
407            source: Box::new(DeltaFunnelError::Config {
408                message: "planning failed\nfor test".to_owned(),
409            }),
410        };
411
412        let display = error.to_string();
413
414        assert!(!display.contains('\n'));
415        assert!(display.contains(r"planning failed\nfor test"));
416        assert!(Error::source(&error).is_some());
417    }
418
419    #[test]
420    fn dependency_error_has_sanitized_display() {
421        let error = DeltaFunnelError::DependencyCompatibility {
422            message: "delta_kernel API smoke test failed".to_owned(),
423        };
424
425        assert_eq!(
426            error.to_string(),
427            "dependency compatibility error: delta_kernel API smoke test failed"
428        );
429    }
430
431    #[test]
432    fn batch_pipeline_error_has_sanitized_display() {
433        let error = DeltaFunnelError::BatchPipeline {
434            phase: super::BatchPipelinePhase::Configuration,
435            option: "output_batch_size",
436            message: "must be greater than zero".to_owned(),
437        };
438
439        assert_eq!(
440            error.to_string(),
441            "batch pipeline configuration error for option `output_batch_size`: must be greater than zero"
442        );
443    }
444
445    #[test]
446    fn batch_pipeline_error_display_escapes_control_characters() {
447        let error = DeltaFunnelError::BatchPipeline {
448            phase: super::BatchPipelinePhase::HandoffSetup,
449            option: "consumer_capacity",
450            message: "invalid\nvalue\tprovided".to_owned(),
451        };
452
453        let display = error.to_string();
454
455        assert!(!display.contains('\n'));
456        assert!(!display.contains('\t'));
457        assert!(display.contains(r"invalid\nvalue\tprovided"));
458    }
459
460    #[test]
461    fn mssql_write_error_has_sanitized_display() {
462        let error = DeltaFunnelError::MssqlWrite {
463            source: arrow_sql_server::Error::BackendUnavailable {
464                backend: arrow_sql_server::WriteBackend::DirectRawBulk,
465                reason: "not available\nfor test".to_owned(),
466            },
467        };
468
469        let display = error.to_string();
470
471        assert!(!display.contains('\n'));
472        assert!(display.contains(r"not available\nfor test"));
473    }
474
475    #[test]
476    fn mssql_write_phase_error_has_sanitized_display_and_context() -> Result<(), DeltaFunnelError> {
477        let connection = crate::MssqlConnectionConfig::new(
478            "server=tcp:sql.example.com;database=warehouse;user=admin;password=secret-token",
479        )?
480        .with_display_label("warehouse-primary");
481        let target_config =
482            crate::MssqlTargetConfig::new(crate::MssqlTargetTable::new("dbo", "orders")?);
483        let schema = arrow_schema::Schema::new(vec![arrow_schema::Field::new(
484            "order_id",
485            arrow_schema::DataType::Int64,
486            false,
487        )]);
488        let output_plan = crate::plan_mssql_target_for_output(
489            schema,
490            "orders_output",
491            &target_config,
492            Some(&connection),
493            arrow_sql_server::PlanOptions::default(),
494        )?;
495        let context = crate::MssqlWriteFailureContext::from_output_plan(
496            &output_plan,
497            crate::MssqlWritePhase::WriteBatch,
498            42,
499            3,
500            125,
501            true,
502            crate::MssqlTargetCleanupStatus::NotApplicable,
503        );
504
505        let error = DeltaFunnelError::MssqlWritePhase {
506            context: Box::new(context),
507            message: "batch failed\nwhile writing".to_owned(),
508        };
509
510        let display = error.to_string();
511
512        assert!(display.contains("orders_output"));
513        assert!(display.contains("write batch"));
514        assert!(!display.contains('\n'));
515        assert!(display.contains(r"batch failed\nwhile writing"));
516        assert!(!display.contains("secret-token"));
517        assert!(!display.contains("server=tcp"));
518        let DeltaFunnelError::MssqlWritePhase { context, .. } = error else {
519            return Err(DeltaFunnelError::Config {
520                message: "expected MssqlWritePhase error".to_owned(),
521            });
522        };
523        assert_eq!(context.phase(), crate::MssqlWritePhase::WriteBatch);
524        assert_eq!(context.output_name(), "orders_output");
525        assert_eq!(context.stats().rows_written(), 42);
526        assert!(context.partial_write_possible());
527        Ok(())
528    }
529
530    #[test]
531    fn mssql_batch_schema_validation_error_has_sanitized_display_and_context()
532    -> Result<(), DeltaFunnelError> {
533        let connection = crate::MssqlConnectionConfig::new(
534            "server=tcp:sql.example.com;database=warehouse;user=admin;password=secret-token",
535        )?
536        .with_display_label("warehouse-primary");
537        let target_config =
538            crate::MssqlTargetConfig::new(crate::MssqlTargetTable::new("dbo", "orders")?);
539        let schema = arrow_schema::Schema::new(vec![arrow_schema::Field::new(
540            "order_id",
541            arrow_schema::DataType::Int64,
542            false,
543        )]);
544        let output_plan = crate::plan_mssql_target_for_output(
545            schema,
546            "orders_output",
547            &target_config,
548            Some(&connection),
549            arrow_sql_server::PlanOptions::default(),
550        )?;
551        let context = crate::MssqlWriteFailureContext::from_output_plan(
552            &output_plan,
553            crate::MssqlWritePhase::ValidateBatchSchema,
554            0,
555            0,
556            0,
557            false,
558            crate::MssqlTargetCleanupStatus::NotApplicable,
559        );
560
561        let error = DeltaFunnelError::MssqlBatchSchemaValidation {
562            context: Box::new(context),
563            source: arrow_sql_server::Error::BackendUnavailable {
564                backend: arrow_sql_server::WriteBackend::DirectRawBulk,
565                reason: "schema mismatch\nfor test".to_owned(),
566            },
567        };
568
569        let display = error.to_string();
570
571        assert!(display.contains("orders_output"));
572        assert!(display.contains("validate batch schema"));
573        assert!(!display.contains('\n'));
574        assert!(display.contains(r"schema mismatch\nfor test"));
575        assert!(!display.contains("secret-token"));
576        assert!(!display.contains("server=tcp"));
577        let DeltaFunnelError::MssqlBatchSchemaValidation { context, source } = error else {
578            return Err(DeltaFunnelError::Config {
579                message: "expected MssqlBatchSchemaValidation error".to_owned(),
580            });
581        };
582        assert_eq!(context.phase(), crate::MssqlWritePhase::ValidateBatchSchema);
583        assert_eq!(context.output_name(), "orders_output");
584        assert_eq!(context.stats().rows_written(), 0);
585        assert!(!context.partial_write_possible());
586        assert!(matches!(
587            source,
588            arrow_sql_server::Error::BackendUnavailable { .. }
589        ));
590        Ok(())
591    }
592
593    #[test]
594    fn invalid_source_name_error_has_sanitized_display() {
595        let error = DeltaFunnelError::InvalidSourceName {
596            name: "orders.latest".to_owned(),
597            reason: "source names may contain only ASCII letters, digits, and underscores",
598        };
599
600        assert_eq!(
601            error.to_string(),
602            "invalid Delta source name `orders.latest`: source names may contain only ASCII letters, digits, and underscores"
603        );
604    }
605
606    #[test]
607    fn invalid_source_name_display_escapes_control_characters() {
608        let error = DeltaFunnelError::InvalidSourceName {
609            name: "orders\nlatest\tname".to_owned(),
610            reason: "source names may contain only ASCII letters, digits, and underscores",
611        };
612
613        let display = error.to_string();
614
615        assert!(!display.contains('\n'));
616        assert!(!display.contains('\t'));
617        assert!(display.contains(r"orders\nlatest\tname"));
618    }
619
620    #[test]
621    fn duplicate_source_name_error_has_sanitized_display() {
622        let error = DeltaFunnelError::DuplicateSourceName {
623            name: "Orders".to_owned(),
624        };
625
626        assert_eq!(error.to_string(), "duplicate Delta source name `Orders`");
627    }
628
629    #[test]
630    fn invalid_source_uri_error_has_sanitized_display() {
631        let error = DeltaFunnelError::InvalidSourceUri {
632            reason: "table location could not be parsed or normalized",
633        };
634
635        assert_eq!(
636            error.to_string(),
637            "invalid Delta source URI: table location could not be parsed or normalized"
638        );
639    }
640
641    #[test]
642    fn source_engine_error_has_sanitized_display() {
643        let error = DeltaFunnelError::DeltaSourceEngine {
644            reason: "object store engine could not be constructed",
645        };
646
647        assert_eq!(
648            error.to_string(),
649            "Delta source engine error: object store engine could not be constructed"
650        );
651    }
652
653    #[test]
654    fn snapshot_load_error_has_sanitized_display() {
655        let error = DeltaFunnelError::DeltaSnapshotLoad {
656            reason: "snapshot could not be loaded".to_owned(),
657        };
658
659        assert_eq!(
660            error.to_string(),
661            "Delta snapshot load error: snapshot could not be loaded"
662        );
663    }
664
665    #[test]
666    fn protocol_compatibility_error_has_sanitized_display() {
667        let error = DeltaFunnelError::DeltaProtocolCompatibility {
668            source_name: "orders\nlatest".to_owned(),
669            table_uri: "s3://user:password@example.com/table?token=secret".to_owned(),
670            snapshot_version: 7,
671            reason: "unsupported Delta reader feature `deletionVectors`".to_owned(),
672        };
673
674        let display = error.to_string();
675
676        assert!(display.contains(r"orders\nlatest"));
677        assert!(display.contains("snapshot version 7"));
678        assert!(display.contains("s3://example.com/table"));
679        assert!(display.contains("deletionVectors"));
680        assert!(!display.contains('\n'));
681        assert!(!display.contains("user"));
682        assert!(!display.contains("password"));
683        assert!(!display.contains("token"));
684        assert!(!display.contains("secret"));
685    }
686
687    #[test]
688    fn source_schema_error_has_sanitized_display() {
689        let error = DeltaFunnelError::DeltaSourceSchema {
690            source_name: "orders\nlatest".to_owned(),
691            table_uri: "s3://user:password@example.com/table?token=secret".to_owned(),
692            reason: "field\nname could not be converted".to_owned(),
693        };
694
695        let display = error.to_string();
696
697        assert!(display.contains(r"orders\nlatest"));
698        assert!(display.contains("s3://example.com/table"));
699        assert!(display.contains(r"field\nname"));
700        assert!(!display.contains('\n'));
701        assert!(!display.contains("user"));
702        assert!(!display.contains("password"));
703        assert!(!display.contains("token"));
704        assert!(!display.contains("secret"));
705    }
706
707    #[test]
708    fn datafusion_registration_error_has_sanitized_display() {
709        let error = DeltaFunnelError::DataFusionRegistration {
710            source_name: "orders\nlatest".to_owned(),
711            table_uri: "s3://user:password@example.com/table?token=secret".to_owned(),
712            reason: "table\nalready exists".to_owned(),
713        };
714
715        let display = error.to_string();
716
717        assert!(display.contains(r"orders\nlatest"));
718        assert!(display.contains("s3://example.com/table"));
719        assert!(display.contains(r"table\nalready exists"));
720        assert!(!display.contains('\n'));
721        assert!(!display.contains("user"));
722        assert!(!display.contains("password"));
723        assert!(!display.contains("token"));
724        assert!(!display.contains("secret"));
725    }
726}