Skip to main content

safe_migrate/rules/
partitions.rs

1// FILE: src/rules/partitions.rs
2use crate::analysis::mutations::{AlterTableActionMutation, 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 PartitionLockRule;
12
13impl Rule for PartitionLockRule {
14    fn id(&self) -> &'static str {
15        "blocking-partition-mutation"
16    }
17    fn default_tier(&self) -> ViolationTier {
18        ViolationTier::Tier1
19    }
20    fn recipe(&self) -> &'static str {
21        "Attaching or detaching partitions takes an ACCESS EXCLUSIVE lock on the parent table. Run ATTACH PARTITION concurrently (or manage locks explicitly during low traffic)."
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::AlterTable(alter) = mutation {
40            match &alter.action {
41                AlterTableActionMutation::AttachPartition { .. }
42                | AlterTableActionMutation::DetachPartition { .. } => {
43                    let (is_temp, is_stale, rows) = match pre_relations.get(&alter.id) {
44                        Some(rel) => {
45                            let stale =
46                                rel.is_stale() && state.baseline_relations.contains(&alter.id);
47                            (
48                                rel.persistence == Persistence::Temporary,
49                                stale,
50                                rel.estimated_rows.unwrap_or(config.default_rows),
51                            )
52                        }
53                        None => (false, true, config.default_rows),
54                    };
55
56                    if is_temp {
57                        return violations;
58                    }
59
60                    if is_stale {
61                        let key = format!("{}_stale_{}", self.id(), alter.id);
62                        violations.push(Violation {
63                            rule_id: self.id(),
64                            title: format!("Table {} statistics are stale. Lock evaluations may be inaccurate.", alter.id),
65                            tier: ViolationTier::Tier2,
66                            recipe: "Run ANALYZE to ensure accurate row estimates before structural changes.",
67                            dedup_key: Some(key),
68                        });
69                    }
70
71                    let tier1_threshold = config.rule_tier1_threshold(self.id());
72                    let tier2_threshold = config.rule_tier2_threshold(self.id());
73
74                    let tier = if rows >= tier1_threshold {
75                        ViolationTier::Tier1
76                    } else if rows >= tier2_threshold {
77                        ViolationTier::Tier2
78                    } else {
79                        ViolationTier::Tier3
80                    };
81
82                    if tier != ViolationTier::Tier3 {
83                        let op_name = if matches!(
84                            alter.action,
85                            AlterTableActionMutation::AttachPartition { .. }
86                        ) {
87                            "Attaching"
88                        } else {
89                            "Detaching"
90                        };
91                        let mut title = format!(
92                            "{} a partition on heavily utilized parent table {}",
93                            op_name, alter.id
94                        );
95                        if is_stale {
96                            title.push_str(" [WARNING: Based on offline/stale statistics]");
97                        }
98
99                        violations.push(Violation {
100                            rule_id: self.id(),
101                            title,
102                            tier,
103                            recipe: self.recipe(),
104                            dedup_key: None,
105                        });
106                    }
107                }
108                _ => {}
109            }
110        }
111        violations
112    }
113}