Skip to main content

safe_migrate/_internal/rules/
mod.rs

1pub mod conflict;
2pub mod constraints;
3pub mod destructive;
4pub mod drift;
5pub mod expressions;
6pub mod functions;
7pub mod idempotency;
8pub mod indexes;
9pub mod opaque;
10pub mod partitions;
11pub mod policies;
12pub mod registry;
13pub mod security;
14pub mod timeouts;
15pub mod transactions;
16pub mod triggers;
17pub mod views;
18
19use crate::_internal::analysis::evidence::EvidenceRecord;
20use crate::_internal::analysis::mutations::Mutation;
21use crate::_internal::analysis::state::{AnalysisState, CascadeResult, Confidence, MutationResult};
22use crate::_internal::engine::config::Config;
23use crate::_internal::report::violations::{Violation, ViolationTier};
24
25/// Read-only inputs supplied to a rule for one analyzed mutation.
26///
27/// Keeping this bundle immutable prevents the engine from accidentally
28/// evaluating a rule against state from a different statement and provides a
29/// single extension point for evidence/capability metadata.
30pub(crate) struct TransitionRecord<'a> {
31    mutation: &'a Mutation,
32    result: &'a MutationResult,
33    pre_state: &'a crate::_internal::analysis::state::PreState,
34    cascade_closure: Option<&'a CascadeResult>,
35    evidence: &'a [EvidenceRecord],
36    confidence: &'a Confidence,
37}
38
39impl<'a> TransitionRecord<'a> {
40    fn new(
41        mutation: &'a Mutation,
42        result: &'a MutationResult,
43        pre_state: &'a crate::_internal::analysis::state::PreState,
44        cascade_closure: Option<&'a CascadeResult>,
45        evidence: &'a [EvidenceRecord],
46        confidence: &'a Confidence,
47    ) -> Self {
48        Self {
49            mutation,
50            result,
51            pre_state,
52            cascade_closure,
53            evidence,
54            confidence,
55        }
56    }
57}
58
59pub struct RuleContext<'a> {
60    pub(crate) transition: TransitionRecord<'a>,
61    pub(crate) state: &'a AnalysisState,
62    pub(crate) config: &'a Config,
63}
64
65/// Semantic state surfaces a rule must account for before claiming an exact
66/// result. Declarations are checked centrally so new rules cannot silently
67/// depend on an untracked part of the transition state.
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub enum RuleCapability {
70    BaselineRelations,
71    CatalogDependencies,
72    RowStatistics,
73    TransactionState,
74    FunctionCatalog,
75}
76
77impl RuleCapability {
78    pub(crate) fn available(self, state: &AnalysisState) -> bool {
79        if self == Self::TransactionState {
80            return state.evidence().iter().all(|record| {
81                record.code != crate::_internal::analysis::evidence::EvidenceCode::TransactionStateUnknown
82            });
83        }
84        if !state.baseline_is_available() {
85            return false;
86        }
87        let family = match self {
88            Self::BaselineRelations | Self::RowStatistics => {
89                crate::_internal::db::cache::CatalogFamily::Relations
90            }
91            Self::CatalogDependencies => crate::_internal::db::cache::CatalogFamily::Dependencies,
92            Self::FunctionCatalog => crate::_internal::db::cache::CatalogFamily::Routines,
93            Self::TransactionState => unreachable!(),
94        };
95        state.baseline_has_coverage(family)
96    }
97
98    /// Check capability availability for the concrete transition being
99    /// evaluated. Catalog-family coverage alone cannot prove row statistics:
100    /// PostgreSQL legitimately reports an unknown estimate for an individual
101    /// relation that has never been analyzed. Keep that uncertainty scoped to
102    /// rules whose finding actually depends on the affected relation.
103    pub(crate) fn available_for(
104        self,
105        state: &AnalysisState,
106        mutation: &crate::_internal::analysis::mutations::Mutation,
107        pre_state: &crate::_internal::analysis::state::PreState,
108    ) -> bool {
109        if self != Self::RowStatistics {
110            return self.available(state);
111        }
112        if !self.available(state) {
113            return false;
114        }
115
116        let mut targets = Vec::new();
117        match mutation {
118            crate::_internal::analysis::mutations::Mutation::AlterTable(alter) => {
119                targets.push(&alter.id);
120                if let crate::_internal::analysis::mutations::AlterTableActionMutation::AddForeignKey {
121                    to_table,
122                    ..
123                } = &alter.action
124                {
125                    targets.push(to_table);
126                }
127            }
128            crate::_internal::analysis::mutations::Mutation::CreateIndex(create) => {
129                targets.push(&create.table);
130            }
131            crate::_internal::analysis::mutations::Mutation::RefreshMaterializedView(refresh) => {
132                targets.push(&refresh.id);
133            }
134            crate::_internal::analysis::mutations::Mutation::DropIndex(drop) => {
135                for index_id in &drop.ids {
136                    targets.extend(pre_state.indexes.iter().filter_map(|edge| {
137                        if edge.dependent == *index_id {
138                            if let crate::_internal::analysis::graph::DependencyKind::IndexOnRelation {
139                                ..
140                            } = edge.kind
141                            {
142                                Some(&edge.referenced)
143                            } else {
144                                None
145                            }
146                        } else {
147                            None
148                        }
149                    }));
150                }
151            }
152            _ => {}
153        }
154
155        // A capability can be declared by a rule that has findings for a
156        // different mutation family. In that case there is no relation-local
157        // statistic to require.
158        targets.into_iter().all(|id| {
159            pre_state
160                .relations
161                .get(id)
162                .is_some_and(|relation| relation.estimated_rows.is_some())
163        })
164    }
165
166    pub(crate) const fn evidence_code(self) -> crate::_internal::analysis::evidence::EvidenceCode {
167        match self {
168            Self::BaselineRelations
169            | Self::CatalogDependencies
170            | Self::RowStatistics
171            | Self::FunctionCatalog => {
172                crate::_internal::analysis::evidence::EvidenceCode::CatalogCoverageIncomplete
173            }
174            Self::TransactionState => {
175                crate::_internal::analysis::evidence::EvidenceCode::TransactionStateUnknown
176            }
177        }
178    }
179}
180
181pub(crate) const BASELINE_STATS_CAPABILITIES: &[RuleCapability] = &[
182    RuleCapability::BaselineRelations,
183    RuleCapability::RowStatistics,
184];
185pub(crate) const BASELINE_RELATION_CAPABILITIES: &[RuleCapability] =
186    &[RuleCapability::BaselineRelations];
187pub(crate) const FUNCTION_CAPABILITIES: &[RuleCapability] = &[RuleCapability::FunctionCatalog];
188pub(crate) const FUNCTION_DEPENDENCY_CAPABILITIES: &[RuleCapability] = &[
189    RuleCapability::FunctionCatalog,
190    RuleCapability::CatalogDependencies,
191];
192pub(crate) const BASELINE_STATS_DEPENDENCY_CAPABILITIES: &[RuleCapability] = &[
193    RuleCapability::BaselineRelations,
194    RuleCapability::CatalogDependencies,
195    RuleCapability::RowStatistics,
196];
197pub(crate) const TRANSACTION_CAPABILITIES: &[RuleCapability] = &[RuleCapability::TransactionState];
198
199impl<'a> RuleContext<'a> {
200    pub(crate) fn new(
201        mutation: &'a Mutation,
202        result: &'a MutationResult,
203        pre_state: &'a crate::_internal::analysis::state::PreState,
204        state: &'a AnalysisState,
205        config: &'a Config,
206        cascade_closure: Option<&'a CascadeResult>,
207    ) -> Self {
208        Self {
209            transition: TransitionRecord::new(
210                mutation,
211                result,
212                pre_state,
213                cascade_closure,
214                state.evidence(),
215                state.confidence(),
216            ),
217            state,
218            config,
219        }
220    }
221
222    pub fn evidence(&self) -> &[EvidenceRecord] {
223        self.transition.evidence
224    }
225
226    pub fn confidence(&self) -> &Confidence {
227        self.transition.confidence
228    }
229
230    pub fn mutation(&self) -> &Mutation {
231        self.transition.mutation
232    }
233
234    pub fn result(&self) -> &MutationResult {
235        self.transition.result
236    }
237
238    pub fn pre_state(&self) -> &crate::_internal::analysis::state::PreState {
239        self.transition.pre_state
240    }
241
242    pub fn state(&self) -> &AnalysisState {
243        self.state
244    }
245
246    pub fn config(&self) -> &Config {
247        self.config
248    }
249
250    pub fn cascade_closure(&self) -> Option<&CascadeResult> {
251        self.transition.cascade_closure
252    }
253}
254
255/// Supported rule interface. Implementations receive one immutable context
256/// object, so future inputs can be added without another argument explosion.
257pub trait Rule {
258    fn id(&self) -> &'static str;
259    fn default_tier(&self) -> ViolationTier;
260    fn recipe(&self) -> &'static str;
261
262    fn required_capabilities(&self) -> &'static [RuleCapability] {
263        &[]
264    }
265
266    fn evaluate(&self, context: &RuleContext<'_>) -> Vec<Violation>;
267}