1use crate::analysis::mutations::{AlterTableActionMutation, Mutation};
3use crate::analysis::state::{AnalysisState, CascadeResult, MutationResult};
4use crate::engine::config::Config;
5use crate::model::relation::Persistence;
6use crate::report::violations::{ObjectKind, OperationKind, Violation, ViolationTier};
7use crate::rules::Rule;
8
9pub struct PartitionLockRule;
10
11impl Rule for PartitionLockRule {
12 fn id(&self) -> &'static str {
13 "blocking-partition-mutation"
14 }
15 fn default_tier(&self) -> ViolationTier {
16 ViolationTier::Tier1
17 }
18 fn recipe(&self) -> &'static str {
19 "Attaching or detaching partitions takes an ACCESS EXCLUSIVE lock on the parent table. Run ATTACH PARTITION concurrently (or manage locks explicitly during low traffic)."
20 }
21
22 fn evaluate(
23 &self,
24 mutation: &Mutation,
25 result: &MutationResult,
26 pre_state: &crate::analysis::state::PreState,
27 state: &AnalysisState,
28 config: &Config,
29 _cascade: Option<&CascadeResult>,
30 ) -> Vec<Violation> {
31 if *result == MutationResult::Skipped {
32 return vec![];
33 }
34
35 let mut violations = Vec::new();
36
37 if let Mutation::AlterTable(alter) = mutation {
38 match &alter.action {
39 AlterTableActionMutation::AttachPartition { .. }
40 | AlterTableActionMutation::DetachPartition { .. } => {
41 let (is_temp, is_stale, rows, is_hash_partitioned) =
42 match pre_state.relations.get(&alter.id) {
43 Some(rel) => {
44 let stale =
45 rel.is_stale() && state.baseline_relations.contains(&alter.id);
46 let is_hash = rel
47 .partition_type
48 .as_ref()
49 .is_some_and(|pt| pt.to_uppercase().contains("HASH"));
50 (
51 rel.persistence == Persistence::Temporary,
52 stale,
53 rel.estimated_rows.unwrap_or(config.default_rows),
54 is_hash,
55 )
56 }
57 None => (false, true, config.default_rows, false),
58 };
59
60 if is_temp {
61 return violations;
62 }
63
64 let op_kind = if matches!(
65 alter.action,
66 AlterTableActionMutation::AttachPartition { .. }
67 ) {
68 OperationKind::AttachPartition
69 } else {
70 OperationKind::DetachPartition
71 };
72
73 if is_stale {
74 let key = format!("{}_stale_{}", self.id(), alter.id);
75 violations.push(Violation { source_range: None,
76 rule_id: self.id(),
77 operation_kind: op_kind.clone(),
78 object_kind: ObjectKind::Table,
79 object_name: alter.id.to_string(),
80 tier: ViolationTier::Tier2,
81 reason: format!("Table {} statistics are stale. Lock evaluations may be inaccurate.", alter.id),
82 recipe: "Run ANALYZE to ensure accurate row estimates before structural changes.",
83 dedup_key: Some(key),
84 sql: None,
85 });
86 }
87
88 let tier1_threshold = config.rule_tier1_threshold(self.id());
89 let tier2_threshold = config.rule_tier2_threshold(self.id());
90
91 let (adjusted_tier1, adjusted_tier2) = if is_hash_partitioned {
92 (tier1_threshold / 2, tier2_threshold / 2)
93 } else {
94 (tier1_threshold, tier2_threshold)
95 };
96
97 let tier = if rows >= adjusted_tier1 {
98 ViolationTier::Tier1
99 } else if rows >= adjusted_tier2 {
100 ViolationTier::Tier2
101 } else {
102 ViolationTier::Tier3
103 };
104
105 if tier != ViolationTier::Tier3 {
106 let op_name = if matches!(
107 alter.action,
108 AlterTableActionMutation::AttachPartition { .. }
109 ) {
110 "Attaching"
111 } else {
112 "Detaching"
113 };
114 let mut reason = format!(
115 "{} a partition on heavily utilized parent table {}",
116 op_name, alter.id
117 );
118 if is_hash_partitioned {
119 reason.push_str(" [HASH partitioning escalates lock severity]");
120 }
121 if is_stale {
122 reason.push_str(" [WARNING: Based on offline/stale statistics]");
123 }
124
125 violations.push(Violation {
126 source_range: None,
127 rule_id: self.id(),
128 operation_kind: op_kind,
129 object_kind: ObjectKind::Table,
130 object_name: alter.id.to_string(),
131 tier,
132 reason,
133 recipe: self.recipe(),
134 dedup_key: None,
135 sql: None,
136 });
137 }
138 }
139 _ => {}
140 }
141 }
142 violations
143 }
144}
145
146pub struct PartitionStrategyMismatchRule;
147
148impl Rule for PartitionStrategyMismatchRule {
149 fn id(&self) -> &'static str {
150 "partition-strategy-mismatch"
151 }
152 fn default_tier(&self) -> ViolationTier {
153 ViolationTier::Tier1
154 }
155 fn recipe(&self) -> &'static str {
156 "Ensure the partition being attached matches the parent table's partition strategy (RANGE/LIST/HASH). Mismatched strategies will cause ATTACH PARTITION to fail."
157 }
158
159 fn evaluate(
160 &self,
161 mutation: &Mutation,
162 result: &MutationResult,
163 pre_state: &crate::analysis::state::PreState,
164 _state: &AnalysisState,
165 _config: &Config,
166 _cascade: Option<&CascadeResult>,
167 ) -> Vec<Violation> {
168 if *result == MutationResult::Skipped {
169 return vec![];
170 }
171
172 let mut violations = Vec::new();
173
174 if let Mutation::AlterTable(alter) = mutation
175 && let AlterTableActionMutation::AttachPartition { child } = &alter.action
176 {
177 let parent_partition_type = pre_state
178 .relations
179 .get(&alter.id)
180 .and_then(|rel| rel.partition_type.clone());
181
182 let partition_partition_type = pre_state
183 .relations
184 .get(child)
185 .and_then(|rel| rel.partition_type.clone());
186
187 if let Some(parent_type) = parent_partition_type {
188 match partition_partition_type {
189 None => {
190 violations.push(Violation {
191 source_range: None,
192 rule_id: self.id(),
193 operation_kind: OperationKind::AttachPartition,
194 object_kind: ObjectKind::Table,
195 object_name: format!("{} -> {}", child, alter.id),
196 tier: self.default_tier(),
197 reason: format!(
198 "ATTACH PARTITION: partition {} has no partition strategy, but parent {} requires {}",
199 child, alter.id, parent_type
200 ),
201 recipe: self.recipe(),
202 dedup_key: None,
203 sql: None,
204 });
205 }
206 Some(part_type)
207 if !part_type
208 .to_uppercase()
209 .contains(&parent_type.to_uppercase()) =>
210 {
211 violations.push(Violation {
212 source_range: None,
213 rule_id: self.id(),
214 operation_kind: OperationKind::AttachPartition,
215 object_kind: ObjectKind::Table,
216 object_name: format!("{} -> {}", child, alter.id),
217 tier: self.default_tier(),
218 reason: format!(
219 "ATTACH PARTITION: partition {} is {} but parent {} is {} (mismatch)",
220 child, part_type, alter.id, parent_type
221 ),
222 recipe: self.recipe(),
223 dedup_key: None,
224 sql: None,
225 });
226 }
227 _ => {}
228 }
229 }
230 }
231 violations
232 }
233}