Skip to main content

safe_migrate/rules/
views.rs

1// FILE: src/rules/views.rs
2use crate::analysis::mutations::Mutation;
3use crate::analysis::state::{AnalysisState, CascadeResult, MutationResult};
4use crate::ast::identifiers::ObjectId;
5use crate::engine::config::Config;
6use crate::model::relation::{Persistence, RelationState};
7use crate::report::violations::{Violation, ViolationTier};
8use crate::rules::Rule;
9use std::collections::HashMap;
10
11pub struct MaterializedViewRefreshRule;
12
13impl Rule for MaterializedViewRefreshRule {
14    fn id(&self) -> &'static str {
15        "blocking-mat-view-refresh"
16    }
17    fn default_tier(&self) -> ViolationTier {
18        ViolationTier::Tier1
19    }
20    fn recipe(&self) -> &'static str {
21        "Refreshing a materialized view without CONCURRENTLY prevents reading from it during the refresh."
22    }
23
24    fn evaluate(
25        &self,
26        mutation: &Mutation,
27        result: &MutationResult,
28        pre_relations: &HashMap<ObjectId, RelationState>,
29        state: &AnalysisState,
30        config: &Config,
31        _cascade: Option<&CascadeResult>,
32    ) -> Vec<Violation> {
33        if *result == MutationResult::Skipped {
34            return vec![];
35        }
36
37        let mut violations = Vec::new();
38
39        if let Mutation::RefreshMaterializedView(refresh) = mutation
40            && !refresh.concurrently
41        {
42            let (is_temp, is_stale, rows) = match pre_relations.get(&refresh.id) {
43                Some(rel) => {
44                    let stale = rel.is_stale() && state.baseline_relations.contains(&refresh.id);
45                    (
46                        rel.persistence == Persistence::Temporary,
47                        stale,
48                        rel.estimated_rows.unwrap_or(config.default_rows),
49                    )
50                }
51                None => (false, true, config.default_rows),
52            };
53
54            if is_temp {
55                return violations;
56            }
57
58            if is_stale {
59                let key = format!("{}_stale_{}", self.id(), refresh.id);
60                violations.push(Violation {
61                    rule_id: self.id(),
62                    title: format!("Materialized view {} statistics are stale. Lock evaluations may be inaccurate.", refresh.id),
63                    tier: ViolationTier::Tier2,
64                    recipe: "Run ANALYZE to ensure accurate row estimates.",
65                    dedup_key: Some(key),
66                });
67            }
68
69            let tier1_threshold = config.rule_tier1_threshold(self.id());
70            let tier2_threshold = config.rule_tier2_threshold(self.id());
71
72            let tier = if rows >= tier1_threshold {
73                ViolationTier::Tier1
74            } else if rows >= tier2_threshold {
75                ViolationTier::Tier2
76            } else {
77                ViolationTier::Tier3
78            };
79
80            if tier != ViolationTier::Tier3 {
81                let mut title = format!("Blocking materialized view refresh on {}", refresh.id);
82                if is_stale {
83                    title.push_str(" [WARNING: Based on offline/stale statistics]");
84                }
85
86                violations.push(Violation {
87                    rule_id: self.id(),
88                    title,
89                    tier,
90                    recipe: self.recipe(),
91                    dedup_key: None,
92                });
93            }
94        }
95        violations
96    }
97}