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