safe_migrate/rules/
views.rs1use 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}