1use super::scanner::CodeReference;
4use crate::ast::{Action, QailCmd};
5use crate::migrate::Schema;
6use std::collections::HashMap;
7
8#[derive(Debug, Default)]
10pub struct MigrationImpact {
11 pub breaking_changes: Vec<BreakingChange>,
13 pub warnings: Vec<Warning>,
15 pub safe_to_run: bool,
17 pub affected_files: usize,
19}
20
21#[derive(Debug)]
23pub enum BreakingChange {
24 DroppedColumn {
26 table: String,
27 column: String,
28 references: Vec<CodeReference>,
29 },
30 DroppedTable {
32 table: String,
33 references: Vec<CodeReference>,
34 },
35 RenamedColumn {
37 table: String,
38 old_name: String,
39 new_name: String,
40 references: Vec<CodeReference>,
41 },
42 TypeChanged {
44 table: String,
45 column: String,
46 old_type: String,
47 new_type: String,
48 references: Vec<CodeReference>,
49 },
50}
51
52#[derive(Debug)]
54pub enum Warning {
55 OrphanedReference {
57 table: String,
58 references: Vec<CodeReference>,
59 },
60}
61
62impl MigrationImpact {
63 pub fn analyze(
65 commands: &[QailCmd],
66 code_refs: &[CodeReference],
67 _old_schema: &Schema,
68 _new_schema: &Schema,
69 ) -> Self {
70 let mut impact = MigrationImpact::default();
71
72 let mut table_refs: HashMap<String, Vec<&CodeReference>> = HashMap::new();
74 let mut column_refs: HashMap<(String, String), Vec<&CodeReference>> = HashMap::new();
75
76 for code_ref in code_refs {
77 table_refs
78 .entry(code_ref.table.clone())
79 .or_default()
80 .push(code_ref);
81
82 for col in &code_ref.columns {
83 column_refs
84 .entry((code_ref.table.clone(), col.clone()))
85 .or_default()
86 .push(code_ref);
87 }
88 }
89
90 for cmd in commands {
92 match cmd.action {
93 Action::Drop => {
94 if let Some(refs) = table_refs.get(&cmd.table)
96 && !refs.is_empty()
97 {
98 impact.breaking_changes.push(BreakingChange::DroppedTable {
99 table: cmd.table.clone(),
100 references: refs.iter().map(|r| (*r).clone()).collect(),
101 });
102 }
103 }
104 Action::AlterDrop => {
105 for col_expr in &cmd.columns {
107 if let crate::ast::Expr::Named(col_name) = col_expr {
108 let key = (cmd.table.clone(), col_name.clone());
109 if let Some(refs) = column_refs.get(&key)
110 && !refs.is_empty()
111 {
112 impact.breaking_changes.push(BreakingChange::DroppedColumn {
113 table: cmd.table.clone(),
114 column: col_name.clone(),
115 references: refs.iter().map(|r| (*r).clone()).collect(),
116 });
117 }
118 }
119 }
120 }
121 Action::Mod => {
122 if let Some(refs) = table_refs.get(&cmd.table)
126 && !refs.is_empty()
127 {
128 impact.breaking_changes.push(BreakingChange::RenamedColumn {
129 table: cmd.table.clone(),
130 old_name: "unknown".to_string(),
131 new_name: "unknown".to_string(),
132 references: refs.iter().map(|r| (*r).clone()).collect(),
133 });
134 }
135 }
136 _ => {}
137 }
138 }
139
140 let mut affected: std::collections::HashSet<_> = std::collections::HashSet::new();
142 for change in &impact.breaking_changes {
143 match change {
144 BreakingChange::DroppedColumn { references, .. }
145 | BreakingChange::DroppedTable { references, .. }
146 | BreakingChange::RenamedColumn { references, .. }
147 | BreakingChange::TypeChanged { references, .. } => {
148 for r in references {
149 affected.insert(r.file.clone());
150 }
151 }
152 }
153 }
154 impact.affected_files = affected.len();
155 impact.safe_to_run = impact.breaking_changes.is_empty();
156
157 impact
158 }
159
160 pub fn report(&self) -> String {
162 let mut output = String::new();
163
164 if self.safe_to_run {
165 output.push_str("✓ Migration is safe to run\n");
166 return output;
167 }
168
169 output.push_str("⚠️ BREAKING CHANGES DETECTED\n\n");
170 output.push_str(&format!("Affected files: {}\n\n", self.affected_files));
171
172 for change in &self.breaking_changes {
173 match change {
174 BreakingChange::DroppedColumn {
175 table,
176 column,
177 references,
178 } => {
179 output.push_str(&format!(
180 "DROP COLUMN {}.{} ({} references)\n",
181 table,
182 column,
183 references.len()
184 ));
185 for r in references.iter().take(5) {
186 output.push_str(&format!(
187 " ❌ {}:{} → {}\n",
188 r.file.display(),
189 r.line,
190 r.snippet
191 ));
192 }
193 if references.len() > 5 {
194 output.push_str(&format!(" ... and {} more\n", references.len() - 5));
195 }
196 output.push('\n');
197 }
198 BreakingChange::DroppedTable { table, references } => {
199 output.push_str(&format!(
200 "DROP TABLE {} ({} references)\n",
201 table,
202 references.len()
203 ));
204 for r in references.iter().take(5) {
205 output.push_str(&format!(
206 " ❌ {}:{} → {}\n",
207 r.file.display(),
208 r.line,
209 r.snippet
210 ));
211 }
212 output.push('\n');
213 }
214 BreakingChange::RenamedColumn {
215 table,
216 old_name,
217 new_name,
218 references,
219 } => {
220 output.push_str(&format!(
221 "RENAME {}.{} → {} ({} references)\n",
222 table,
223 old_name,
224 new_name,
225 references.len()
226 ));
227 for r in references.iter().take(5) {
228 output.push_str(&format!(
229 " ⚠️ {}:{} → {}\n",
230 r.file.display(),
231 r.line,
232 r.snippet
233 ));
234 }
235 output.push('\n');
236 }
237 BreakingChange::TypeChanged {
238 table,
239 column,
240 old_type,
241 new_type,
242 references,
243 } => {
244 output.push_str(&format!(
245 "TYPE CHANGE {}.{}: {} → {} ({} references)\n",
246 table,
247 column,
248 old_type,
249 new_type,
250 references.len()
251 ));
252 for r in references.iter().take(5) {
253 output.push_str(&format!(
254 " ⚠️ {}:{} → {}\n",
255 r.file.display(),
256 r.line,
257 r.snippet
258 ));
259 }
260 output.push('\n');
261 }
262 }
263 }
264
265 output
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272 use std::path::PathBuf;
273
274 #[test]
275 fn test_detect_dropped_table() {
276 let cmd = QailCmd {
277 action: Action::Drop,
278 table: "users".to_string(),
279 ..Default::default()
280 };
281
282 let code_ref = CodeReference {
283 file: PathBuf::from("src/handlers.rs"),
284 line: 42,
285 table: "users".to_string(),
286 columns: vec!["name".to_string()],
287 query_type: super::super::scanner::QueryType::Qail,
288 snippet: "get::users".to_string(),
289 };
290
291 let old_schema = Schema::new();
292 let new_schema = Schema::new();
293
294 let impact = MigrationImpact::analyze(&[cmd], &[code_ref], &old_schema, &new_schema);
295
296 assert!(!impact.safe_to_run);
297 assert_eq!(impact.breaking_changes.len(), 1);
298 }
299}