qail_core/migrate/
named_migration.rs1use std::collections::HashSet;
11
12#[derive(Debug, Clone, Default)]
14pub struct MigrationMeta {
15 pub name: String,
17 pub depends: Vec<String>,
19 pub author: Option<String>,
21 pub created: Option<String>,
23}
24
25impl MigrationMeta {
26 pub fn new(name: &str) -> Self {
28 Self {
29 name: name.to_string(),
30 ..Default::default()
31 }
32 }
33
34 pub fn with_depends(mut self, deps: Vec<String>) -> Self {
36 self.depends = deps;
37 self
38 }
39
40 pub fn with_author(mut self, author: &str) -> Self {
42 self.author = Some(author.to_string());
43 self
44 }
45
46 pub fn to_header(&self) -> String {
48 let mut lines = vec![format!("-- migration: {}", self.name)];
49
50 if !self.depends.is_empty() {
51 lines.push(format!("-- depends: {}", self.depends.join(", ")));
52 }
53
54 if let Some(ref author) = self.author {
55 lines.push(format!("-- author: {}", author));
56 }
57
58 if let Some(ref created) = self.created {
59 lines.push(format!("-- created: {}", created));
60 }
61
62 lines.push(String::new()); lines.join("\n")
64 }
65}
66
67pub fn parse_migration_meta(content: &str) -> Option<MigrationMeta> {
70 let mut meta = MigrationMeta::default();
71 let mut found_name = false;
72
73 for line in content.lines() {
74 let line = line.trim();
75
76 if let Some(name) = line.strip_prefix("-- migration:") {
77 meta.name = name.trim().to_string();
78 found_name = true;
79 } else if let Some(deps) = line.strip_prefix("-- depends:") {
80 meta.depends = deps
81 .split(',')
82 .map(|s| s.trim().to_string())
83 .filter(|s| !s.is_empty())
84 .collect();
85 } else if let Some(author) = line.strip_prefix("-- author:") {
86 meta.author = Some(author.trim().to_string());
87 } else if let Some(created) = line.strip_prefix("-- created:") {
88 meta.created = Some(created.trim().to_string());
89 } else if !line.starts_with("--") && !line.is_empty() {
90 break;
92 }
93 }
94
95 if found_name { Some(meta) } else { None }
96}
97
98pub fn validate_dependencies(migrations: &[MigrationMeta]) -> Result<Vec<String>, String> {
100 let names: HashSet<_> = migrations.iter().map(|m| m.name.as_str()).collect();
101
102 for mig in migrations {
103 for dep in &mig.depends {
104 if !names.contains(dep.as_str()) {
105 return Err(format!(
106 "Migration '{}' depends on '{}' which doesn't exist",
107 mig.name, dep
108 ));
109 }
110 }
111 }
112
113 let mut order = Vec::new();
115 let mut visited = HashSet::new();
116 let mut in_progress = HashSet::new();
117
118 fn visit<'a>(
119 name: &'a str,
120 migrations: &'a [MigrationMeta],
121 visited: &mut HashSet<&'a str>,
122 in_progress: &mut HashSet<&'a str>,
123 order: &mut Vec<String>,
124 ) -> Result<(), String> {
125 if in_progress.contains(name) {
126 return Err(format!("Circular dependency detected involving '{}'", name));
127 }
128 if visited.contains(name) {
129 return Ok(());
130 }
131
132 in_progress.insert(name);
133
134 if let Some(mig) = migrations.iter().find(|m| m.name == name) {
135 for dep in &mig.depends {
136 visit(dep, migrations, visited, in_progress, order)?;
137 }
138 }
139
140 in_progress.remove(name);
141 visited.insert(name);
142 order.push(name.to_string());
143
144 Ok(())
145 }
146
147 for mig in migrations {
148 visit(
149 &mig.name,
150 migrations,
151 &mut visited,
152 &mut in_progress,
153 &mut order,
154 )?;
155 }
156
157 Ok(order)
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn test_parse_migration_meta() {
166 let content = r#"-- migration: 003_add_avatars
167-- depends: 001_init, 002_add_users
168-- author: orion
169
170+table avatars {
171 id UUID primary_key
172}
173"#;
174 let meta = parse_migration_meta(content).unwrap();
175 assert_eq!(meta.name, "003_add_avatars");
176 assert_eq!(meta.depends, vec!["001_init", "002_add_users"]);
177 assert_eq!(meta.author, Some("orion".to_string()));
178 }
179
180 #[test]
181 fn test_meta_to_header() {
182 let meta = MigrationMeta::new("test_migration")
183 .with_depends(vec!["dep1".to_string()])
184 .with_author("tester");
185
186 let header = meta.to_header();
187 assert!(header.contains("-- migration: test_migration"));
188 assert!(header.contains("-- depends: dep1"));
189 assert!(header.contains("-- author: tester"));
190 }
191
192 #[test]
193 fn test_dependency_validation() {
194 let migs = vec![
195 MigrationMeta::new("001_init"),
196 MigrationMeta::new("002_users").with_depends(vec!["001_init".to_string()]),
197 MigrationMeta::new("003_posts").with_depends(vec!["002_users".to_string()]),
198 ];
199
200 let order = validate_dependencies(&migs).unwrap();
201 assert_eq!(order, vec!["001_init", "002_users", "003_posts"]);
202 }
203}