1use std::collections::{BTreeMap, BTreeSet};
4
5use serde::{Deserialize, Serialize};
6
7use crate::{MigrationDependencySpec, MigrationGraph};
8
9type MigrationKey = (String, String);
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct AppliedMigrationRecord {
14 pub app_label: String,
16 pub name: String,
18 pub checksum: String,
20 #[serde(default)]
22 pub applied_at: Option<String>,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum ValidationCode {
29 EmptyAppLabel,
31 EmptyName,
33 DuplicateMigration,
35 DuplicateMigrationNumber,
37 SelfDependency,
39 MissingDependency,
41 DependencyCycle,
43 DuplicateAppliedRecord,
45 UnknownAppliedMigration,
47 AppliedDependencyMissing,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct MigrationValidationError {
54 pub code: ValidationCode,
56 pub app_label: String,
58 pub name: String,
60 pub message: String,
62}
63
64pub fn validate_graph(
66 graph: &MigrationGraph,
67 applied: &[AppliedMigrationRecord],
68) -> Vec<MigrationValidationError> {
69 let mut errors = Vec::new();
70 let graph_keys = validate_loaded_migrations(graph, &mut errors);
71 validate_dependencies(graph, &graph_keys, &mut errors);
72 validate_cycles(graph, &graph_keys, &mut errors);
73 validate_applied_records(graph, applied, &graph_keys, &mut errors);
74 errors
75}
76
77fn validate_loaded_migrations(
78 graph: &MigrationGraph,
79 errors: &mut Vec<MigrationValidationError>,
80) -> BTreeSet<MigrationKey> {
81 let mut keys = BTreeSet::new();
82 let mut seen_keys = BTreeSet::new();
83 let mut numbers: BTreeMap<(String, String), String> = BTreeMap::new();
84
85 for migration in &graph.migrations {
86 if migration.app_label.is_empty() {
87 errors.push(error(
88 ValidationCode::EmptyAppLabel,
89 &migration.app_label,
90 &migration.name,
91 format!("Migration {} has an empty app label", migration.name),
92 ));
93 }
94 if migration.name.is_empty() {
95 errors.push(error(
96 ValidationCode::EmptyName,
97 &migration.app_label,
98 &migration.name,
99 format!("Migration {} has an empty name", migration.app_label),
100 ));
101 }
102
103 let key = (migration.app_label.clone(), migration.name.clone());
104 if !seen_keys.insert(key.clone()) {
105 errors.push(error(
106 ValidationCode::DuplicateMigration,
107 &migration.app_label,
108 &migration.name,
109 format!(
110 "Migration {}.{} is defined more than once",
111 migration.app_label, migration.name
112 ),
113 ));
114 }
115 keys.insert(key);
116
117 if let Some(number) = migration_number(&migration.name) {
118 let number_key = (migration.app_label.clone(), number);
119 if let Some(existing_name) = numbers.get(&number_key) {
120 if existing_name != &migration.name {
121 errors.push(error(
122 ValidationCode::DuplicateMigrationNumber,
123 &migration.app_label,
124 &migration.name,
125 format!(
126 "Migration {}.{} uses the same number as {}.{}",
127 migration.app_label, migration.name, migration.app_label, existing_name
128 ),
129 ));
130 }
131 } else {
132 numbers.insert(number_key, migration.name.clone());
133 }
134 }
135 }
136
137 keys
138}
139
140fn validate_dependencies(
141 graph: &MigrationGraph,
142 graph_keys: &BTreeSet<MigrationKey>,
143 errors: &mut Vec<MigrationValidationError>,
144) {
145 for migration in &graph.migrations {
146 let migration_key = (migration.app_label.clone(), migration.name.clone());
147 for dependency in &migration.dependencies {
148 let dependency_key = dependency_key(dependency);
149 if dependency_key == migration_key {
150 errors.push(error(
151 ValidationCode::SelfDependency,
152 &migration.app_label,
153 &migration.name,
154 format!(
155 "Migration {}.{} depends on itself",
156 migration.app_label, migration.name
157 ),
158 ));
159 } else if !graph_keys.contains(&dependency_key) {
160 errors.push(error(
161 ValidationCode::MissingDependency,
162 &migration.app_label,
163 &migration.name,
164 format!(
165 "Migration {}.{} depends on {}.{} which does not exist",
166 migration.app_label,
167 migration.name,
168 dependency.app_label,
169 dependency.migration_name
170 ),
171 ));
172 }
173 }
174 }
175}
176
177fn validate_cycles(
178 graph: &MigrationGraph,
179 graph_keys: &BTreeSet<MigrationKey>,
180 errors: &mut Vec<MigrationValidationError>,
181) {
182 let mut adjacency: BTreeMap<MigrationKey, Vec<MigrationKey>> = BTreeMap::new();
183 for migration in &graph.migrations {
184 let key = (migration.app_label.clone(), migration.name.clone());
185 let dependencies = migration
186 .dependencies
187 .iter()
188 .map(dependency_key)
189 .filter(|dependency| graph_keys.contains(dependency))
190 .collect();
191 adjacency.insert(key, dependencies);
192 }
193
194 let mut state: BTreeMap<MigrationKey, VisitState> = BTreeMap::new();
195 let mut stack = Vec::new();
196 let mut reported = BTreeSet::new();
197
198 for key in adjacency.keys() {
199 detect_cycle(
200 key,
201 &adjacency,
202 &mut state,
203 &mut stack,
204 &mut reported,
205 errors,
206 );
207 }
208}
209
210fn detect_cycle(
211 key: &MigrationKey,
212 adjacency: &BTreeMap<MigrationKey, Vec<MigrationKey>>,
213 state: &mut BTreeMap<MigrationKey, VisitState>,
214 stack: &mut Vec<MigrationKey>,
215 reported: &mut BTreeSet<String>,
216 errors: &mut Vec<MigrationValidationError>,
217) {
218 match state.get(key) {
219 Some(VisitState::Done) => return,
220 Some(VisitState::Visiting) => {
221 report_cycle(key, stack, reported, errors);
222 return;
223 }
224 None => {}
225 }
226
227 state.insert(key.clone(), VisitState::Visiting);
228 stack.push(key.clone());
229
230 if let Some(dependencies) = adjacency.get(key) {
231 for dependency in dependencies {
232 detect_cycle(dependency, adjacency, state, stack, reported, errors);
233 }
234 }
235
236 stack.pop();
237 state.insert(key.clone(), VisitState::Done);
238}
239
240fn report_cycle(
241 key: &MigrationKey,
242 stack: &[MigrationKey],
243 reported: &mut BTreeSet<String>,
244 errors: &mut Vec<MigrationValidationError>,
245) {
246 let Some(start) = stack.iter().position(|stack_key| stack_key == key) else {
247 return;
248 };
249 let mut cycle = stack[start..].to_vec();
250 cycle.push(key.clone());
251 let message_path = cycle
252 .iter()
253 .map(|(app_label, name)| format!("{app_label}.{name}"))
254 .collect::<Vec<_>>()
255 .join(" -> ");
256 if reported.insert(message_path.clone()) {
257 errors.push(error(
258 ValidationCode::DependencyCycle,
259 &key.0,
260 &key.1,
261 format!("Migration dependency cycle detected: {message_path}"),
262 ));
263 }
264}
265
266fn validate_applied_records(
267 graph: &MigrationGraph,
268 applied: &[AppliedMigrationRecord],
269 graph_keys: &BTreeSet<MigrationKey>,
270 errors: &mut Vec<MigrationValidationError>,
271) {
272 let mut applied_keys = BTreeSet::new();
273 for record in applied {
274 let key = (record.app_label.clone(), record.name.clone());
275 if !applied_keys.insert(key.clone()) {
276 errors.push(error(
277 ValidationCode::DuplicateAppliedRecord,
278 &record.app_label,
279 &record.name,
280 format!(
281 "Applied migration {}.{} is recorded more than once",
282 record.app_label, record.name
283 ),
284 ));
285 }
286 if !graph_keys.contains(&key) {
287 errors.push(error(
288 ValidationCode::UnknownAppliedMigration,
289 &record.app_label,
290 &record.name,
291 format!(
292 "Applied migration {}.{} is not present in the loaded graph",
293 record.app_label, record.name
294 ),
295 ));
296 }
297 }
298
299 for migration in &graph.migrations {
300 let key = (migration.app_label.clone(), migration.name.clone());
301 if !applied_keys.contains(&key) {
302 continue;
303 }
304 for dependency in &migration.dependencies {
305 let dependency_key = dependency_key(dependency);
306 if graph_keys.contains(&dependency_key) && !applied_keys.contains(&dependency_key) {
307 errors.push(error(
308 ValidationCode::AppliedDependencyMissing,
309 &migration.app_label,
310 &migration.name,
311 format!(
312 "Applied migration {}.{} depends on unapplied migration {}.{}",
313 migration.app_label,
314 migration.name,
315 dependency.app_label,
316 dependency.migration_name
317 ),
318 ));
319 }
320 }
321 }
322}
323
324fn dependency_key(dependency: &MigrationDependencySpec) -> MigrationKey {
325 (
326 dependency.app_label.clone(),
327 dependency.migration_name.clone(),
328 )
329}
330
331fn migration_number(name: &str) -> Option<String> {
332 let bytes = name.as_bytes();
333 if bytes.len() < 5 || bytes[4] != b'_' {
334 return None;
335 }
336 let prefix = &bytes[..4];
337 if prefix.iter().all(u8::is_ascii_digit) {
338 Some(name[..4].to_string())
339 } else {
340 None
341 }
342}
343
344fn error(
345 code: ValidationCode,
346 app_label: &str,
347 name: &str,
348 message: String,
349) -> MigrationValidationError {
350 MigrationValidationError {
351 code,
352 app_label: app_label.to_string(),
353 name: name.to_string(),
354 message,
355 }
356}
357
358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
359enum VisitState {
360 Visiting,
361 Done,
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367 use crate::{MigrationGraph, MigrationSpec};
368
369 fn migration(name: &str, dependencies: Vec<(&str, &str)>) -> MigrationSpec {
370 MigrationSpec {
371 app_label: "app".to_string(),
372 name: name.to_string(),
373 dependencies: dependencies
374 .into_iter()
375 .map(|(app_label, migration_name)| MigrationDependencySpec {
376 app_label: app_label.to_string(),
377 migration_name: migration_name.to_string(),
378 })
379 .collect(),
380 operations: Vec::new(),
381 checksum: Some(format!("{name}-checksum")),
382 reversible: true,
383 }
384 }
385
386 fn graph(migrations: Vec<MigrationSpec>) -> MigrationGraph {
387 MigrationGraph { migrations }
388 }
389
390 fn applied(name: &str) -> AppliedMigrationRecord {
391 AppliedMigrationRecord {
392 app_label: "app".to_string(),
393 name: name.to_string(),
394 checksum: format!("{name}-checksum"),
395 applied_at: Some("2026-06-05T00:00:00".to_string()),
396 }
397 }
398
399 fn codes(errors: &[MigrationValidationError]) -> Vec<ValidationCode> {
400 errors.iter().map(|error| error.code).collect()
401 }
402
403 #[test]
404 fn valid_graph_has_no_errors() {
405 let graph = graph(vec![
406 migration("0001_initial", vec![]),
407 migration("0002_next", vec![("app", "0001_initial")]),
408 ]);
409
410 assert_eq!(
411 validate_graph(&graph, &[applied("0001_initial")]),
412 Vec::new()
413 );
414 }
415
416 #[test]
417 fn duplicate_migration_identity_is_reported() {
418 let graph = graph(vec![
419 migration("0001_initial", vec![]),
420 migration("0001_initial", vec![]),
421 ]);
422
423 assert_eq!(
424 codes(&validate_graph(&graph, &[])),
425 vec![ValidationCode::DuplicateMigration]
426 );
427 }
428
429 #[test]
430 fn duplicate_migration_number_is_reported() {
431 let graph = graph(vec![
432 migration("0001_initial", vec![]),
433 migration("0001_second", vec![]),
434 ]);
435
436 assert_eq!(
437 codes(&validate_graph(&graph, &[])),
438 vec![ValidationCode::DuplicateMigrationNumber]
439 );
440 }
441
442 #[test]
443 fn missing_dependency_is_reported() {
444 let graph = graph(vec![migration("0002_next", vec![("app", "0001_initial")])]);
445
446 assert_eq!(
447 codes(&validate_graph(&graph, &[])),
448 vec![ValidationCode::MissingDependency]
449 );
450 }
451
452 #[test]
453 fn self_dependency_is_reported() {
454 let graph = graph(vec![migration(
455 "0001_initial",
456 vec![("app", "0001_initial")],
457 )]);
458
459 assert_eq!(
460 codes(&validate_graph(&graph, &[])),
461 vec![
462 ValidationCode::SelfDependency,
463 ValidationCode::DependencyCycle
464 ]
465 );
466 }
467
468 #[test]
469 fn dependency_cycle_is_reported() {
470 let graph = graph(vec![
471 migration("0001_initial", vec![("app", "0002_next")]),
472 migration("0002_next", vec![("app", "0001_initial")]),
473 ]);
474
475 assert_eq!(
476 codes(&validate_graph(&graph, &[])),
477 vec![ValidationCode::DependencyCycle]
478 );
479 }
480
481 #[test]
482 fn unknown_applied_record_is_reported() {
483 let graph = graph(vec![migration("0001_initial", vec![])]);
484
485 assert_eq!(
486 codes(&validate_graph(&graph, &[applied("0002_unknown")])),
487 vec![ValidationCode::UnknownAppliedMigration]
488 );
489 }
490
491 #[test]
492 fn applied_dependency_gap_is_reported() {
493 let graph = graph(vec![
494 migration("0001_initial", vec![]),
495 migration("0002_next", vec![("app", "0001_initial")]),
496 ]);
497
498 assert_eq!(
499 codes(&validate_graph(&graph, &[applied("0002_next")])),
500 vec![ValidationCode::AppliedDependencyMissing]
501 );
502 }
503}