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#[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 #[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 #[must_use]
37 pub fn output_name(&self) -> &str {
38 &self.output_name
39 }
40
41 #[must_use]
43 pub const fn rows_written(&self) -> u64 {
44 self.rows_written
45 }
46
47 #[must_use]
49 pub const fn batches_written(&self) -> u64 {
50 self.batches_written
51 }
52
53 #[must_use]
55 pub const fn elapsed_ms(&self) -> u64 {
56 self.elapsed_ms
57 }
58}
59
60#[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 #[must_use]
81 pub const fn index(&self) -> u64 {
82 self.index
83 }
84
85 #[must_use]
87 pub fn name(&self) -> &str {
88 &self.name
89 }
90
91 #[must_use]
93 pub fn arrow_type(&self) -> &str {
94 &self.arrow_type
95 }
96
97 #[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#[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 #[must_use]
233 pub const fn status(&self) -> PhaseStatus {
234 self.status
235 }
236
237 #[must_use]
239 pub const fn input_batches(&self) -> u64 {
240 self.input_batches
241 }
242
243 #[must_use]
245 pub const fn input_rows(&self) -> u64 {
246 self.input_rows
247 }
248
249 #[must_use]
251 pub const fn output_batches(&self) -> u64 {
252 self.output_batches
253 }
254
255 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
333pub enum MssqlTargetCleanupStatus {
334 NotApplicable,
336 NotAttempted,
338 Succeeded,
340 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#[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 #[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 #[must_use]
490 pub fn output_name(&self) -> &str {
491 &self.output_name
492 }
493
494 #[must_use]
496 pub fn target_table(&self) -> &MssqlTargetTable {
497 &self.target_table
498 }
499
500 #[must_use]
502 pub const fn load_mode(&self) -> LoadMode {
503 self.load_mode
504 }
505
506 #[must_use]
508 pub const fn connection_source(&self) -> MssqlConnectionSource {
509 self.connection_source
510 }
511
512 #[must_use]
514 pub const fn connection(&self) -> &MssqlConnectionSummary {
515 &self.connection
516 }
517
518 #[must_use]
520 pub const fn stats(&self) -> &MssqlWriteStats {
521 &self.stats
522 }
523
524 #[must_use]
526 pub fn output_schema(&self) -> &[MssqlOutputFieldReport] {
527 &self.output_schema
528 }
529
530 #[must_use]
532 pub const fn output_row_count(&self) -> RowCount {
533 self.output_row_count
534 }
535
536 #[must_use]
538 pub const fn target_row_count(&self) -> RowCount {
539 self.target_row_count
540 }
541
542 #[must_use]
545 pub const fn target_row_count_before_write(&self) -> RowCount {
546 self.target_row_count_before_write
547 }
548
549 #[must_use]
552 pub const fn target_row_count_after_write(&self) -> RowCount {
553 self.target_row_count
554 }
555
556 #[must_use]
558 pub const fn validation_status(&self) -> ValidationStatus {
559 self.validation_status
560 }
561
562 #[must_use]
564 pub const fn batch_shaping(&self) -> MssqlBatchShapingReport {
565 self.batch_shaping
566 }
567
568 #[must_use]
570 pub fn phase_timings(&self) -> &[PhaseTimingReport] {
571 &self.phase_timings
572 }
573
574 #[must_use]
576 pub const fn partial_write_possible(&self) -> bool {
577 self.partial_write_possible
578 }
579
580 #[must_use]
582 pub const fn cleanup(&self) -> MssqlTargetCleanupStatus {
583 self.cleanup
584 }
585}
586
587#[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 #[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 #[must_use]
612 pub fn output_name(&self) -> &str {
613 &self.output_name
614 }
615
616 #[must_use]
618 pub const fn target_table(&self) -> &MssqlTargetTable {
619 &self.target_table
620 }
621
622 #[must_use]
624 pub const fn load_mode(&self) -> LoadMode {
625 self.load_mode
626 }
627
628 #[must_use]
630 pub const fn connection_source(&self) -> MssqlConnectionSource {
631 self.connection_source
632 }
633
634 #[must_use]
636 pub const fn connection(&self) -> &MssqlConnectionSummary {
637 &self.connection
638 }
639}
640
641#[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 #[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 #[must_use]
697 pub const fn phase(&self) -> MssqlWritePhase {
698 self.phase
699 }
700
701 #[must_use]
703 pub fn output_name(&self) -> &str {
704 self.report.output_name()
705 }
706
707 #[must_use]
709 pub fn target_table(&self) -> &MssqlTargetTable {
710 self.report.target_table()
711 }
712
713 #[must_use]
715 pub const fn load_mode(&self) -> LoadMode {
716 self.report.load_mode()
717 }
718
719 #[must_use]
721 pub const fn connection_source(&self) -> MssqlConnectionSource {
722 self.report.connection_source()
723 }
724
725 #[must_use]
727 pub const fn connection(&self) -> &MssqlConnectionSummary {
728 self.report.connection()
729 }
730
731 #[must_use]
733 pub const fn stats(&self) -> &MssqlWriteStats {
734 self.report.stats()
735 }
736
737 #[must_use]
739 pub const fn output_row_count(&self) -> RowCount {
740 self.report.output_row_count()
741 }
742
743 #[must_use]
745 pub const fn target_row_count(&self) -> RowCount {
746 self.report.target_row_count()
747 }
748
749 #[must_use]
751 pub const fn target_row_count_before_write(&self) -> RowCount {
752 self.report.target_row_count_before_write()
753 }
754
755 #[must_use]
757 pub const fn target_row_count_after_write(&self) -> RowCount {
758 self.report.target_row_count_after_write()
759 }
760
761 #[must_use]
763 pub const fn validation_status(&self) -> ValidationStatus {
764 self.report.validation_status()
765 }
766
767 #[must_use]
769 pub const fn batch_shaping(&self) -> MssqlBatchShapingReport {
770 self.report.batch_shaping()
771 }
772
773 #[must_use]
775 pub const fn partial_write_possible(&self) -> bool {
776 self.report.partial_write_possible()
777 }
778
779 #[must_use]
781 pub const fn cleanup(&self) -> MssqlTargetCleanupStatus {
782 self.report.cleanup()
783 }
784
785 #[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 #[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}