vespertide_loader/
migrations.rs1use std::env;
2use std::fs;
3use std::path::PathBuf;
4
5use anyhow::{Context, Result};
6use vespertide_config::VespertideConfig;
7use vespertide_core::MigrationPlan;
8use vespertide_planner::validate_migration_plan;
9
10pub fn load_migrations(config: &VespertideConfig) -> Result<Vec<MigrationPlan>> {
12 let migrations_dir = config.migrations_dir();
13 if !migrations_dir.exists() {
14 return Ok(Vec::new());
15 }
16
17 let mut plans = Vec::new();
18 let entries = fs::read_dir(migrations_dir).context("read migrations directory")?;
19
20 for entry in entries {
21 let entry = entry.context("read directory entry")?;
22 let path = entry.path();
23 if path.is_file() {
24 let ext = path.extension().and_then(|s| s.to_str());
25 if ext == Some("json") || ext == Some("yaml") || ext == Some("yml") {
26 let content = fs::read_to_string(&path)
27 .with_context(|| format!("read migration file: {}", path.display()))?;
28
29 let plan: MigrationPlan = if ext == Some("json") {
30 serde_json::from_str(&content)
31 .with_context(|| format!("parse migration: {}", path.display()))?
32 } else {
33 serde_yaml::from_str(&content)
34 .with_context(|| format!("parse migration: {}", path.display()))?
35 };
36
37 validate_migration_plan(&plan)
39 .with_context(|| format!("validate migration: {}", path.display()))?;
40
41 plans.push(plan);
42 }
43 }
44 }
45
46 plans.sort_by_key(|p| p.version);
48 Ok(plans)
49}
50
51pub fn load_migrations_from_dir(
53 project_root: Option<PathBuf>,
54) -> Result<Vec<MigrationPlan>, Box<dyn std::error::Error>> {
55 let project_root = if let Some(root) = project_root {
57 root
58 } else {
59 let manifest_dir = env::var("CARGO_MANIFEST_DIR")
60 .map_err(|_| "CARGO_MANIFEST_DIR environment variable not set")?;
61 PathBuf::from(manifest_dir)
62 };
63
64 let config = crate::config::load_config_or_default(Some(project_root.clone()))
66 .map_err(|e| format!("Failed to load config: {}", e))?;
67
68 let migrations_dir = project_root.join(config.migrations_dir());
70 if !migrations_dir.exists() {
71 return Ok(Vec::new());
72 }
73
74 let mut plans = Vec::new();
75 let entries = fs::read_dir(&migrations_dir)
76 .map_err(|e| format!("Failed to read migrations directory: {}", e))?;
77
78 for entry in entries {
79 let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
80 let path = entry.path();
81 if path.is_file() {
82 let ext = path.extension().and_then(|s| s.to_str());
83 if ext == Some("json") || ext == Some("yaml") || ext == Some("yml") {
84 let content = fs::read_to_string(&path)
85 .context(format!("Failed to read migration file {}", path.display()))?;
86
87 let plan: MigrationPlan = if ext == Some("json") {
88 serde_json::from_str(&content).map_err(|e| {
89 format!("Failed to parse JSON migration {}: {}", path.display(), e)
90 })?
91 } else {
92 serde_yaml::from_str(&content).map_err(|e| {
93 format!("Failed to parse YAML migration {}: {}", path.display(), e)
94 })?
95 };
96
97 plans.push(plan);
98 }
99 }
100 }
101
102 plans.sort_by_key(|p| p.version);
104 Ok(plans)
105}
106
107pub fn load_migrations_at_compile_time() -> Result<Vec<MigrationPlan>, Box<dyn std::error::Error>> {
109 load_migrations_from_dir(None)
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use serial_test::serial;
116 use std::fs;
117 use tempfile::TempDir;
118
119 #[test]
120 fn test_load_migrations_from_dir_with_no_migrations_dir() {
121 let temp_dir = TempDir::new().unwrap();
122 let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf()));
123 assert!(result.is_ok());
124 assert_eq!(result.unwrap().len(), 0);
125 }
126
127 #[test]
128 fn test_load_migrations_from_dir_with_empty_migrations_dir() {
129 let temp_dir = TempDir::new().unwrap();
130 let migrations_dir = temp_dir.path().join("migrations");
131 fs::create_dir_all(&migrations_dir).unwrap();
132
133 let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf()));
134 assert!(result.is_ok());
135 assert_eq!(result.unwrap().len(), 0);
136 }
137
138 #[test]
139 fn test_load_migrations_from_dir_with_json_migration() {
140 let temp_dir = TempDir::new().unwrap();
141 let migrations_dir = temp_dir.path().join("migrations");
142 fs::create_dir_all(&migrations_dir).unwrap();
143
144 let migration_content = r#"{
145 "version": 1,
146 "actions": [
147 {
148 "type": "create_table",
149 "table": "users",
150 "columns": [
151 {
152 "name": "id",
153 "type": "integer",
154 "nullable": false
155 }
156 ],
157 "constraints": []
158 }
159 ]
160 }"#;
161
162 fs::write(migrations_dir.join("0001_test.json"), migration_content).unwrap();
163
164 let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf()));
165 assert!(result.is_ok());
166 let plans = result.unwrap();
167 assert_eq!(plans.len(), 1);
168 assert_eq!(plans[0].version, 1);
169 }
170
171 #[test]
172 fn test_load_migrations_from_dir_sorts_by_version() {
173 let temp_dir = TempDir::new().unwrap();
174 let migrations_dir = temp_dir.path().join("migrations");
175 fs::create_dir_all(&migrations_dir).unwrap();
176
177 let migration1 = r#"{"version": 2, "actions": []}"#;
178 let migration2 = r#"{"version": 1, "actions": []}"#;
179 let migration3 = r#"{"version": 3, "actions": []}"#;
180
181 fs::write(migrations_dir.join("0002_second.json"), migration1).unwrap();
182 fs::write(migrations_dir.join("0001_first.json"), migration2).unwrap();
183 fs::write(migrations_dir.join("0003_third.json"), migration3).unwrap();
184
185 let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf()));
186 assert!(result.is_ok());
187 let plans = result.unwrap();
188 assert_eq!(plans.len(), 3);
189 assert_eq!(plans[0].version, 1);
190 assert_eq!(plans[1].version, 2);
191 assert_eq!(plans[2].version, 3);
192 }
193
194 #[test]
195 fn test_load_migrations_from_dir_with_yaml_migration() {
196 let temp_dir = TempDir::new().unwrap();
197 let migrations_dir = temp_dir.path().join("migrations");
198 fs::create_dir_all(&migrations_dir).unwrap();
199
200 let migration_content = r#"---
201version: 1
202actions:
203 - type: create_table
204 table: users
205 columns:
206 - name: id
207 type: integer
208 nullable: false
209 constraints: []
210"#;
211
212 fs::write(migrations_dir.join("0001_test.yaml"), migration_content).unwrap();
213
214 let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf()));
215 assert!(result.is_ok());
216 let plans = result.unwrap();
217 assert_eq!(plans.len(), 1);
218 assert_eq!(plans[0].version, 1);
219 }
220
221 #[test]
222 fn test_load_migrations_from_dir_with_yml_migration() {
223 let temp_dir = TempDir::new().unwrap();
224 let migrations_dir = temp_dir.path().join("migrations");
225 fs::create_dir_all(&migrations_dir).unwrap();
226
227 let migration_content = r#"---
228version: 1
229actions:
230 - type: create_table
231 table: users
232 columns:
233 - name: id
234 type: integer
235 nullable: false
236 constraints: []
237"#;
238
239 fs::write(migrations_dir.join("0001_test.yml"), migration_content).unwrap();
240
241 let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf()));
242 assert!(result.is_ok());
243 let plans = result.unwrap();
244 assert_eq!(plans.len(), 1);
245 assert_eq!(plans[0].version, 1);
246 }
247
248 #[test]
249 fn test_load_migrations_from_dir_with_invalid_json() {
250 let temp_dir = TempDir::new().unwrap();
251 let migrations_dir = temp_dir.path().join("migrations");
252 fs::create_dir_all(&migrations_dir).unwrap();
253
254 let invalid_json = r#"{"version": 1, "actions": [invalid]}"#;
255 fs::write(migrations_dir.join("0001_invalid.json"), invalid_json).unwrap();
256
257 let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf()));
258 assert!(result.is_err());
259 let err_msg = result.unwrap_err().to_string();
260 assert!(err_msg.contains("Failed to parse JSON migration"));
261 }
262
263 #[test]
264 fn test_load_migrations_from_dir_with_invalid_yaml() {
265 let temp_dir = TempDir::new().unwrap();
266 let migrations_dir = temp_dir.path().join("migrations");
267 fs::create_dir_all(&migrations_dir).unwrap();
268
269 let invalid_yaml = r#"---
270version: 1
271actions:
272 - invalid: [syntax
273"#;
274 fs::write(migrations_dir.join("0001_invalid.yaml"), invalid_yaml).unwrap();
275
276 let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf()));
277 assert!(result.is_err());
278 let err_msg = result.unwrap_err().to_string();
279 assert!(err_msg.contains("Failed to parse YAML migration"));
280 }
281
282 #[test]
283 fn test_load_migrations_from_dir_with_unreadable_file() {
284 let temp_dir = TempDir::new().unwrap();
298 let migrations_dir = temp_dir.path().join("migrations");
299 fs::create_dir_all(&migrations_dir).unwrap();
300
301 let file_path = migrations_dir.join("0001_test.json");
302 fs::write(&file_path, r#"{"version": 1, "actions": []}"#).unwrap();
303
304 let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf()));
305 assert!(result.is_ok());
306 }
307
308 #[test]
309 #[serial]
310 fn test_load_migrations_from_dir_without_project_root() {
311 let original = env::var("CARGO_MANIFEST_DIR").ok();
313
314 unsafe {
318 env::remove_var("CARGO_MANIFEST_DIR");
319 }
320
321 let result = load_migrations_from_dir(None);
322 assert!(result.is_err());
323 let err_msg = result.unwrap_err().to_string();
324 assert!(err_msg.contains("CARGO_MANIFEST_DIR environment variable not set"));
325
326 if let Some(val) = original {
328 unsafe {
329 env::set_var("CARGO_MANIFEST_DIR", val);
330 }
331 }
332 }
333
334 #[test]
335 fn test_load_migrations_at_compile_time() {
336 let result = load_migrations_at_compile_time();
340 let _ = result;
344 }
345}