safe_migrate/_internal/analysis/
evidence.rs1use serde::Serialize;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
8#[serde(rename_all = "snake_case")]
9pub enum EvidenceCode {
10 BaselineUnavailable,
11 BaselineStale,
12 CatalogCoverageIncomplete,
13 UnsupportedStatement,
14 UnsupportedSemantics,
15 UnresolvedReference,
16 UnknownObjectState,
17 TransactionStateUnknown,
18 UnmodeledState,
19}
20
21impl EvidenceCode {
22 pub const fn as_str(self) -> &'static str {
23 match self {
24 Self::BaselineUnavailable => "baseline_unavailable",
25 Self::BaselineStale => "baseline_stale",
26 Self::CatalogCoverageIncomplete => "catalog_coverage_incomplete",
27 Self::UnsupportedStatement => "unsupported_statement",
28 Self::UnsupportedSemantics => "unsupported_semantics",
29 Self::UnresolvedReference => "unresolved_reference",
30 Self::UnknownObjectState => "unknown_object_state",
31 Self::TransactionStateUnknown => "transaction_state_unknown",
32 Self::UnmodeledState => "unmodeled_state",
33 }
34 }
35
36 pub const fn summary(self) -> &'static str {
37 match self {
38 Self::BaselineUnavailable => "no synchronized baseline was available",
39 Self::BaselineStale => "the synchronized baseline may be stale",
40 Self::CatalogCoverageIncomplete => {
41 "the synchronized catalog does not contain all required evidence"
42 }
43 Self::UnsupportedStatement => "the statement has no typed semantic model",
44 Self::UnsupportedSemantics => "the statement contains unsupported semantics",
45 Self::UnresolvedReference => "an object reference could not be resolved exactly",
46 Self::UnknownObjectState => "the required object state is unknown",
47 Self::TransactionStateUnknown => "transaction state cannot be modeled exactly",
48 Self::UnmodeledState => {
49 "required PostgreSQL state is deliberately outside the semantic model"
50 }
51 }
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
57#[serde(rename_all = "snake_case")]
58pub enum EvidenceScope {
59 Statement,
60 Chain,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
65pub struct EvidenceLocation {
66 pub file: String,
67 pub statement_index: usize,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
72pub struct EvidenceRecord {
73 pub code: EvidenceCode,
74 pub scope: EvidenceScope,
75 pub summary: &'static str,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub location: Option<EvidenceLocation>,
78}
79
80impl EvidenceRecord {
81 pub fn new(code: EvidenceCode, scope: EvidenceScope) -> Self {
82 Self {
83 code,
84 scope,
85 summary: code.summary(),
86 location: None,
87 }
88 }
89
90 pub fn at(mut self, location: EvidenceLocation) -> Self {
91 self.location = Some(location);
92 self
93 }
94}
95
96#[derive(Debug, Clone, Default, PartialEq, Eq)]
98pub struct EvidenceLog {
99 records: Vec<EvidenceRecord>,
100}
101
102impl EvidenceLog {
103 pub fn records(&self) -> &[EvidenceRecord] {
104 &self.records
105 }
106
107 pub fn contains(&self, record: &EvidenceRecord) -> bool {
108 self.records.contains(record)
109 }
110
111 pub fn insert(&mut self, record: EvidenceRecord) -> bool {
113 if self.contains(&record) {
114 return false;
115 }
116 self.records.push(record);
117 self.records.sort();
118 true
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn evidence_is_deduplicated_and_stably_ordered() {
128 let mut log = EvidenceLog::default();
129 let stale = EvidenceRecord::new(EvidenceCode::BaselineStale, EvidenceScope::Chain);
130 let unsupported =
131 EvidenceRecord::new(EvidenceCode::UnsupportedStatement, EvidenceScope::Statement).at(
132 EvidenceLocation {
133 file: "001.sql".to_string(),
134 statement_index: 2,
135 },
136 );
137
138 assert!(log.insert(unsupported.clone()));
139 assert!(log.insert(stale.clone()));
140 assert!(!log.insert(unsupported.clone()));
141 assert_eq!(log.records(), &[stale, unsupported]);
142 }
143}