1use std::{
9 fmt,
10 time::{Duration, Instant},
11};
12
13use crate::error::DeltaFunnelError;
14
15pub mod delta;
16mod json;
17pub mod sql_server;
18
19pub use delta::{
20 DeltaProtocolReport, DeltaProviderSchedulingReport, DeltaSourceReport, SourceUsageStatus,
21};
22pub use sql_server::{
23 MssqlDryRunOutputFieldReport, MssqlDryRunOutputReport, MssqlDryRunSqlIdentityReport,
24 MssqlDryRunSqlIdentityState, MssqlDryRunWorkflowReport, WriteAllCacheAliasReport,
25 WriteAllCacheAliasStatus, WriteAllCacheCandidateSkip, WriteAllCacheCandidateSkipReason,
26 WriteAllCacheReport, WriteAllNoCacheReason, WriteAllReport,
27};
28
29#[must_use]
31pub const fn usize_to_u64_saturating(value: usize) -> u64 {
32 if size_of::<usize>() > size_of::<u64>() && value > u64::MAX as usize {
33 u64::MAX
34 } else {
35 value as u64
36 }
37}
38
39#[must_use]
41pub const fn u128_to_u64_saturating(value: u128) -> u64 {
42 if value > u64::MAX as u128 {
43 u64::MAX
44 } else {
45 value as u64
46 }
47}
48
49#[must_use]
51pub fn duration_to_micros_saturating(duration: Duration) -> u64 {
52 u128_to_u64_saturating(duration.as_micros())
53}
54
55#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
57pub enum TargetValidationMode {
58 Disabled,
60 #[default]
62 ValidateIfPossible,
63 Require,
65}
66
67impl TargetValidationMode {
68 #[must_use]
70 pub const fn as_str(self) -> &'static str {
71 match self {
72 Self::Disabled => "disabled",
73 Self::ValidateIfPossible => "validate_if_possible",
74 Self::Require => "require",
75 }
76 }
77}
78
79impl fmt::Display for TargetValidationMode {
80 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
81 formatter.write_str(self.as_str())
82 }
83}
84
85#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
90pub enum DryRunScanSummaryMode {
91 #[default]
98 MetadataOnly,
99 ExhaustScanMetadata,
105}
106
107impl DryRunScanSummaryMode {
108 #[must_use]
110 pub const fn as_str(self) -> &'static str {
111 match self {
112 Self::MetadataOnly => "metadata_only",
113 Self::ExhaustScanMetadata => "exhaust_scan_metadata",
114 }
115 }
116}
117
118impl fmt::Display for DryRunScanSummaryMode {
119 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
120 formatter.write_str(self.as_str())
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub enum RowCountKind {
127 Exact,
129 Estimated,
131 Partial,
133 Unavailable,
135}
136
137impl RowCountKind {
138 #[must_use]
140 pub const fn as_str(self) -> &'static str {
141 match self {
142 Self::Exact => "exact",
143 Self::Estimated => "estimated",
144 Self::Partial => "partial",
145 Self::Unavailable => "unavailable",
146 }
147 }
148}
149
150impl fmt::Display for RowCountKind {
151 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
152 formatter.write_str(self.as_str())
153 }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum RowCount {
159 Exact(u64),
161 Estimated(u64),
163 Partial(u64),
165 Unavailable,
167}
168
169impl RowCount {
170 #[must_use]
172 pub const fn exact(value: u64) -> Self {
173 Self::Exact(value)
174 }
175
176 #[must_use]
178 pub const fn exact_from_usize(value: usize) -> Self {
179 Self::Exact(usize_to_u64_saturating(value))
180 }
181
182 #[must_use]
184 pub const fn exact_from_u128(value: u128) -> Self {
185 Self::Exact(u128_to_u64_saturating(value))
186 }
187
188 #[must_use]
190 pub const fn estimated(value: u64) -> Self {
191 Self::Estimated(value)
192 }
193
194 #[must_use]
196 pub const fn estimated_from_usize(value: usize) -> Self {
197 Self::Estimated(usize_to_u64_saturating(value))
198 }
199
200 #[must_use]
202 pub const fn estimated_from_u128(value: u128) -> Self {
203 Self::Estimated(u128_to_u64_saturating(value))
204 }
205
206 #[must_use]
208 pub const fn partial(value: u64) -> Self {
209 Self::Partial(value)
210 }
211
212 #[must_use]
214 pub const fn partial_from_usize(value: usize) -> Self {
215 Self::Partial(usize_to_u64_saturating(value))
216 }
217
218 #[must_use]
220 pub const fn partial_from_u128(value: u128) -> Self {
221 Self::Partial(u128_to_u64_saturating(value))
222 }
223
224 #[must_use]
226 pub const fn unavailable() -> Self {
227 Self::Unavailable
228 }
229
230 #[must_use]
232 pub const fn kind(&self) -> RowCountKind {
233 match self {
234 Self::Exact(_) => RowCountKind::Exact,
235 Self::Estimated(_) => RowCountKind::Estimated,
236 Self::Partial(_) => RowCountKind::Partial,
237 Self::Unavailable => RowCountKind::Unavailable,
238 }
239 }
240
241 #[must_use]
243 pub const fn value(&self) -> Option<u64> {
244 match self {
245 Self::Exact(value) | Self::Estimated(value) | Self::Partial(value) => Some(*value),
246 Self::Unavailable => None,
247 }
248 }
249
250 #[must_use]
252 pub const fn exact_value(&self) -> Option<u64> {
253 match self {
254 Self::Exact(value) => Some(*value),
255 Self::Estimated(_) | Self::Partial(_) | Self::Unavailable => None,
256 }
257 }
258
259 #[must_use]
261 pub const fn estimated_value(&self) -> Option<u64> {
262 match self {
263 Self::Estimated(value) => Some(*value),
264 Self::Exact(_) | Self::Partial(_) | Self::Unavailable => None,
265 }
266 }
267
268 #[must_use]
270 pub const fn partial_value(&self) -> Option<u64> {
271 match self {
272 Self::Partial(value) => Some(*value),
273 Self::Exact(_) | Self::Estimated(_) | Self::Unavailable => None,
274 }
275 }
276
277 #[must_use]
279 pub const fn is_exact(&self) -> bool {
280 matches!(self, Self::Exact(_))
281 }
282
283 #[must_use]
285 pub const fn is_estimated(&self) -> bool {
286 matches!(self, Self::Estimated(_))
287 }
288
289 #[must_use]
291 pub const fn is_partial(&self) -> bool {
292 matches!(self, Self::Partial(_))
293 }
294
295 #[must_use]
297 pub const fn is_unavailable(&self) -> bool {
298 matches!(self, Self::Unavailable)
299 }
300}
301
302impl fmt::Display for RowCount {
303 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
304 match self.value() {
305 Some(value) => write!(formatter, "{}:{value}", self.kind()),
306 None => formatter.write_str(self.kind().as_str()),
307 }
308 }
309}
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
313pub enum FileCountKind {
314 Exact,
316 Estimated,
318 Unavailable,
320 Skipped,
322 NotExecuted,
324}
325
326impl FileCountKind {
327 #[must_use]
329 pub const fn as_str(self) -> &'static str {
330 match self {
331 Self::Exact => "exact",
332 Self::Estimated => "estimated",
333 Self::Unavailable => "unavailable",
334 Self::Skipped => "skipped",
335 Self::NotExecuted => "not_executed",
336 }
337 }
338}
339
340impl fmt::Display for FileCountKind {
341 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
342 formatter.write_str(self.as_str())
343 }
344}
345
346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
348pub enum FileCount {
349 Exact(u64),
351 Estimated(u64),
353 Unavailable,
355 Skipped,
357 NotExecuted,
359}
360
361impl FileCount {
362 #[must_use]
364 pub const fn exact(value: u64) -> Self {
365 Self::Exact(value)
366 }
367
368 #[must_use]
370 pub const fn exact_from_usize(value: usize) -> Self {
371 Self::Exact(usize_to_u64_saturating(value))
372 }
373
374 #[must_use]
376 pub const fn exact_from_u128(value: u128) -> Self {
377 Self::Exact(u128_to_u64_saturating(value))
378 }
379
380 #[must_use]
382 pub const fn estimated(value: u64) -> Self {
383 Self::Estimated(value)
384 }
385
386 #[must_use]
388 pub const fn estimated_from_usize(value: usize) -> Self {
389 Self::Estimated(usize_to_u64_saturating(value))
390 }
391
392 #[must_use]
394 pub const fn estimated_from_u128(value: u128) -> Self {
395 Self::Estimated(u128_to_u64_saturating(value))
396 }
397
398 #[must_use]
400 pub const fn unavailable() -> Self {
401 Self::Unavailable
402 }
403
404 #[must_use]
406 pub const fn skipped() -> Self {
407 Self::Skipped
408 }
409
410 #[must_use]
412 pub const fn not_executed() -> Self {
413 Self::NotExecuted
414 }
415
416 #[must_use]
418 pub const fn kind(&self) -> FileCountKind {
419 match self {
420 Self::Exact(_) => FileCountKind::Exact,
421 Self::Estimated(_) => FileCountKind::Estimated,
422 Self::Unavailable => FileCountKind::Unavailable,
423 Self::Skipped => FileCountKind::Skipped,
424 Self::NotExecuted => FileCountKind::NotExecuted,
425 }
426 }
427
428 #[must_use]
430 pub const fn value(&self) -> Option<u64> {
431 match self {
432 Self::Exact(value) | Self::Estimated(value) => Some(*value),
433 Self::Unavailable | Self::Skipped | Self::NotExecuted => None,
434 }
435 }
436
437 #[must_use]
439 pub const fn exact_value(&self) -> Option<u64> {
440 match self {
441 Self::Exact(value) => Some(*value),
442 Self::Estimated(_) | Self::Unavailable | Self::Skipped | Self::NotExecuted => None,
443 }
444 }
445
446 #[must_use]
448 pub const fn estimated_value(&self) -> Option<u64> {
449 match self {
450 Self::Estimated(value) => Some(*value),
451 Self::Exact(_) | Self::Unavailable | Self::Skipped | Self::NotExecuted => None,
452 }
453 }
454
455 #[must_use]
457 pub const fn is_exact(&self) -> bool {
458 matches!(self, Self::Exact(_))
459 }
460
461 #[must_use]
463 pub const fn is_estimated(&self) -> bool {
464 matches!(self, Self::Estimated(_))
465 }
466
467 #[must_use]
469 pub const fn is_unavailable(&self) -> bool {
470 matches!(self, Self::Unavailable)
471 }
472
473 #[must_use]
475 pub const fn is_skipped(&self) -> bool {
476 matches!(self, Self::Skipped)
477 }
478
479 #[must_use]
481 pub const fn is_not_executed(&self) -> bool {
482 matches!(self, Self::NotExecuted)
483 }
484}
485
486impl fmt::Display for FileCount {
487 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
488 match self.value() {
489 Some(value) => write!(formatter, "{}:{value}", self.kind()),
490 None => formatter.write_str(self.kind().as_str()),
491 }
492 }
493}
494
495#[derive(Debug, Clone, Copy, PartialEq, Eq)]
501pub enum ReportReasonCode {
502 ValidationDisabled,
504 DryRun,
506 CapabilityUnavailable,
508 PermissionUnavailable,
510 PriorFailure,
512 UnsupportedLoadMode,
514 MissingTargetAccess,
516 MissingExactOutputRows,
518 CostAvoidance,
520 NotExecuted,
522 FailureBeforeValidation,
524}
525
526impl ReportReasonCode {
527 #[must_use]
529 pub const fn as_str(self) -> &'static str {
530 match self {
531 Self::ValidationDisabled => "validation_disabled",
532 Self::DryRun => "dry_run",
533 Self::CapabilityUnavailable => "capability_unavailable",
534 Self::PermissionUnavailable => "permission_unavailable",
535 Self::PriorFailure => "prior_failure",
536 Self::UnsupportedLoadMode => "unsupported_load_mode",
537 Self::MissingTargetAccess => "missing_target_access",
538 Self::MissingExactOutputRows => "missing_exact_output_rows",
539 Self::CostAvoidance => "cost_avoidance",
540 Self::NotExecuted => "not_executed",
541 Self::FailureBeforeValidation => "failure_before_validation",
542 }
543 }
544}
545
546impl fmt::Display for ReportReasonCode {
547 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
548 formatter.write_str(self.as_str())
549 }
550}
551
552#[derive(Debug, Clone, Copy, PartialEq, Eq)]
554pub enum ValidationStatusKind {
555 Disabled,
557 Passed,
559 Failed,
561 Skipped,
563 Unavailable,
565 RequiredButFailed,
567}
568
569impl ValidationStatusKind {
570 #[must_use]
572 pub const fn as_str(self) -> &'static str {
573 match self {
574 Self::Disabled => "disabled",
575 Self::Passed => "passed",
576 Self::Failed => "failed",
577 Self::Skipped => "skipped",
578 Self::Unavailable => "unavailable",
579 Self::RequiredButFailed => "required_but_failed",
580 }
581 }
582}
583
584impl fmt::Display for ValidationStatusKind {
585 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
586 formatter.write_str(self.as_str())
587 }
588}
589
590#[derive(Debug, Clone, Copy, PartialEq, Eq)]
592pub enum ValidationStatus {
593 Disabled {
595 reason: ReportReasonCode,
597 },
598 Passed,
600 Failed,
602 Skipped {
604 reason: ReportReasonCode,
606 },
607 Unavailable {
609 reason: ReportReasonCode,
611 },
612 RequiredButFailed {
614 reason: ReportReasonCode,
616 },
617}
618
619impl ValidationStatus {
620 #[must_use]
622 pub const fn disabled() -> Self {
623 Self::Disabled {
624 reason: ReportReasonCode::ValidationDisabled,
625 }
626 }
627
628 #[must_use]
630 pub const fn passed() -> Self {
631 Self::Passed
632 }
633
634 #[must_use]
636 pub const fn failed() -> Self {
637 Self::Failed
638 }
639
640 #[must_use]
642 pub const fn skipped(reason: ReportReasonCode) -> Self {
643 Self::Skipped { reason }
644 }
645
646 #[must_use]
648 pub const fn unavailable(reason: ReportReasonCode) -> Self {
649 Self::Unavailable { reason }
650 }
651
652 #[must_use]
654 pub const fn required_but_failed(reason: ReportReasonCode) -> Self {
655 Self::RequiredButFailed { reason }
656 }
657
658 #[must_use]
660 pub const fn kind(&self) -> ValidationStatusKind {
661 match self {
662 Self::Disabled { .. } => ValidationStatusKind::Disabled,
663 Self::Passed => ValidationStatusKind::Passed,
664 Self::Failed => ValidationStatusKind::Failed,
665 Self::Skipped { .. } => ValidationStatusKind::Skipped,
666 Self::Unavailable { .. } => ValidationStatusKind::Unavailable,
667 Self::RequiredButFailed { .. } => ValidationStatusKind::RequiredButFailed,
668 }
669 }
670
671 #[must_use]
673 pub const fn reason(&self) -> Option<ReportReasonCode> {
674 match self {
675 Self::Disabled { reason }
676 | Self::Skipped { reason }
677 | Self::Unavailable { reason }
678 | Self::RequiredButFailed { reason } => Some(*reason),
679 Self::Passed | Self::Failed => None,
680 }
681 }
682
683 #[must_use]
685 pub const fn is_disabled(&self) -> bool {
686 matches!(self, Self::Disabled { .. })
687 }
688
689 #[must_use]
691 pub const fn is_passed(&self) -> bool {
692 matches!(self, Self::Passed)
693 }
694
695 #[must_use]
697 pub const fn is_failed(&self) -> bool {
698 matches!(self, Self::Failed)
699 }
700
701 #[must_use]
703 pub const fn is_skipped(&self) -> bool {
704 matches!(self, Self::Skipped { .. })
705 }
706
707 #[must_use]
709 pub const fn is_unavailable(&self) -> bool {
710 matches!(self, Self::Unavailable { .. })
711 }
712
713 #[must_use]
715 pub const fn is_required_but_failed(&self) -> bool {
716 matches!(self, Self::RequiredButFailed { .. })
717 }
718
719 #[must_use]
721 pub const fn is_success(&self) -> bool {
722 matches!(self, Self::Passed)
723 }
724}
725
726impl fmt::Display for ValidationStatus {
727 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
728 match self.reason() {
729 Some(reason) => write!(formatter, "{}:{reason}", self.kind()),
730 None => formatter.write_str(self.kind().as_str()),
731 }
732 }
733}
734
735#[derive(Debug, Clone, Copy, PartialEq, Eq)]
737pub enum PhaseStatusKind {
738 Completed,
740 Failed,
742 Skipped,
744 NotStarted,
746 Unavailable,
748}
749
750impl PhaseStatusKind {
751 #[must_use]
753 pub const fn as_str(self) -> &'static str {
754 match self {
755 Self::Completed => "completed",
756 Self::Failed => "failed",
757 Self::Skipped => "skipped",
758 Self::NotStarted => "not_started",
759 Self::Unavailable => "unavailable",
760 }
761 }
762}
763
764impl fmt::Display for PhaseStatusKind {
765 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
766 formatter.write_str(self.as_str())
767 }
768}
769
770#[derive(Debug, Clone, Copy, PartialEq, Eq)]
772pub enum PhaseStatus {
773 Completed,
775 Failed,
777 Skipped {
779 reason: ReportReasonCode,
781 },
782 NotStarted {
784 reason: ReportReasonCode,
786 },
787 Unavailable {
789 reason: ReportReasonCode,
791 },
792}
793
794impl PhaseStatus {
795 #[must_use]
797 pub const fn completed() -> Self {
798 Self::Completed
799 }
800
801 #[must_use]
803 pub const fn failed() -> Self {
804 Self::Failed
805 }
806
807 #[must_use]
809 pub const fn skipped(reason: ReportReasonCode) -> Self {
810 Self::Skipped { reason }
811 }
812
813 #[must_use]
815 pub const fn not_started(reason: ReportReasonCode) -> Self {
816 Self::NotStarted { reason }
817 }
818
819 #[must_use]
821 pub const fn unavailable(reason: ReportReasonCode) -> Self {
822 Self::Unavailable { reason }
823 }
824
825 #[must_use]
827 pub const fn kind(&self) -> PhaseStatusKind {
828 match self {
829 Self::Completed => PhaseStatusKind::Completed,
830 Self::Failed => PhaseStatusKind::Failed,
831 Self::Skipped { .. } => PhaseStatusKind::Skipped,
832 Self::NotStarted { .. } => PhaseStatusKind::NotStarted,
833 Self::Unavailable { .. } => PhaseStatusKind::Unavailable,
834 }
835 }
836
837 #[must_use]
839 pub const fn reason(&self) -> Option<ReportReasonCode> {
840 match self {
841 Self::Skipped { reason }
842 | Self::NotStarted { reason }
843 | Self::Unavailable { reason } => Some(*reason),
844 Self::Completed | Self::Failed => None,
845 }
846 }
847
848 #[must_use]
850 pub const fn is_completed(&self) -> bool {
851 matches!(self, Self::Completed)
852 }
853
854 #[must_use]
856 pub const fn is_failed(&self) -> bool {
857 matches!(self, Self::Failed)
858 }
859
860 #[must_use]
862 pub const fn is_skipped(&self) -> bool {
863 matches!(self, Self::Skipped { .. })
864 }
865
866 #[must_use]
868 pub const fn is_not_started(&self) -> bool {
869 matches!(self, Self::NotStarted { .. })
870 }
871
872 #[must_use]
874 pub const fn is_unavailable(&self) -> bool {
875 matches!(self, Self::Unavailable { .. })
876 }
877}
878
879impl fmt::Display for PhaseStatus {
880 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
881 match self.reason() {
882 Some(reason) => write!(formatter, "{}:{reason}", self.kind()),
883 None => formatter.write_str(self.kind().as_str()),
884 }
885 }
886}
887
888#[derive(Debug, Clone, PartialEq, Eq)]
890pub struct PhaseTimingReport {
891 phase_name: String,
892 status: PhaseStatus,
893 elapsed_micros: Option<u64>,
894}
895
896impl PhaseTimingReport {
897 #[must_use]
899 pub fn completed(phase_name: impl Into<String>, elapsed: Duration) -> Self {
900 Self {
901 phase_name: phase_name.into(),
902 status: PhaseStatus::completed(),
903 elapsed_micros: Some(duration_to_micros_saturating(elapsed)),
904 }
905 }
906
907 #[must_use]
909 pub fn failed(phase_name: impl Into<String>, elapsed: Duration) -> Self {
910 Self {
911 phase_name: phase_name.into(),
912 status: PhaseStatus::failed(),
913 elapsed_micros: Some(duration_to_micros_saturating(elapsed)),
914 }
915 }
916
917 #[must_use]
919 pub fn skipped(phase_name: impl Into<String>, reason: ReportReasonCode) -> Self {
920 Self {
921 phase_name: phase_name.into(),
922 status: PhaseStatus::skipped(reason),
923 elapsed_micros: None,
924 }
925 }
926
927 #[must_use]
929 pub fn not_started(phase_name: impl Into<String>, reason: ReportReasonCode) -> Self {
930 Self {
931 phase_name: phase_name.into(),
932 status: PhaseStatus::not_started(reason),
933 elapsed_micros: None,
934 }
935 }
936
937 #[must_use]
939 pub fn unavailable(phase_name: impl Into<String>, reason: ReportReasonCode) -> Self {
940 Self {
941 phase_name: phase_name.into(),
942 status: PhaseStatus::unavailable(reason),
943 elapsed_micros: None,
944 }
945 }
946
947 #[must_use]
949 pub fn phase_name(&self) -> &str {
950 &self.phase_name
951 }
952
953 #[must_use]
955 pub const fn status(&self) -> PhaseStatus {
956 self.status
957 }
958
959 #[must_use]
961 pub const fn elapsed_micros(&self) -> Option<u64> {
962 self.elapsed_micros
963 }
964}
965
966#[derive(Debug)]
968pub(crate) struct PhaseTimer {
969 phase_name: &'static str,
970 started_at: Instant,
971}
972
973impl PhaseTimer {
974 pub(crate) fn start(phase_name: &'static str) -> Self {
975 Self {
976 phase_name,
977 started_at: Instant::now(),
978 }
979 }
980
981 pub(crate) fn completed(self) -> PhaseTimingReport {
982 PhaseTimingReport::completed(self.phase_name, self.started_at.elapsed())
983 }
984
985 pub(crate) fn failed(self) -> PhaseTimingReport {
986 PhaseTimingReport::failed(self.phase_name, self.started_at.elapsed())
987 }
988}
989
990#[derive(Debug, Clone, Copy, PartialEq, Eq)]
992pub enum OutputStatusKind {
993 Planned,
995 Succeeded,
997 Failed,
999 Skipped,
1001 DryRunPlanned,
1003 ValidationFailed,
1005}
1006
1007impl OutputStatusKind {
1008 #[must_use]
1010 pub const fn as_str(self) -> &'static str {
1011 match self {
1012 Self::Planned => "planned",
1013 Self::Succeeded => "succeeded",
1014 Self::Failed => "failed",
1015 Self::Skipped => "skipped",
1016 Self::DryRunPlanned => "dry_run_planned",
1017 Self::ValidationFailed => "validation_failed",
1018 }
1019 }
1020}
1021
1022impl fmt::Display for OutputStatusKind {
1023 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1024 formatter.write_str(self.as_str())
1025 }
1026}
1027
1028#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1030pub enum OutputStatus {
1031 Planned,
1033 Succeeded,
1035 Failed,
1037 Skipped {
1039 reason: ReportReasonCode,
1041 },
1042 DryRunPlanned,
1044 ValidationFailed {
1046 validation: ValidationStatus,
1048 },
1049}
1050
1051impl OutputStatus {
1052 #[must_use]
1054 pub const fn planned() -> Self {
1055 Self::Planned
1056 }
1057
1058 #[must_use]
1060 pub const fn succeeded() -> Self {
1061 Self::Succeeded
1062 }
1063
1064 #[must_use]
1066 pub const fn failed() -> Self {
1067 Self::Failed
1068 }
1069
1070 #[must_use]
1072 pub const fn skipped(reason: ReportReasonCode) -> Self {
1073 Self::Skipped { reason }
1074 }
1075
1076 #[must_use]
1078 pub const fn dry_run_planned() -> Self {
1079 Self::DryRunPlanned
1080 }
1081
1082 #[must_use]
1084 pub const fn validation_failed(validation: ValidationStatus) -> Self {
1085 Self::ValidationFailed { validation }
1086 }
1087
1088 #[must_use]
1090 pub const fn kind(&self) -> OutputStatusKind {
1091 match self {
1092 Self::Planned => OutputStatusKind::Planned,
1093 Self::Succeeded => OutputStatusKind::Succeeded,
1094 Self::Failed => OutputStatusKind::Failed,
1095 Self::Skipped { .. } => OutputStatusKind::Skipped,
1096 Self::DryRunPlanned => OutputStatusKind::DryRunPlanned,
1097 Self::ValidationFailed { .. } => OutputStatusKind::ValidationFailed,
1098 }
1099 }
1100
1101 #[must_use]
1103 pub const fn reason(&self) -> Option<ReportReasonCode> {
1104 match self {
1105 Self::Skipped { reason } => Some(*reason),
1106 Self::Planned
1107 | Self::Succeeded
1108 | Self::Failed
1109 | Self::DryRunPlanned
1110 | Self::ValidationFailed { .. } => None,
1111 }
1112 }
1113
1114 #[must_use]
1116 pub const fn validation(&self) -> Option<ValidationStatus> {
1117 match self {
1118 Self::ValidationFailed { validation } => Some(*validation),
1119 Self::Planned
1120 | Self::Succeeded
1121 | Self::Failed
1122 | Self::Skipped { .. }
1123 | Self::DryRunPlanned => None,
1124 }
1125 }
1126
1127 #[must_use]
1129 pub const fn is_planned(&self) -> bool {
1130 matches!(self, Self::Planned)
1131 }
1132
1133 #[must_use]
1135 pub const fn is_succeeded(&self) -> bool {
1136 matches!(self, Self::Succeeded)
1137 }
1138
1139 #[must_use]
1141 pub const fn is_failed(&self) -> bool {
1142 matches!(self, Self::Failed | Self::ValidationFailed { .. })
1143 }
1144
1145 #[must_use]
1147 pub const fn is_skipped(&self) -> bool {
1148 matches!(self, Self::Skipped { .. })
1149 }
1150
1151 #[must_use]
1153 pub const fn is_dry_run_planned(&self) -> bool {
1154 matches!(self, Self::DryRunPlanned)
1155 }
1156
1157 #[must_use]
1159 pub const fn is_validation_failed(&self) -> bool {
1160 matches!(self, Self::ValidationFailed { .. })
1161 }
1162}
1163
1164impl fmt::Display for OutputStatus {
1165 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1166 match self {
1167 Self::Skipped { reason } => write!(formatter, "{}:{reason}", self.kind()),
1168 Self::ValidationFailed { validation } => {
1169 write!(formatter, "{}:{validation}", self.kind())
1170 }
1171 Self::Planned | Self::Succeeded | Self::Failed | Self::DryRunPlanned => {
1172 formatter.write_str(self.kind().as_str())
1173 }
1174 }
1175 }
1176}
1177
1178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1180pub enum WorkflowStatusKind {
1181 Success,
1183 PartialSuccess,
1185 Failure,
1187 Skipped,
1189 NoOp,
1191}
1192
1193impl WorkflowStatusKind {
1194 #[must_use]
1196 pub const fn as_str(self) -> &'static str {
1197 match self {
1198 Self::Success => "success",
1199 Self::PartialSuccess => "partial_success",
1200 Self::Failure => "failure",
1201 Self::Skipped => "skipped",
1202 Self::NoOp => "no_op",
1203 }
1204 }
1205}
1206
1207impl fmt::Display for WorkflowStatusKind {
1208 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1209 formatter.write_str(self.as_str())
1210 }
1211}
1212
1213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1215pub enum WorkflowStatus {
1216 Success,
1218 PartialSuccess,
1220 Failure,
1222 Skipped {
1224 reason: ReportReasonCode,
1226 },
1227 NoOp {
1229 reason: ReportReasonCode,
1231 },
1232}
1233
1234impl WorkflowStatus {
1235 #[must_use]
1237 pub const fn success() -> Self {
1238 Self::Success
1239 }
1240
1241 #[must_use]
1243 pub const fn partial_success() -> Self {
1244 Self::PartialSuccess
1245 }
1246
1247 #[must_use]
1249 pub const fn failure() -> Self {
1250 Self::Failure
1251 }
1252
1253 #[must_use]
1255 pub const fn skipped(reason: ReportReasonCode) -> Self {
1256 Self::Skipped { reason }
1257 }
1258
1259 #[must_use]
1261 pub const fn no_op(reason: ReportReasonCode) -> Self {
1262 Self::NoOp { reason }
1263 }
1264
1265 #[must_use]
1267 pub const fn kind(&self) -> WorkflowStatusKind {
1268 match self {
1269 Self::Success => WorkflowStatusKind::Success,
1270 Self::PartialSuccess => WorkflowStatusKind::PartialSuccess,
1271 Self::Failure => WorkflowStatusKind::Failure,
1272 Self::Skipped { .. } => WorkflowStatusKind::Skipped,
1273 Self::NoOp { .. } => WorkflowStatusKind::NoOp,
1274 }
1275 }
1276
1277 #[must_use]
1279 pub const fn reason(&self) -> Option<ReportReasonCode> {
1280 match self {
1281 Self::Skipped { reason } | Self::NoOp { reason } => Some(*reason),
1282 Self::Success | Self::PartialSuccess | Self::Failure => None,
1283 }
1284 }
1285
1286 #[must_use]
1288 pub const fn is_success(&self) -> bool {
1289 matches!(self, Self::Success)
1290 }
1291
1292 #[must_use]
1294 pub const fn is_partial_success(&self) -> bool {
1295 matches!(self, Self::PartialSuccess)
1296 }
1297
1298 #[must_use]
1300 pub const fn is_failure(&self) -> bool {
1301 matches!(self, Self::Failure)
1302 }
1303
1304 #[must_use]
1306 pub const fn is_skipped(&self) -> bool {
1307 matches!(self, Self::Skipped { .. })
1308 }
1309
1310 #[must_use]
1312 pub const fn is_no_op(&self) -> bool {
1313 matches!(self, Self::NoOp { .. })
1314 }
1315}
1316
1317impl fmt::Display for WorkflowStatus {
1318 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1319 match self.reason() {
1320 Some(reason) => write!(formatter, "{}:{reason}", self.kind()),
1321 None => formatter.write_str(self.kind().as_str()),
1322 }
1323 }
1324}
1325
1326#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1336pub struct ValidationOptions {
1337 target_validation_mode: TargetValidationMode,
1340 dry_run_scan_summary_mode: DryRunScanSummaryMode,
1344 require_successful_planning: bool,
1347}
1348
1349impl Default for ValidationOptions {
1350 fn default() -> Self {
1351 Self::new()
1352 }
1353}
1354
1355impl ValidationOptions {
1356 #[must_use]
1358 pub const fn new() -> Self {
1359 Self {
1360 target_validation_mode: TargetValidationMode::ValidateIfPossible,
1361 dry_run_scan_summary_mode: DryRunScanSummaryMode::MetadataOnly,
1362 require_successful_planning: true,
1363 }
1364 }
1365
1366 #[must_use]
1368 pub const fn with_target_validation_mode(
1369 mut self,
1370 target_validation_mode: TargetValidationMode,
1371 ) -> Self {
1372 self.target_validation_mode = target_validation_mode;
1373 self
1374 }
1375
1376 #[must_use]
1378 pub const fn with_dry_run_scan_summary_mode(
1379 mut self,
1380 dry_run_scan_summary_mode: DryRunScanSummaryMode,
1381 ) -> Self {
1382 self.dry_run_scan_summary_mode = dry_run_scan_summary_mode;
1383 self
1384 }
1385
1386 #[must_use]
1388 pub const fn with_require_successful_planning(
1389 mut self,
1390 require_successful_planning: bool,
1391 ) -> Self {
1392 self.require_successful_planning = require_successful_planning;
1393 self
1394 }
1395
1396 #[must_use]
1398 pub const fn target_validation_mode(&self) -> TargetValidationMode {
1399 self.target_validation_mode
1400 }
1401
1402 #[must_use]
1404 pub const fn dry_run_scan_summary_mode(&self) -> DryRunScanSummaryMode {
1405 self.dry_run_scan_summary_mode
1406 }
1407
1408 #[must_use]
1410 pub const fn require_successful_planning(&self) -> bool {
1411 self.require_successful_planning
1412 }
1413
1414 pub const fn validate(&self) -> Result<(), DeltaFunnelError> {
1422 let _ = self.target_validation_mode;
1423 let _ = self.dry_run_scan_summary_mode;
1424 let _ = self.require_successful_planning;
1425 Ok(())
1426 }
1427}
1428
1429#[cfg(test)]
1430mod tests {
1431 use std::time::Duration;
1432
1433 use super::{
1434 DryRunScanSummaryMode, FileCount, FileCountKind, OutputStatus, OutputStatusKind,
1435 PhaseStatus, PhaseStatusKind, PhaseTimer, PhaseTimingReport, ReportReasonCode, RowCount,
1436 RowCountKind, TargetValidationMode, ValidationOptions, ValidationStatus,
1437 ValidationStatusKind, WorkflowStatus, WorkflowStatusKind, duration_to_micros_saturating,
1438 u128_to_u64_saturating,
1439 };
1440
1441 #[test]
1442 fn target_validation_modes_expose_stable_codes() {
1443 assert_eq!(TargetValidationMode::Disabled.as_str(), "disabled");
1444 assert_eq!(
1445 TargetValidationMode::ValidateIfPossible.as_str(),
1446 "validate_if_possible"
1447 );
1448 assert_eq!(TargetValidationMode::Require.as_str(), "require");
1449 assert_eq!(
1450 TargetValidationMode::ValidateIfPossible.to_string(),
1451 "validate_if_possible"
1452 );
1453 }
1454
1455 #[test]
1456 fn dry_run_scan_summary_modes_expose_stable_codes() {
1457 assert_eq!(
1458 DryRunScanSummaryMode::MetadataOnly.as_str(),
1459 "metadata_only"
1460 );
1461 assert_eq!(
1462 DryRunScanSummaryMode::ExhaustScanMetadata.as_str(),
1463 "exhaust_scan_metadata"
1464 );
1465 assert_eq!(
1466 DryRunScanSummaryMode::ExhaustScanMetadata.to_string(),
1467 "exhaust_scan_metadata"
1468 );
1469 }
1470
1471 #[test]
1472 fn validation_options_default_preserves_planning_intent() {
1473 let options = ValidationOptions::default();
1474
1475 assert_eq!(
1476 options.target_validation_mode(),
1477 TargetValidationMode::ValidateIfPossible
1478 );
1479 assert_eq!(
1480 options.dry_run_scan_summary_mode(),
1481 DryRunScanSummaryMode::MetadataOnly
1482 );
1483 assert!(options.require_successful_planning());
1484 assert!(options.validate().is_ok());
1485 }
1486
1487 #[test]
1488 fn validation_options_accessors_return_configured_values() {
1489 let options = ValidationOptions::new()
1490 .with_target_validation_mode(TargetValidationMode::Require)
1491 .with_dry_run_scan_summary_mode(DryRunScanSummaryMode::ExhaustScanMetadata)
1492 .with_require_successful_planning(false);
1493
1494 assert_eq!(
1495 options.target_validation_mode(),
1496 TargetValidationMode::Require
1497 );
1498 assert_eq!(
1499 options.dry_run_scan_summary_mode(),
1500 DryRunScanSummaryMode::ExhaustScanMetadata
1501 );
1502 assert!(!options.require_successful_planning());
1503 }
1504
1505 #[test]
1506 fn validation_options_debug_does_not_include_external_values() {
1507 let options = ValidationOptions::new()
1508 .with_target_validation_mode(TargetValidationMode::Disabled)
1509 .with_dry_run_scan_summary_mode(DryRunScanSummaryMode::MetadataOnly);
1510
1511 let debug = format!("{options:?}");
1512
1513 assert!(debug.contains("Disabled"));
1514 assert!(debug.contains("MetadataOnly"));
1515 assert!(!debug.contains("server="));
1516 assert!(!debug.contains("password"));
1517 }
1518
1519 #[test]
1520 fn row_count_variants_expose_kind_and_values() {
1521 let exact = RowCount::exact(10);
1522 let estimated = RowCount::estimated(20);
1523 let partial = RowCount::partial(30);
1524 let unavailable = RowCount::unavailable();
1525
1526 assert_eq!(exact.kind(), RowCountKind::Exact);
1527 assert_eq!(exact.value(), Some(10));
1528 assert_eq!(exact.exact_value(), Some(10));
1529 assert!(exact.is_exact());
1530
1531 assert_eq!(estimated.kind(), RowCountKind::Estimated);
1532 assert_eq!(estimated.value(), Some(20));
1533 assert_eq!(estimated.estimated_value(), Some(20));
1534 assert!(estimated.is_estimated());
1535
1536 assert_eq!(partial.kind(), RowCountKind::Partial);
1537 assert_eq!(partial.value(), Some(30));
1538 assert_eq!(partial.partial_value(), Some(30));
1539 assert_eq!(partial.exact_value(), None);
1540 assert!(partial.is_partial());
1541
1542 assert_eq!(unavailable.kind(), RowCountKind::Unavailable);
1543 assert_eq!(unavailable.value(), None);
1544 assert!(unavailable.is_unavailable());
1545 }
1546
1547 #[test]
1548 fn file_count_variants_expose_kind_and_values() {
1549 let exact = FileCount::exact(2);
1550 let estimated = FileCount::estimated(3);
1551 let unavailable = FileCount::unavailable();
1552 let skipped = FileCount::skipped();
1553 let not_executed = FileCount::not_executed();
1554
1555 assert_eq!(exact.kind(), FileCountKind::Exact);
1556 assert_eq!(exact.value(), Some(2));
1557 assert_eq!(exact.exact_value(), Some(2));
1558 assert!(exact.is_exact());
1559
1560 assert_eq!(estimated.kind(), FileCountKind::Estimated);
1561 assert_eq!(estimated.value(), Some(3));
1562 assert_eq!(estimated.estimated_value(), Some(3));
1563 assert!(estimated.is_estimated());
1564
1565 assert_eq!(unavailable.kind(), FileCountKind::Unavailable);
1566 assert_eq!(unavailable.value(), None);
1567 assert!(unavailable.is_unavailable());
1568
1569 assert_eq!(skipped.kind(), FileCountKind::Skipped);
1570 assert_eq!(skipped.value(), None);
1571 assert!(skipped.is_skipped());
1572
1573 assert_eq!(not_executed.kind(), FileCountKind::NotExecuted);
1574 assert_eq!(not_executed.value(), None);
1575 assert!(not_executed.is_not_executed());
1576 }
1577
1578 #[test]
1579 fn count_kinds_expose_stable_codes() {
1580 assert_eq!(RowCountKind::Exact.as_str(), "exact");
1581 assert_eq!(RowCountKind::Estimated.as_str(), "estimated");
1582 assert_eq!(RowCountKind::Partial.as_str(), "partial");
1583 assert_eq!(RowCountKind::Unavailable.as_str(), "unavailable");
1584 assert_eq!(RowCountKind::Partial.to_string(), "partial");
1585
1586 assert_eq!(FileCountKind::Exact.as_str(), "exact");
1587 assert_eq!(FileCountKind::Estimated.as_str(), "estimated");
1588 assert_eq!(FileCountKind::Unavailable.as_str(), "unavailable");
1589 assert_eq!(FileCountKind::Skipped.as_str(), "skipped");
1590 assert_eq!(FileCountKind::NotExecuted.as_str(), "not_executed");
1591 assert_eq!(FileCountKind::NotExecuted.to_string(), "not_executed");
1592 }
1593
1594 #[test]
1595 fn count_display_is_stable_and_safe() {
1596 assert_eq!(RowCount::exact(12).to_string(), "exact:12");
1597 assert_eq!(RowCount::estimated(34).to_string(), "estimated:34");
1598 assert_eq!(RowCount::partial(56).to_string(), "partial:56");
1599 assert_eq!(RowCount::unavailable().to_string(), "unavailable");
1600
1601 assert_eq!(FileCount::exact(7).to_string(), "exact:7");
1602 assert_eq!(FileCount::estimated(8).to_string(), "estimated:8");
1603 assert_eq!(FileCount::unavailable().to_string(), "unavailable");
1604 assert_eq!(FileCount::skipped().to_string(), "skipped");
1605 assert_eq!(FileCount::not_executed().to_string(), "not_executed");
1606
1607 let debug = format!("{:?} {:?}", RowCount::partial(1), FileCount::not_executed());
1608 assert!(!debug.contains("server="));
1609 assert!(!debug.contains("password"));
1610 }
1611
1612 #[test]
1613 fn count_constructors_saturate_wide_values() {
1614 assert_eq!(u128_to_u64_saturating(u128::from(u64::MAX) + 1), u64::MAX);
1615 assert_eq!(RowCount::exact_from_u128(u128::MAX).value(), Some(u64::MAX));
1616 assert_eq!(
1617 RowCount::estimated_from_u128(u128::MAX).value(),
1618 Some(u64::MAX)
1619 );
1620 assert_eq!(
1621 RowCount::partial_from_u128(u128::MAX).value(),
1622 Some(u64::MAX)
1623 );
1624 assert_eq!(
1625 FileCount::exact_from_u128(u128::MAX).value(),
1626 Some(u64::MAX)
1627 );
1628 assert_eq!(
1629 FileCount::estimated_from_u128(u128::MAX).value(),
1630 Some(u64::MAX)
1631 );
1632
1633 assert_eq!(RowCount::exact_from_usize(5).value(), Some(5));
1634 assert_eq!(RowCount::estimated_from_usize(6).value(), Some(6));
1635 assert_eq!(RowCount::partial_from_usize(7).value(), Some(7));
1636 assert_eq!(FileCount::exact_from_usize(8).value(), Some(8));
1637 assert_eq!(FileCount::estimated_from_usize(9).value(), Some(9));
1638 }
1639
1640 #[test]
1641 fn report_reason_codes_cover_stable_skip_and_unavailable_reasons() {
1642 let cases = [
1643 (ReportReasonCode::ValidationDisabled, "validation_disabled"),
1644 (ReportReasonCode::DryRun, "dry_run"),
1645 (
1646 ReportReasonCode::CapabilityUnavailable,
1647 "capability_unavailable",
1648 ),
1649 (
1650 ReportReasonCode::PermissionUnavailable,
1651 "permission_unavailable",
1652 ),
1653 (ReportReasonCode::PriorFailure, "prior_failure"),
1654 (
1655 ReportReasonCode::UnsupportedLoadMode,
1656 "unsupported_load_mode",
1657 ),
1658 (
1659 ReportReasonCode::MissingTargetAccess,
1660 "missing_target_access",
1661 ),
1662 (
1663 ReportReasonCode::MissingExactOutputRows,
1664 "missing_exact_output_rows",
1665 ),
1666 (ReportReasonCode::CostAvoidance, "cost_avoidance"),
1667 (ReportReasonCode::NotExecuted, "not_executed"),
1668 (
1669 ReportReasonCode::FailureBeforeValidation,
1670 "failure_before_validation",
1671 ),
1672 ];
1673
1674 for (reason, code) in cases {
1675 assert_eq!(reason.as_str(), code);
1676 assert_eq!(reason.to_string(), code);
1677 }
1678 }
1679
1680 #[test]
1681 fn report_reason_debug_does_not_include_external_values() {
1682 let debug = format!("{:?}", ReportReasonCode::MissingTargetAccess);
1683
1684 assert!(debug.contains("MissingTargetAccess"));
1685 assert!(!debug.contains("server="));
1686 assert!(!debug.contains("password"));
1687 }
1688
1689 #[test]
1690 fn validation_status_kinds_expose_stable_codes() {
1691 assert_eq!(ValidationStatusKind::Disabled.as_str(), "disabled");
1692 assert_eq!(ValidationStatusKind::Passed.as_str(), "passed");
1693 assert_eq!(ValidationStatusKind::Failed.as_str(), "failed");
1694 assert_eq!(ValidationStatusKind::Skipped.as_str(), "skipped");
1695 assert_eq!(ValidationStatusKind::Unavailable.as_str(), "unavailable");
1696 assert_eq!(
1697 ValidationStatusKind::RequiredButFailed.as_str(),
1698 "required_but_failed"
1699 );
1700 assert_eq!(
1701 ValidationStatusKind::RequiredButFailed.to_string(),
1702 "required_but_failed"
1703 );
1704 }
1705
1706 #[test]
1707 fn validation_status_variants_expose_kind_reasons_and_helpers() {
1708 let disabled = ValidationStatus::disabled();
1709 let passed = ValidationStatus::passed();
1710 let failed = ValidationStatus::failed();
1711 let skipped = ValidationStatus::skipped(ReportReasonCode::DryRun);
1712 let unavailable = ValidationStatus::unavailable(ReportReasonCode::MissingTargetAccess);
1713 let required =
1714 ValidationStatus::required_but_failed(ReportReasonCode::MissingExactOutputRows);
1715
1716 assert_eq!(disabled.kind(), ValidationStatusKind::Disabled);
1717 assert_eq!(
1718 disabled.reason(),
1719 Some(ReportReasonCode::ValidationDisabled)
1720 );
1721 assert!(disabled.is_disabled());
1722
1723 assert_eq!(passed.kind(), ValidationStatusKind::Passed);
1724 assert_eq!(passed.reason(), None);
1725 assert!(passed.is_passed());
1726 assert!(passed.is_success());
1727
1728 assert_eq!(failed.kind(), ValidationStatusKind::Failed);
1729 assert_eq!(failed.reason(), None);
1730 assert!(failed.is_failed());
1731 assert!(!failed.is_success());
1732
1733 assert_eq!(skipped.kind(), ValidationStatusKind::Skipped);
1734 assert_eq!(skipped.reason(), Some(ReportReasonCode::DryRun));
1735 assert!(skipped.is_skipped());
1736
1737 assert_eq!(unavailable.kind(), ValidationStatusKind::Unavailable);
1738 assert_eq!(
1739 unavailable.reason(),
1740 Some(ReportReasonCode::MissingTargetAccess)
1741 );
1742 assert!(unavailable.is_unavailable());
1743
1744 assert_eq!(required.kind(), ValidationStatusKind::RequiredButFailed);
1745 assert_eq!(
1746 required.reason(),
1747 Some(ReportReasonCode::MissingExactOutputRows)
1748 );
1749 assert!(required.is_required_but_failed());
1750 }
1751
1752 #[test]
1753 fn validation_status_display_is_stable_and_safe() {
1754 assert_eq!(
1755 ValidationStatus::disabled().to_string(),
1756 "disabled:validation_disabled"
1757 );
1758 assert_eq!(ValidationStatus::passed().to_string(), "passed");
1759 assert_eq!(ValidationStatus::failed().to_string(), "failed");
1760 assert_eq!(
1761 ValidationStatus::skipped(ReportReasonCode::DryRun).to_string(),
1762 "skipped:dry_run"
1763 );
1764 assert_eq!(
1765 ValidationStatus::unavailable(ReportReasonCode::PermissionUnavailable).to_string(),
1766 "unavailable:permission_unavailable"
1767 );
1768 assert_eq!(
1769 ValidationStatus::required_but_failed(ReportReasonCode::MissingExactOutputRows)
1770 .to_string(),
1771 "required_but_failed:missing_exact_output_rows"
1772 );
1773
1774 let debug = format!(
1775 "{:?}",
1776 ValidationStatus::required_but_failed(ReportReasonCode::MissingTargetAccess)
1777 );
1778 assert!(debug.contains("RequiredButFailed"));
1779 assert!(!debug.contains("server="));
1780 assert!(!debug.contains("password"));
1781 }
1782
1783 #[test]
1784 fn phase_status_kinds_expose_stable_codes() {
1785 assert_eq!(PhaseStatusKind::Completed.as_str(), "completed");
1786 assert_eq!(PhaseStatusKind::Failed.as_str(), "failed");
1787 assert_eq!(PhaseStatusKind::Skipped.as_str(), "skipped");
1788 assert_eq!(PhaseStatusKind::NotStarted.as_str(), "not_started");
1789 assert_eq!(PhaseStatusKind::Unavailable.as_str(), "unavailable");
1790 assert_eq!(PhaseStatusKind::NotStarted.to_string(), "not_started");
1791 }
1792
1793 #[test]
1794 fn phase_status_variants_expose_kind_reasons_and_helpers() {
1795 let completed = PhaseStatus::completed();
1796 let failed = PhaseStatus::failed();
1797 let skipped = PhaseStatus::skipped(ReportReasonCode::DryRun);
1798 let not_started = PhaseStatus::not_started(ReportReasonCode::PriorFailure);
1799 let unavailable = PhaseStatus::unavailable(ReportReasonCode::CapabilityUnavailable);
1800
1801 assert_eq!(completed.kind(), PhaseStatusKind::Completed);
1802 assert_eq!(completed.reason(), None);
1803 assert!(completed.is_completed());
1804
1805 assert_eq!(failed.kind(), PhaseStatusKind::Failed);
1806 assert_eq!(failed.reason(), None);
1807 assert!(failed.is_failed());
1808
1809 assert_eq!(skipped.kind(), PhaseStatusKind::Skipped);
1810 assert_eq!(skipped.reason(), Some(ReportReasonCode::DryRun));
1811 assert!(skipped.is_skipped());
1812
1813 assert_eq!(not_started.kind(), PhaseStatusKind::NotStarted);
1814 assert_eq!(not_started.reason(), Some(ReportReasonCode::PriorFailure));
1815 assert!(not_started.is_not_started());
1816
1817 assert_eq!(unavailable.kind(), PhaseStatusKind::Unavailable);
1818 assert_eq!(
1819 unavailable.reason(),
1820 Some(ReportReasonCode::CapabilityUnavailable)
1821 );
1822 assert!(unavailable.is_unavailable());
1823 }
1824
1825 #[test]
1826 fn phase_status_display_is_stable_and_safe() {
1827 assert_eq!(PhaseStatus::completed().to_string(), "completed");
1828 assert_eq!(PhaseStatus::failed().to_string(), "failed");
1829 assert_eq!(
1830 PhaseStatus::skipped(ReportReasonCode::DryRun).to_string(),
1831 "skipped:dry_run"
1832 );
1833 assert_eq!(
1834 PhaseStatus::not_started(ReportReasonCode::PriorFailure).to_string(),
1835 "not_started:prior_failure"
1836 );
1837 assert_eq!(
1838 PhaseStatus::unavailable(ReportReasonCode::CapabilityUnavailable).to_string(),
1839 "unavailable:capability_unavailable"
1840 );
1841
1842 let debug = format!(
1843 "{:?}",
1844 PhaseStatus::unavailable(ReportReasonCode::MissingTargetAccess)
1845 );
1846 assert!(debug.contains("Unavailable"));
1847 assert!(!debug.contains("server="));
1848 assert!(!debug.contains("password"));
1849 }
1850
1851 #[test]
1852 fn duration_to_micros_saturates_wide_durations() {
1853 assert_eq!(duration_to_micros_saturating(Duration::from_micros(42)), 42);
1854 assert_eq!(
1855 duration_to_micros_saturating(Duration::new(u64::MAX, 999_999_999)),
1856 u64::MAX
1857 );
1858 }
1859
1860 #[test]
1861 fn phase_timing_report_preserves_status_reason_and_elapsed_time() {
1862 let completed =
1863 PhaseTimingReport::completed("sql_target_planning", Duration::from_micros(1_234));
1864 assert_eq!(completed.phase_name(), "sql_target_planning");
1865 assert_eq!(completed.status(), PhaseStatus::completed());
1866 assert_eq!(completed.elapsed_micros(), Some(1_234));
1867
1868 let failed = PhaseTimingReport::failed("sql_write", Duration::from_millis(2));
1869 assert_eq!(failed.status(), PhaseStatus::failed());
1870 assert_eq!(failed.elapsed_micros(), Some(2_000));
1871
1872 let skipped =
1873 PhaseTimingReport::skipped("validation", ReportReasonCode::ValidationDisabled);
1874 assert_eq!(
1875 skipped.status(),
1876 PhaseStatus::skipped(ReportReasonCode::ValidationDisabled)
1877 );
1878 assert_eq!(skipped.elapsed_micros(), None);
1879
1880 let not_started =
1881 PhaseTimingReport::not_started("finalize", ReportReasonCode::PriorFailure);
1882 assert_eq!(
1883 not_started.status(),
1884 PhaseStatus::not_started(ReportReasonCode::PriorFailure)
1885 );
1886 assert_eq!(not_started.elapsed_micros(), None);
1887
1888 let unavailable =
1889 PhaseTimingReport::unavailable("source_loading", ReportReasonCode::NotExecuted);
1890 assert_eq!(
1891 unavailable.status(),
1892 PhaseStatus::unavailable(ReportReasonCode::NotExecuted)
1893 );
1894 assert_eq!(unavailable.elapsed_micros(), None);
1895 }
1896
1897 #[test]
1898 fn phase_timer_uses_monotonic_elapsed_time() {
1899 let completed = PhaseTimer::start("config_validation").completed();
1900 assert_eq!(completed.phase_name(), "config_validation");
1901 assert_eq!(completed.status(), PhaseStatus::completed());
1902 assert!(completed.elapsed_micros().is_some());
1903
1904 let failed = PhaseTimer::start("source_loading").failed();
1905 assert_eq!(failed.phase_name(), "source_loading");
1906 assert_eq!(failed.status(), PhaseStatus::failed());
1907 assert!(failed.elapsed_micros().is_some());
1908 }
1909
1910 #[test]
1911 fn output_status_kinds_expose_stable_codes() {
1912 assert_eq!(OutputStatusKind::Planned.as_str(), "planned");
1913 assert_eq!(OutputStatusKind::Succeeded.as_str(), "succeeded");
1914 assert_eq!(OutputStatusKind::Failed.as_str(), "failed");
1915 assert_eq!(OutputStatusKind::Skipped.as_str(), "skipped");
1916 assert_eq!(OutputStatusKind::DryRunPlanned.as_str(), "dry_run_planned");
1917 assert_eq!(
1918 OutputStatusKind::ValidationFailed.as_str(),
1919 "validation_failed"
1920 );
1921 assert_eq!(
1922 OutputStatusKind::DryRunPlanned.to_string(),
1923 "dry_run_planned"
1924 );
1925 }
1926
1927 #[test]
1928 fn output_status_variants_expose_kind_reasons_validation_and_helpers() {
1929 let planned = OutputStatus::planned();
1930 let succeeded = OutputStatus::succeeded();
1931 let failed = OutputStatus::failed();
1932 let skipped = OutputStatus::skipped(ReportReasonCode::PriorFailure);
1933 let dry_run = OutputStatus::dry_run_planned();
1934 let validation =
1935 ValidationStatus::required_but_failed(ReportReasonCode::MissingExactOutputRows);
1936 let validation_failed = OutputStatus::validation_failed(validation);
1937
1938 assert_eq!(planned.kind(), OutputStatusKind::Planned);
1939 assert_eq!(planned.reason(), None);
1940 assert!(planned.is_planned());
1941
1942 assert_eq!(succeeded.kind(), OutputStatusKind::Succeeded);
1943 assert!(succeeded.is_succeeded());
1944
1945 assert_eq!(failed.kind(), OutputStatusKind::Failed);
1946 assert!(failed.is_failed());
1947 assert!(!failed.is_validation_failed());
1948
1949 assert_eq!(skipped.kind(), OutputStatusKind::Skipped);
1950 assert_eq!(skipped.reason(), Some(ReportReasonCode::PriorFailure));
1951 assert!(skipped.is_skipped());
1952
1953 assert_eq!(dry_run.kind(), OutputStatusKind::DryRunPlanned);
1954 assert!(dry_run.is_dry_run_planned());
1955
1956 assert_eq!(validation_failed.kind(), OutputStatusKind::ValidationFailed);
1957 assert_eq!(validation_failed.validation(), Some(validation));
1958 assert!(validation_failed.is_failed());
1959 assert!(validation_failed.is_validation_failed());
1960 }
1961
1962 #[test]
1963 fn output_status_display_is_stable_and_safe() {
1964 assert_eq!(OutputStatus::planned().to_string(), "planned");
1965 assert_eq!(OutputStatus::succeeded().to_string(), "succeeded");
1966 assert_eq!(OutputStatus::failed().to_string(), "failed");
1967 assert_eq!(
1968 OutputStatus::skipped(ReportReasonCode::PriorFailure).to_string(),
1969 "skipped:prior_failure"
1970 );
1971 assert_eq!(
1972 OutputStatus::dry_run_planned().to_string(),
1973 "dry_run_planned"
1974 );
1975 assert_eq!(
1976 OutputStatus::validation_failed(ValidationStatus::required_but_failed(
1977 ReportReasonCode::MissingExactOutputRows
1978 ))
1979 .to_string(),
1980 "validation_failed:required_but_failed:missing_exact_output_rows"
1981 );
1982
1983 let debug = format!(
1984 "{:?}",
1985 OutputStatus::validation_failed(ValidationStatus::unavailable(
1986 ReportReasonCode::MissingTargetAccess
1987 ))
1988 );
1989 assert!(debug.contains("ValidationFailed"));
1990 assert!(!debug.contains("server="));
1991 assert!(!debug.contains("password"));
1992 }
1993
1994 #[test]
1995 fn workflow_status_kinds_expose_stable_codes() {
1996 assert_eq!(WorkflowStatusKind::Success.as_str(), "success");
1997 assert_eq!(
1998 WorkflowStatusKind::PartialSuccess.as_str(),
1999 "partial_success"
2000 );
2001 assert_eq!(WorkflowStatusKind::Failure.as_str(), "failure");
2002 assert_eq!(WorkflowStatusKind::Skipped.as_str(), "skipped");
2003 assert_eq!(WorkflowStatusKind::NoOp.as_str(), "no_op");
2004 assert_eq!(
2005 WorkflowStatusKind::PartialSuccess.to_string(),
2006 "partial_success"
2007 );
2008 }
2009
2010 #[test]
2011 fn workflow_status_variants_expose_kind_reasons_and_helpers() {
2012 let success = WorkflowStatus::success();
2013 let partial = WorkflowStatus::partial_success();
2014 let failure = WorkflowStatus::failure();
2015 let skipped = WorkflowStatus::skipped(ReportReasonCode::DryRun);
2016 let no_op = WorkflowStatus::no_op(ReportReasonCode::NotExecuted);
2017
2018 assert_eq!(success.kind(), WorkflowStatusKind::Success);
2019 assert_eq!(success.reason(), None);
2020 assert!(success.is_success());
2021
2022 assert_eq!(partial.kind(), WorkflowStatusKind::PartialSuccess);
2023 assert_eq!(partial.reason(), None);
2024 assert!(partial.is_partial_success());
2025
2026 assert_eq!(failure.kind(), WorkflowStatusKind::Failure);
2027 assert_eq!(failure.reason(), None);
2028 assert!(failure.is_failure());
2029
2030 assert_eq!(skipped.kind(), WorkflowStatusKind::Skipped);
2031 assert_eq!(skipped.reason(), Some(ReportReasonCode::DryRun));
2032 assert!(skipped.is_skipped());
2033
2034 assert_eq!(no_op.kind(), WorkflowStatusKind::NoOp);
2035 assert_eq!(no_op.reason(), Some(ReportReasonCode::NotExecuted));
2036 assert!(no_op.is_no_op());
2037 }
2038
2039 #[test]
2040 fn workflow_status_display_is_stable_and_safe() {
2041 assert_eq!(WorkflowStatus::success().to_string(), "success");
2042 assert_eq!(
2043 WorkflowStatus::partial_success().to_string(),
2044 "partial_success"
2045 );
2046 assert_eq!(WorkflowStatus::failure().to_string(), "failure");
2047 assert_eq!(
2048 WorkflowStatus::skipped(ReportReasonCode::DryRun).to_string(),
2049 "skipped:dry_run"
2050 );
2051 assert_eq!(
2052 WorkflowStatus::no_op(ReportReasonCode::NotExecuted).to_string(),
2053 "no_op:not_executed"
2054 );
2055
2056 let debug = format!(
2057 "{:?}",
2058 WorkflowStatus::skipped(ReportReasonCode::PriorFailure)
2059 );
2060 assert!(debug.contains("Skipped"));
2061 assert!(!debug.contains("server="));
2062 assert!(!debug.contains("password"));
2063 }
2064}