Skip to main content

delta_funnel/report/sql_server/
write.rs

1use std::fmt;
2
3use crate::{
4    MssqlConnectionSource, MssqlConnectionSummary, MssqlTargetOutputPlan, MssqlTargetTable,
5    MssqlWritePhase, PhaseStatus, PhaseTimingReport, ReportReasonCode, RowCount, ValidationStatus,
6    sql_server::LoadMode, support::sanitize_text_for_display,
7};
8
9/// Per-output SQL Server write statistics.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct MssqlWriteStats {
12    output_name: String,
13    rows_written: u64,
14    batches_written: u64,
15    elapsed_ms: u64,
16}
17
18impl MssqlWriteStats {
19    /// Builds write statistics for one selected output.
20    #[must_use]
21    pub fn new(
22        output_name: impl Into<String>,
23        rows_written: u64,
24        batches_written: u64,
25        elapsed_ms: u64,
26    ) -> Self {
27        Self {
28            output_name: output_name.into(),
29            rows_written,
30            batches_written,
31            elapsed_ms,
32        }
33    }
34
35    /// Returns the selected output name.
36    #[must_use]
37    pub fn output_name(&self) -> &str {
38        &self.output_name
39    }
40
41    /// Returns the number of rows accepted by SQL Server writing.
42    #[must_use]
43    pub const fn rows_written(&self) -> u64 {
44        self.rows_written
45    }
46
47    /// Returns the number of batches accepted by SQL Server writing.
48    #[must_use]
49    pub const fn batches_written(&self) -> u64 {
50        self.batches_written
51    }
52
53    /// Returns elapsed write time in milliseconds.
54    #[must_use]
55    pub const fn elapsed_ms(&self) -> u64 {
56        self.elapsed_ms
57    }
58}
59
60/// Output schema field included in an MSSQL execute report.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct MssqlOutputFieldReport {
63    index: u64,
64    name: String,
65    arrow_type: String,
66    nullable: bool,
67}
68
69impl MssqlOutputFieldReport {
70    pub(crate) fn from_mapping(mapping: &arrow_tiberius::SchemaMapping) -> Self {
71        Self {
72            index: crate::usize_to_u64_saturating(mapping.arrow().index()),
73            name: mapping.arrow().name().to_owned(),
74            arrow_type: mapping.arrow().data_type().to_string(),
75            nullable: mapping.arrow().nullable(),
76        }
77    }
78
79    /// Returns the zero-based output field index.
80    #[must_use]
81    pub const fn index(&self) -> u64 {
82        self.index
83    }
84
85    /// Returns the output field name.
86    #[must_use]
87    pub fn name(&self) -> &str {
88        &self.name
89    }
90
91    /// Returns the Arrow data type as a stable display string.
92    #[must_use]
93    pub fn arrow_type(&self) -> &str {
94        &self.arrow_type
95    }
96
97    /// Returns true when the output field is nullable.
98    #[must_use]
99    pub const fn nullable(&self) -> bool {
100        self.nullable
101    }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub(crate) struct MssqlWriteDiagnosticField {
106    index: u64,
107    name: String,
108}
109
110impl MssqlWriteDiagnosticField {
111    fn from_arrow_tiberius(field: &arrow_tiberius::FieldRef) -> Self {
112        Self {
113            index: crate::usize_to_u64_saturating(field.index()),
114            name: field.name().to_owned(),
115        }
116    }
117
118    pub(crate) const fn index(&self) -> u64 {
119        self.index
120    }
121
122    pub(crate) fn name(&self) -> &str {
123        &self.name
124    }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub(crate) struct MssqlWriteDiagnostic {
129    severity: arrow_tiberius::DiagnosticSeverity,
130    code: arrow_tiberius::DiagnosticCode,
131    message: String,
132    field: Option<MssqlWriteDiagnosticField>,
133    row: Option<u64>,
134}
135
136impl MssqlWriteDiagnostic {
137    pub(crate) fn from_arrow_tiberius(diagnostic: &arrow_tiberius::Diagnostic) -> Self {
138        Self {
139            severity: diagnostic.severity(),
140            code: diagnostic.code(),
141            message: sanitize_text_for_display(diagnostic.message()),
142            field: diagnostic
143                .field()
144                .map(MssqlWriteDiagnosticField::from_arrow_tiberius),
145            row: diagnostic.row().map(crate::usize_to_u64_saturating),
146        }
147    }
148
149    pub(crate) const fn severity(&self) -> arrow_tiberius::DiagnosticSeverity {
150        self.severity
151    }
152
153    pub(crate) const fn code(&self) -> arrow_tiberius::DiagnosticCode {
154        self.code
155    }
156
157    pub(crate) fn message(&self) -> &str {
158        &self.message
159    }
160
161    pub(crate) fn field(&self) -> Option<&MssqlWriteDiagnosticField> {
162        self.field.as_ref()
163    }
164
165    pub(crate) const fn row(&self) -> Option<u64> {
166        self.row
167    }
168}
169
170/// Per-output query stream and identity batch-shaping counters.
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub struct MssqlBatchShapingReport {
173    status: PhaseStatus,
174    input_batches: u64,
175    input_rows: u64,
176    output_batches: u64,
177    output_rows: u64,
178}
179
180impl MssqlBatchShapingReport {
181    pub(crate) fn completed(
182        input_batches: u64,
183        input_rows: u64,
184        output_batches: u64,
185        output_rows: u64,
186    ) -> Self {
187        Self {
188            status: PhaseStatus::completed(),
189            input_batches,
190            input_rows,
191            output_batches,
192            output_rows,
193        }
194    }
195
196    pub(crate) fn failed(
197        input_batches: u64,
198        input_rows: u64,
199        output_batches: u64,
200        output_rows: u64,
201    ) -> Self {
202        Self {
203            status: PhaseStatus::failed(),
204            input_batches,
205            input_rows,
206            output_batches,
207            output_rows,
208        }
209    }
210
211    pub(crate) fn not_started(reason: ReportReasonCode) -> Self {
212        Self {
213            status: PhaseStatus::not_started(reason),
214            input_batches: 0,
215            input_rows: 0,
216            output_batches: 0,
217            output_rows: 0,
218        }
219    }
220
221    pub(crate) fn skipped(reason: ReportReasonCode) -> Self {
222        Self {
223            status: PhaseStatus::skipped(reason),
224            input_batches: 0,
225            input_rows: 0,
226            output_batches: 0,
227            output_rows: 0,
228        }
229    }
230
231    /// Returns the batch shaping phase status.
232    #[must_use]
233    pub const fn status(&self) -> PhaseStatus {
234        self.status
235    }
236
237    /// Returns batches consumed from the selected output stream.
238    #[must_use]
239    pub const fn input_batches(&self) -> u64 {
240        self.input_batches
241    }
242
243    /// Returns rows consumed from the selected output stream.
244    #[must_use]
245    pub const fn input_rows(&self) -> u64 {
246        self.input_rows
247    }
248
249    /// Returns batches emitted after batch shaping.
250    #[must_use]
251    pub const fn output_batches(&self) -> u64 {
252        self.output_batches
253    }
254
255    /// Returns rows emitted after batch shaping.
256    #[must_use]
257    pub const fn output_rows(&self) -> u64 {
258        self.output_rows
259    }
260}
261
262#[derive(Debug, Clone, PartialEq, Eq)]
263pub(crate) struct MssqlWriteReportMetrics {
264    pub(crate) output_row_count: RowCount,
265    pub(crate) target_row_count_before_write: RowCount,
266    pub(crate) target_row_count: RowCount,
267    pub(crate) validation_status: ValidationStatus,
268    pub(crate) batch_shaping: MssqlBatchShapingReport,
269    pub(crate) phase_timings: Vec<PhaseTimingReport>,
270    pub(crate) rows_written: u64,
271    pub(crate) batches_written: u64,
272    pub(crate) elapsed_ms: u64,
273    pub(crate) partial_write_possible: bool,
274    pub(crate) cleanup: MssqlTargetCleanupStatus,
275}
276
277impl MssqlWriteReportMetrics {
278    pub(crate) const fn new(
279        output_row_count: RowCount,
280        batch_shaping: MssqlBatchShapingReport,
281        rows_written: u64,
282        batches_written: u64,
283        elapsed_ms: u64,
284        partial_write_possible: bool,
285        cleanup: MssqlTargetCleanupStatus,
286    ) -> Self {
287        Self {
288            output_row_count,
289            target_row_count_before_write: RowCount::unavailable(),
290            target_row_count: RowCount::unavailable(),
291            validation_status: ValidationStatus::skipped(ReportReasonCode::NotExecuted),
292            batch_shaping,
293            phase_timings: Vec::new(),
294            rows_written,
295            batches_written,
296            elapsed_ms,
297            partial_write_possible,
298            cleanup,
299        }
300    }
301
302    pub(crate) fn with_phase_timings(mut self, phase_timings: Vec<PhaseTimingReport>) -> Self {
303        self.phase_timings = phase_timings;
304        self
305    }
306
307    #[allow(dead_code)]
308    pub(crate) const fn with_target_validation(
309        mut self,
310        target_row_count: RowCount,
311        validation_status: ValidationStatus,
312    ) -> Self {
313        self.target_row_count = target_row_count;
314        self.validation_status = validation_status;
315        self
316    }
317
318    pub(crate) const fn with_target_delta_validation(
319        mut self,
320        target_row_count_before_write: RowCount,
321        target_row_count_after_write: RowCount,
322        validation_status: ValidationStatus,
323    ) -> Self {
324        self.target_row_count_before_write = target_row_count_before_write;
325        self.target_row_count = target_row_count_after_write;
326        self.validation_status = validation_status;
327        self
328    }
329}
330
331/// Cleanup reporting state for a SQL Server target owned by create-and-load.
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
333pub enum MssqlTargetCleanupStatus {
334    /// No cleanup is owned by this output, such as append-existing mode.
335    NotApplicable,
336    /// Cleanup would be owned by this output, but the target was not created.
337    NotAttempted,
338    /// Cleanup was required, attempted, and succeeded.
339    Succeeded,
340    /// Cleanup was required, attempted, and failed.
341    Failed,
342}
343
344impl fmt::Display for MssqlTargetCleanupStatus {
345    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
346        formatter.write_str(match self {
347            Self::NotApplicable => "not applicable",
348            Self::NotAttempted => "not attempted",
349            Self::Succeeded => "succeeded",
350            Self::Failed => "failed",
351        })
352    }
353}
354
355/// Redacted per-output SQL Server write report.
356#[derive(Debug, Clone, PartialEq, Eq)]
357pub struct MssqlWriteReport {
358    output_name: String,
359    target_table: MssqlTargetTable,
360    load_mode: LoadMode,
361    connection_source: MssqlConnectionSource,
362    connection: MssqlConnectionSummary,
363    output_schema: Vec<MssqlOutputFieldReport>,
364    output_row_count: RowCount,
365    target_row_count_before_write: RowCount,
366    target_row_count: RowCount,
367    validation_status: ValidationStatus,
368    batch_shaping: MssqlBatchShapingReport,
369    phase_timings: Vec<PhaseTimingReport>,
370    stats: MssqlWriteStats,
371    partial_write_possible: bool,
372    cleanup: MssqlTargetCleanupStatus,
373}
374
375impl MssqlWriteReport {
376    /// Builds a write report from the already planned SQL Server output target.
377    #[must_use]
378    pub fn from_output_plan(
379        output_plan: &MssqlTargetOutputPlan,
380        rows_written: u64,
381        batches_written: u64,
382        elapsed_ms: u64,
383        partial_write_possible: bool,
384        cleanup: MssqlTargetCleanupStatus,
385    ) -> Self {
386        Self::from_output_plan_with_metrics(
387            output_plan,
388            MssqlWriteReportMetrics::new(
389                RowCount::exact(rows_written),
390                MssqlBatchShapingReport::completed(
391                    batches_written,
392                    rows_written,
393                    batches_written,
394                    rows_written,
395                ),
396                rows_written,
397                batches_written,
398                elapsed_ms,
399                partial_write_possible,
400                cleanup,
401            ),
402        )
403    }
404
405    pub(crate) fn from_output_plan_with_metrics(
406        output_plan: &MssqlTargetOutputPlan,
407        metrics: MssqlWriteReportMetrics,
408    ) -> Self {
409        let output_name = output_plan.output_name().to_owned();
410        let output_schema = output_plan
411            .schema_mappings()
412            .iter()
413            .map(MssqlOutputFieldReport::from_mapping)
414            .collect();
415
416        Self {
417            output_name: output_name.clone(),
418            target_table: output_plan.target_table().clone(),
419            load_mode: output_plan.load_mode(),
420            connection_source: output_plan.connection_source(),
421            connection: output_plan.connection().clone(),
422            output_schema,
423            output_row_count: metrics.output_row_count,
424            target_row_count_before_write: metrics.target_row_count_before_write,
425            target_row_count: metrics.target_row_count,
426            validation_status: metrics.validation_status,
427            batch_shaping: metrics.batch_shaping,
428            phase_timings: metrics.phase_timings,
429            stats: MssqlWriteStats::new(
430                output_name,
431                metrics.rows_written,
432                metrics.batches_written,
433                metrics.elapsed_ms,
434            ),
435            partial_write_possible: metrics.partial_write_possible,
436            cleanup: metrics.cleanup,
437        }
438    }
439
440    pub(crate) fn with_phase_timings(mut self, phase_timings: Vec<PhaseTimingReport>) -> Self {
441        let mut existing_timings = std::mem::take(&mut self.phase_timings);
442        let mut phase_timings = phase_timings;
443        phase_timings.append(&mut existing_timings);
444        self.phase_timings = phase_timings;
445        self
446    }
447
448    pub(crate) fn with_target_delta_validation(
449        mut self,
450        target_row_count_before_write: RowCount,
451        target_row_count_after_write: RowCount,
452        validation_status: ValidationStatus,
453        validation_timing: PhaseTimingReport,
454    ) -> Self {
455        self.target_row_count_before_write = target_row_count_before_write;
456        self.target_row_count = target_row_count_after_write;
457        self.validation_status = validation_status;
458        replace_phase_timing(&mut self.phase_timings, validation_timing);
459        self
460    }
461
462    pub(crate) fn with_appended_phase_timings(
463        mut self,
464        mut phase_timings: Vec<PhaseTimingReport>,
465    ) -> Self {
466        self.phase_timings.append(&mut phase_timings);
467        self
468    }
469
470    pub(crate) fn with_cleanup(mut self, cleanup: MssqlTargetCleanupStatus) -> Self {
471        self.cleanup = cleanup;
472        self
473    }
474
475    #[allow(dead_code)]
476    pub(crate) fn with_target_validation(
477        mut self,
478        target_row_count: RowCount,
479        validation_status: ValidationStatus,
480        validation_timing: PhaseTimingReport,
481    ) -> Self {
482        self.target_row_count = target_row_count;
483        self.validation_status = validation_status;
484        replace_phase_timing(&mut self.phase_timings, validation_timing);
485        self
486    }
487
488    /// Returns the selected output name.
489    #[must_use]
490    pub fn output_name(&self) -> &str {
491        &self.output_name
492    }
493
494    /// Returns the effective target table.
495    #[must_use]
496    pub fn target_table(&self) -> &MssqlTargetTable {
497        &self.target_table
498    }
499
500    /// Returns the requested target lifecycle mode.
501    #[must_use]
502    pub const fn load_mode(&self) -> LoadMode {
503        self.load_mode
504    }
505
506    /// Returns where the effective connection came from.
507    #[must_use]
508    pub const fn connection_source(&self) -> MssqlConnectionSource {
509        self.connection_source
510    }
511
512    /// Returns the redacted effective connection summary.
513    #[must_use]
514    pub const fn connection(&self) -> &MssqlConnectionSummary {
515        &self.connection
516    }
517
518    /// Returns per-output write statistics.
519    #[must_use]
520    pub const fn stats(&self) -> &MssqlWriteStats {
521        &self.stats
522    }
523
524    /// Returns the selected output schema fields.
525    #[must_use]
526    pub fn output_schema(&self) -> &[MssqlOutputFieldReport] {
527        &self.output_schema
528    }
529
530    /// Returns query output row evidence for the selected output stream.
531    #[must_use]
532    pub const fn output_row_count(&self) -> RowCount {
533        self.output_row_count
534    }
535
536    /// Returns target-side row count evidence after the SQL Server write.
537    #[must_use]
538    pub const fn target_row_count(&self) -> RowCount {
539        self.target_row_count
540    }
541
542    /// Returns target-side row count evidence before the SQL Server write.
543    /// For append-existing validation, concurrent target writes can affect the row-count delta.
544    #[must_use]
545    pub const fn target_row_count_before_write(&self) -> RowCount {
546        self.target_row_count_before_write
547    }
548
549    /// Returns target-side row count evidence after the SQL Server write.
550    /// For append-existing validation, concurrent target writes can affect the row-count delta.
551    #[must_use]
552    pub const fn target_row_count_after_write(&self) -> RowCount {
553        self.target_row_count
554    }
555
556    /// Returns target-side validation status for this output.
557    #[must_use]
558    pub const fn validation_status(&self) -> ValidationStatus {
559        self.validation_status
560    }
561
562    /// Returns identity batch-shaping counters for the selected output stream.
563    #[must_use]
564    pub const fn batch_shaping(&self) -> MssqlBatchShapingReport {
565        self.batch_shaping
566    }
567
568    /// Returns workflow phase timing reports for this output when available.
569    #[must_use]
570    pub fn phase_timings(&self) -> &[PhaseTimingReport] {
571        &self.phase_timings
572    }
573
574    /// Returns whether the target may contain a partial write after failure.
575    #[must_use]
576    pub const fn partial_write_possible(&self) -> bool {
577        self.partial_write_possible
578    }
579
580    /// Returns cleanup reporting state for DeltaFunnel-owned target cleanup.
581    #[must_use]
582    pub const fn cleanup(&self) -> MssqlTargetCleanupStatus {
583        self.cleanup
584    }
585}
586
587/// Redacted report for a successful planned-output schema validation.
588#[derive(Debug, Clone, PartialEq, Eq)]
589pub struct MssqlOutputBatchValidationReport {
590    output_name: String,
591    target_table: MssqlTargetTable,
592    load_mode: LoadMode,
593    connection_source: MssqlConnectionSource,
594    connection: MssqlConnectionSummary,
595}
596
597impl MssqlOutputBatchValidationReport {
598    /// Builds a validation report from the already planned SQL Server output target.
599    #[must_use]
600    pub fn from_output_plan(output_plan: &MssqlTargetOutputPlan) -> Self {
601        Self {
602            output_name: output_plan.output_name().to_owned(),
603            target_table: output_plan.target_table().clone(),
604            load_mode: output_plan.load_mode(),
605            connection_source: output_plan.connection_source(),
606            connection: output_plan.connection().clone(),
607        }
608    }
609
610    /// Returns the selected output name.
611    #[must_use]
612    pub fn output_name(&self) -> &str {
613        &self.output_name
614    }
615
616    /// Returns the effective target table.
617    #[must_use]
618    pub const fn target_table(&self) -> &MssqlTargetTable {
619        &self.target_table
620    }
621
622    /// Returns the requested target lifecycle mode.
623    #[must_use]
624    pub const fn load_mode(&self) -> LoadMode {
625        self.load_mode
626    }
627
628    /// Returns where the effective connection came from.
629    #[must_use]
630    pub const fn connection_source(&self) -> MssqlConnectionSource {
631        self.connection_source
632    }
633
634    /// Returns the redacted effective connection summary.
635    #[must_use]
636    pub const fn connection(&self) -> &MssqlConnectionSummary {
637        &self.connection
638    }
639}
640
641/// Structured context for a one-output SQL Server write failure.
642#[derive(Debug, Clone, PartialEq, Eq)]
643pub struct MssqlWriteFailureContext {
644    phase: MssqlWritePhase,
645    report: MssqlWriteReport,
646    diagnostics: Vec<MssqlWriteDiagnostic>,
647    cleanup_error: Option<String>,
648}
649
650impl MssqlWriteFailureContext {
651    /// Builds failure context from the already planned SQL Server output target.
652    #[must_use]
653    pub fn from_output_plan(
654        output_plan: &MssqlTargetOutputPlan,
655        phase: MssqlWritePhase,
656        rows_written: u64,
657        batches_written: u64,
658        elapsed_ms: u64,
659        partial_write_possible: bool,
660        cleanup: MssqlTargetCleanupStatus,
661    ) -> Self {
662        Self::from_output_plan_with_metrics(
663            output_plan,
664            phase,
665            MssqlWriteReportMetrics::new(
666                RowCount::partial(rows_written),
667                MssqlBatchShapingReport::failed(
668                    batches_written,
669                    rows_written,
670                    batches_written,
671                    rows_written,
672                ),
673                rows_written,
674                batches_written,
675                elapsed_ms,
676                partial_write_possible,
677                cleanup,
678            ),
679        )
680    }
681
682    pub(crate) fn from_output_plan_with_metrics(
683        output_plan: &MssqlTargetOutputPlan,
684        phase: MssqlWritePhase,
685        metrics: MssqlWriteReportMetrics,
686    ) -> Self {
687        Self {
688            phase,
689            report: MssqlWriteReport::from_output_plan_with_metrics(output_plan, metrics),
690            diagnostics: Vec::new(),
691            cleanup_error: None,
692        }
693    }
694
695    /// Returns the write phase associated with the failure.
696    #[must_use]
697    pub const fn phase(&self) -> MssqlWritePhase {
698        self.phase
699    }
700
701    /// Returns the selected output name.
702    #[must_use]
703    pub fn output_name(&self) -> &str {
704        self.report.output_name()
705    }
706
707    /// Returns the effective target table.
708    #[must_use]
709    pub fn target_table(&self) -> &MssqlTargetTable {
710        self.report.target_table()
711    }
712
713    /// Returns the requested target lifecycle mode.
714    #[must_use]
715    pub const fn load_mode(&self) -> LoadMode {
716        self.report.load_mode()
717    }
718
719    /// Returns where the effective connection came from.
720    #[must_use]
721    pub const fn connection_source(&self) -> MssqlConnectionSource {
722        self.report.connection_source()
723    }
724
725    /// Returns the redacted effective connection summary.
726    #[must_use]
727    pub const fn connection(&self) -> &MssqlConnectionSummary {
728        self.report.connection()
729    }
730
731    /// Returns accepted write statistics known at failure time.
732    #[must_use]
733    pub const fn stats(&self) -> &MssqlWriteStats {
734        self.report.stats()
735    }
736
737    /// Returns query output row evidence known at failure time.
738    #[must_use]
739    pub const fn output_row_count(&self) -> RowCount {
740        self.report.output_row_count()
741    }
742
743    /// Returns target-side row count evidence known at failure time.
744    #[must_use]
745    pub const fn target_row_count(&self) -> RowCount {
746        self.report.target_row_count()
747    }
748
749    /// Returns target-side row count evidence known before the write.
750    #[must_use]
751    pub const fn target_row_count_before_write(&self) -> RowCount {
752        self.report.target_row_count_before_write()
753    }
754
755    /// Returns target-side row count evidence known after the write.
756    #[must_use]
757    pub const fn target_row_count_after_write(&self) -> RowCount {
758        self.report.target_row_count_after_write()
759    }
760
761    /// Returns target-side validation status known at failure time.
762    #[must_use]
763    pub const fn validation_status(&self) -> ValidationStatus {
764        self.report.validation_status()
765    }
766
767    /// Returns identity batch-shaping counters known at failure time.
768    #[must_use]
769    pub const fn batch_shaping(&self) -> MssqlBatchShapingReport {
770        self.report.batch_shaping()
771    }
772
773    /// Returns whether the target may contain a partial write after failure.
774    #[must_use]
775    pub const fn partial_write_possible(&self) -> bool {
776        self.report.partial_write_possible()
777    }
778
779    /// Returns cleanup reporting state for DeltaFunnel-owned target cleanup.
780    #[must_use]
781    pub const fn cleanup(&self) -> MssqlTargetCleanupStatus {
782        self.report.cleanup()
783    }
784
785    /// Returns the redacted write report associated with the failure.
786    #[must_use]
787    pub const fn report(&self) -> &MssqlWriteReport {
788        &self.report
789    }
790
791    pub(crate) fn diagnostics(&self) -> &[MssqlWriteDiagnostic] {
792        &self.diagnostics
793    }
794
795    pub(crate) fn cleanup_error(&self) -> Option<&str> {
796        self.cleanup_error.as_deref()
797    }
798
799    pub(crate) fn with_diagnostics(mut self, diagnostics: Vec<MssqlWriteDiagnostic>) -> Self {
800        self.diagnostics = diagnostics;
801        self
802    }
803
804    pub(crate) fn with_cleanup_error(mut self, cleanup_error: impl AsRef<str>) -> Self {
805        self.cleanup_error = Some(sanitize_text_for_display(cleanup_error.as_ref()));
806        self
807    }
808
809    pub(crate) fn with_phase_timings(mut self, phase_timings: Vec<PhaseTimingReport>) -> Self {
810        self.report = self.report.with_phase_timings(phase_timings);
811        self
812    }
813
814    pub(crate) fn with_appended_phase_timings(
815        mut self,
816        phase_timings: Vec<PhaseTimingReport>,
817    ) -> Self {
818        self.report = self.report.with_appended_phase_timings(phase_timings);
819        self
820    }
821
822    /// Returns workflow phase timing reports known at failure time.
823    #[must_use]
824    pub fn phase_timings(&self) -> &[PhaseTimingReport] {
825        self.report.phase_timings()
826    }
827}
828
829#[allow(dead_code)]
830fn replace_phase_timing(phase_timings: &mut Vec<PhaseTimingReport>, timing: PhaseTimingReport) {
831    if let Some(existing_timing) = phase_timings
832        .iter_mut()
833        .find(|existing_timing| existing_timing.phase_name() == timing.phase_name())
834    {
835        *existing_timing = timing;
836    } else {
837        phase_timings.push(timing);
838    }
839}