safe_migrate/_internal/rules/
mod.rs1pub 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
25pub(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#[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 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 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
255pub 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}