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