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